尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

共享电单车运维App跨平台改造:共享81%代码的复盘

共享电单车运维App跨平台改造:共享81%代码的复盘 共享电单车运维 App 这块业务最难的不是功能多而是同一个功能要在 Android 和 iOS 上各写一遍。团队里每次排期业务方问得最多的就是“安卓先上iOS 什么时候能好”。后来我们痛下决心把整个运维 App 拆成了「共享 81% 宿主 19%」的结构——4 万行有效业务代码里三万多行走在共享层一遍开发两端生效剩下七千多行留在各自宿主工程里只处理绕不开的系统差异。今天这篇文章就是这次拆分的完整复盘从决策逻辑到落地细节再到我们踩过的坑一次性讲透。1. 共享电单车运维场景到底需要一款什么样的 App1.1 一线运维的工作流决定了 App 的功能边界共享电单车的运维不像网约车那样“线上派单、线下接单”就完了。一个运维小哥从早高峰前一小时开始要干的活非常碎找低电量车、给换电柜补电池、处理故障车、把停在禁停区的车挪到 P 点还要拍照上报破损车辆。这些动作全部压在一部手机上所以运维 App 的核心功能其实非常聚焦扫码开锁运维模式需要绕过用户端余额校验直接进入维修/调度状态车辆定位和寻车导航经常是“显示在附近但就是找不到车”的郊区角落电池电量实时读取决定要不要换电工单接收、流转、回传系统派单和自己抢单都支持故障上报拍照、选故障类型、选配件、上传P 点/禁停区判定车能不能在这个位置落锁这些功能有一个共同点业务逻辑高度固定但系统底层能力差异巨大。扫码要用相机定位要用 GPS电量读取要走蓝牙 BLE拍照上传要碰相册权限。业务规则集中在共享层做底层能力交给宿主层这个拆分逻辑就是从这种场景里自然长出来的。1.2 双端割裂带来的真实痛苦项目早期我们只有 Android 版运维 App因为一线小哥发的都是安卓定制机皮实、续航长、换电池方便。但过了半年管理岗和加盟商开始提意见他们日常用的是 iPhone想看车辆实时状态、核对异常工单总不能在裤兜里再揣一台安卓机。于是 iOS 版的需求被提上了日程。当时团队的第一反应是“照 Android 翻译一遍”觉得不就是同样的界面再来一次嘛。真正开始排期才发现问题地图 SDK、扫码拍照、蓝牙连接、消息推送每一层都要重新适配。最要命的是后续迭代运维规则是高频变化的比如“电量低于 30% 必须下线”调整为“低于 25%”、“P 点半径从 50 米改成 30 米”这种改动按传统双端流程要走两遍开发、两遍测试、两遍发版。也就是从那一刻起我们开始认真考虑“不用写两遍”的方案。1.3 “不是写两遍而是拆两层”这个决策是怎么来的一开始我们讨论过三个方向Web 套壳、纯原生双写、跨平台共享层。Web 套壳看着成本低但扫码连续对焦、蓝牙 BLE 连接、离线地图这类硬能力做不好直接被否掉纯原生双写就是维持现状只是工作量翻倍最后我们把重心放在跨平台共享层上让页面和业务逻辑写一份底层原生能力通过桥接给共享层调用。方案定下来之后我们又面临“共享层和宿主层到底各放多少东西”的问题。逐页拆解后得到的结果就是标题里那个比例整个运维 App 的有效业务代码约 4 万行其中共享部分做到了 81%宿主部分只有 19%。这个数据不是拍脑袋出来的而是细致盘点每个模块之后统计出来的。接下来我详细说说拆分的依据。2. 把 4 万行代码切成「共享 81% 宿主 19%」的依据2.1 拆分的第一原则跟业务走还是跟系统走我们内部定了一条非常朴素的判断标准这段代码的核心逻辑如果跟着业务规则变动就放进共享层如果跟着操作系统能力走就留在宿主层。比如“车辆电量低于多少阈值就不能开锁”这是业务规则以后改阈值只要动共享层但“怎么从车辆的蓝牙广播里读出电量字段”这是系统级通讯能力Android 和 iOS 的 BLE API 完全不一样必须各自原生实现。用这个标准把所有功能过一遍结论非常清晰。页面布局、表单校验、工单状态机、用户登录态、网络请求封装、埋点上报这些 90% 跟着公司业务走全进共享层。App 启动流程、推送注册、相机/相册调用、蓝牙扫描连接、后台定位权限这些 90% 跟着系统走留在宿主层。2.2 职责清单共享层和宿主层各管什么我把两边的职责整理成了表格开发的时候大家照着这个表划边界基本不会吵起来能力模块归共享层还是宿主层原因页面 UI 与交互共享层两端视觉必须一致业务文案调整频繁表单校验 / 状态管理共享层纯业务逻辑跟系统无关网络层封装 / API 请求共享层统一接口协议方便统一鉴权和埋点登录 / 账号体系共享层会话状态需要跨端一致规则变更一次生效工单流转 / 电池阈值判断共享层典型业务规则迭代频繁地图展示 / 聚合标记共享层地图 SDK 用桥接包裹后业务层可以共用蓝牙 BLE 扫描与连接宿主层Android/iOS 权限模型和 API 差异大摄像头调用 / 相册选取宿主层系统 UI 和权限流程不同推送注册与离线消息宿主层依赖各厂商推送通道后台定位与保活宿主层两端的后台策略完全不一样App 启动 / 生命周期宿主层入口和系统事件绑定2.3 为什么是 81:19而不是 90:10 或者 70:30有一个细节对比例影响特别大蓝牙 BLE 通信。共享电单车运维 App 里扫码之后要通过蓝牙和车辆建立连接读取电池信息、下发解锁指令这部分代码绕不开系统差异我们认为留两套实施成本最低。地图也类似高德地图在 Android 和 iOS 上的初始化方式、权限申请、卡片交互都不完全一致虽然用了桥接但底层适配代码还是留在了宿主侧。最终统计出来共享层约 3.2 万行宿主层约 0.8 万行加起来 4 万行有效业务代码。假设不做拆分按传统双端原生开发共享层这 3.2 万行逻辑至少要翻一倍宿主层本身也写两遍总代码量保守估计奔着 8 万行去了。所以“共享 81% 宿主 19%”本质上是把最肥的核心业务捞进了共享层把真正硬骨头留在宿主层单独啃。3. 共享层的 3.2 万行是怎么组织起来的3.1 技术选型为什么是共享层而不是双端 UI 复用我们把共享层定为 React Native既没有选 Flutter也没有继续加码纯原生。原因有两条第一现有团队的 Android 和 iOS 原生基础都不弱RN 的桥接模型对我们来说更顺手需要调原生能力时可以直接用现有实现第二后端接口已经是 JSON 为主RN 的 JavaScript 生态处理这类数据非常自然团队的 Web 开发经验也能平移一部分过来。这里有段背景可以补充下早期我们分析过 Flutter它的自绘 UI 引擎确实漂亮但我们的页面结构是标准的表单、列表、地图混排原生控件完全够用没必要引入一整套新的渲染引擎。再加上我们还需要“不发版本就调整业务规则”的能力RN 的 JSBundle 远程更新机制刚好能解决这个问题。3.2 三层结构页面、业务与基础设施共享层的内部结构我没有用扁平目录一拉到底而是分了三层每层职责明确越往上越接近具体业务页面层约占共享层 40%扫码页、地图寻车页、工单列表、工单详情、故障上报页、巡检打卡页、个人中心。这一层只做“展示 收集用户操作”不直接接触原生能力。业务层约占共享层 35%登录会话管理、工单状态机待接单、已接单、处理中、已完成、已取消、电池 SOC 判断、P 点围栏校验、车辆状态模型。这一层是业务规则最集中的地方也是改版频率最高的地方。基础设施层约占共享层 25%网络请求封装、统一日志与埋点、本地 SQLite 存储、权限请求的二次封装、通用 UI 组件库按钮、弹窗、扫码框、地图标记气泡。这个分层的价值在迁移期就体现出来了。我们一开始只把“页面 基础设施”搬过来业务层先用 mock 数据跑通后面再逐个模块替换真实逻辑整体风险非常可控。3.3 状态管理避免两端状态不一致的关键跨端共享最怕“界面一样、状态各玩各的”。我们的处理办法是在共享层做统一状态管理选的是轻量的 Zustand。运维小哥在 Android 上把一辆车标记为“维修中”这个状态必须立刻反映到工单列表和车辆地图标记上不能只改了本地页面没改全局状态。P 点围栏判定是个典型例子车停在围栏边缘GPS 漂移一点就可能误判。我们把“是否在围栏内”的判断逻辑放在共享层的业务层输入是经纬度输出是布尔值跟两端原生定位完全解耦。这样调围栏算法只动共享层测试也只测一份逻辑两端表现天然一致。3.4 远程更新运维 App 最值钱的能力共享层的另一个红利是远程更新。以前改一条“电池低电量阈值”Android 和 iOS 各自发版审核快慢不一经常出现“安卓已经按新规则跑了苹果用户还在用老规则”的情况。现在这种改动直接在后台发布一次 JSBundle 更新运维小哥打开 App 时静默拉取规则变更一夜之间全量生效。这个能力在运维场景里价值极大因为电单车项目经常有临时运营策略比如节假日重点区域车辆调度、恶劣天气下发“部分车辆暂停运营”的指令。这类临时逻辑如果都要发版等版本审核通过天气都过去三天了。4. 宿主层那 0.8 万行为什么不能硬塞进共享层4.1 宿主层不是“没用的壳”它是 App 的地基很多人听到“宿主 19%”会觉得宿主层就是个壳包一层 WebView 再挂个标题栏。这个印象是错的。宿主层承载的是系统级能力和 App 生命周期它的代码量不一定大但复杂度和权限敏感度是最高的。共享层跑得再欢也得靠宿主层把底层地基打好。以启动流程为例App 冷启动的一瞬间宿主层要完成 RN 引擎初始化、地图 SDK 注册、推送通道建立、获取本机设备信息这些动作发生的时间窗口很窄任何一步卡住都会导致白屏或首屏延迟。我们最初在 iOS 上遇到过启动时地图初始化阻塞 JS 加载的问题花了很大力气优化依赖调动顺序才解决这就是典型的宿主层问题。4.2 蓝牙 BLEAndroid 和 iOS 的差异到底有多大共享电单车的车辆端通讯模块用的是低功耗蓝牙开锁、读电量、读车辆故障码全靠它。蓝牙这块是我见过两端差异最大的系统能力远不止“权限名不同”这么简单差异维度AndroidiOS扫描方式支持主动扫描 广播回调可自定义扫描间隔依赖 CBCentralManager扫描回调粒度粗权限模型6.0 起需动态申请定位权限才能扫描 BLE需同时授权蓝牙权限和定位权限且分“使用期间”和“始终”连接机制可以多设备并发连接状态回调频繁单个中心设备同时连接数量受限连接状态机偏严格后台运行可通过前台 Service 保活保持连接进入后台后系统可能挂起 BLE 回调需要 Background Modes 配置厂商兼容国产 ROM 对蓝牙扫描有各自限制相对统一但对广播数据解析要求更高这些差异如果硬塞进共享层最终结果是共享层代码里全是平台判断根本没法维护。所以我们在宿主层做了两个薄薄的蓝牙模块Android 和 iOS 各自实现对外暴露一套统一接口共享层只需要调用“scan”“connect”“readBattery”“unlock”这几个动作就行。4.3 地图与定位运营区域的公共底座电单车地图页面看起来是“一张地图 一堆标记”实际底下的地图 SDK 初始化、相机视角控制、聚合标记、路线规划每项都依赖原生地图库。Android 端我们用的高德地图版本和 iOS 端不是同一套 API字段命名和回调时机都有区别。宿主层把地图组件包了一层统一组件共享层传入标记数据宿主层负责渲染和交互这样共享层不用关心地图是哪个厂家的。定位权限就更典型了。Android 12 以上区分“粗略定位”和“精确定位”用户如果只给了粗略权限扫描蓝牙会直接失败因为 BLE 扫描在 Android 上依赖精确定位权限。iOS 则区分“使用期间定位”和“始终定位”运维小哥把 App 切到后台去挪车时如果没给“始终”权限后台位置回调就会断。这些权限引导和降级策略只能在宿主层做共享层只能通过桥接拿到最终的定位结果。4.4 共享层和宿主层怎么通信桥接协议的设计为了让两段代码各司其职我们给共享层和宿主层之间的通信定了一套简单的协议。共享层通过统一的 callHost 方法调用宿主能力宿主层通过事件回调把结果送回共享层。接口规范长这样// 共享层 - 宿主层请求打开蓝牙并连接车辆 const result await callHost({ module: BLE, action: connectVehicle, params: { mac: AA:BB:CC:DD:EE:FF, timeout: 10000 } }) // 返回值统一为 { code: 0, data: {...} } 或 { code: -1, message: 连接超时 }同样的宿主层主动上报事件时也走统一的事件总线。比如车辆蓝牙断开宿主层检测到后广播onVehicleDisconnected共享层收到后更新页面状态并弹出提示。这套协议的好处是两端宿主实现的命名和参数保持一致共享层传参不会因为平台差异而走样。5. 从双端独立维护到单共享层的迁移路线5.1 第一步代码测绘给旧 App 建立“重复度地图”迁移不是“把 Android 代码复制进共享层”就完事而是先把两个端的历史代码盘一遍。我们当时做了一次代码测绘把 Android App 和 iOS App 的页面、组件、接口调用全部列出来按功能区排序标出哪些页面只有 Android 有、哪些页面两端都有、哪些逻辑其实已经过时可以直接删掉。这个步骤很容易被跳过但价值非常大。我们测完发现Android 端有一个“充值记录”页面iOS 端早就用“财务总览”替代了两端对“工单状态”的叫法还不一样Android 叫“已接单”iOS 叫“处理中”。如果直接照着某个端搬这些历史包袱就会被带进共享层得不偿失。5.2 第二步骨架期先搭好宿主壳和共享层初始化迁移第一阶段不搬业务页面先把工程骨架立起来。Android 端和 iOS 端各建一个新的宿主工程集成 RN 引擎保证“打开 App 能看到一个 Hello World 页面”。这一步看起来简单实际踩坑不少光是 iOS 的 Pod 依赖和 Android 的 Gradle 版本对齐就花了两三天还遇到过 RN 新架构和旧原生库冲突的问题。骨架期还有一个重要任务把统一桥接协议先跑通。我在宿主层写了几个 test 模块共享层能调通“获取设备型号”“读取电量”这类原生方法说明桥接链路是通的后面搬页面才有底气。5.3 第三步按价值密度排序逐个搬迁页面我们当时把页面分成三批搬迁。第一批是“扫码开锁 工单列表”因为这是运维最常用、最核心的路径也是两端差异最大的地方先啃硬骨头第二批是“故障上报 地图寻车”依赖原生能力多需要边搬边调第三批才是个人中心、消息通知等边缘页面。每一批的验收标准都是一样的共享层页面在老 App 同款功能上跑一遍交互一致、数据一致、埋点一致。这里有个经验搬页面时不要顺手改交互逻辑先把行为复制过来让用户感觉到“还是原来那个功能”等整体切换完成后再统一优化否则两边测试说不清问题出在迁移还是改版。5.4 第四步账号体系和消息推送合流账号体系如果不能统一共享层的价值就废了一半。我们花了比较大的精力把两端的登录态统一成一套 token 体系共享层所有的网络请求都自动带上 token宿主层的推送注册也把 token 和用户 ID 绑定。这样运维小哥在 Android 上登录后再到 iOS 上打开 App不会要求重新登录推送也不会“安卓有、苹果没有”。这一步其实很容易被低估但它直接影响用户感知。如果账号数据出现“两端不同步”的情况用户会直接认为 App 是坏的什么架构优势都体现不出来。5.5 第五步老 App 灰度下线所有页面搬迁完成后我们没有直接下架老 App而是做了两轮灰度第一轮内部测试团队用新 App 跑一周重点对比扫码成功率、定位准确率、蓝牙连接时长这几个核心指标第二轮随机选 20% 的运维账号切到新 App观察工单处理量和异常上报率连续三天没有明显波动后才逐步把流量切完。灰度期间有一个指标让我印象很深新 App 在 iOS 上的蓝牙连接时长比老 App 平均快了 0.8 秒。原因是迁移的时候我们顺手把蓝牙连接的 retry 策略优化了以前是连接失败后等待 5 秒重试现在改成 2 秒后重试且最多重试 3 次。这个优化属于“搬家顺便装修”用户未必能说出差异但体感上会觉得新 App 更跟手。6. 4 万行共享改造中踩过的坑与排查实录6.1 坑一iOS 退后台后 BLE 连接被系统挂起做完第一轮灰度有 iOS 用户反馈开着的锁锁还没落手机锁屏了一下App 再打开就显示“连接已断开”工单也断了。我们一开始以为是蓝牙模块的问题排查半天发现根源在 iOS 系统策略App 进入后台后系统会挂起 BLE 连接回调除非明确开启了 Background Modes 里的“Uses Bluetooth LE accessories”。这个问题在开发机上不出现因为测试时手机一般不会锁屏一旦上真实场景运维小哥经常一边推车一边锁屏问题就暴露了。解决方法是 iOS 宿主工程里配置好后台运行权限同时在桥接层增加“App 从后台回前台时主动检测蓝牙连接状态如果断开就自动重连”的逻辑。6.2 坑二Android 精细定位权限被用户降级P 点围栏直接失效第二次灰度中我们发现一小部分 Android 用户扫码开锁后P 点围栏判断不准明明车停在 P 点内却提示“不在停车区域”。查了一圈问题出在 Android 12 的权限系统用户安装 App 时如果选择了“大致定位”系统给的是粗略定位精度BLE 扫描和 P 点判定用的都是精细定位两者互相干扰。我们后来在宿主层加了一个前置检查启动时判断定位权限是“精确”还是“粗略”如果发现是粗略权限就弹出一个引导对话框用运维业务的语言解释“需要精确到 10 米内的定位权限才能找到车辆”点击按钮跳转系统设置。这个引导上线后权限问题导致的围栏误判减少了 90% 以上。6.3 坑三iOS 扫码偶发无响应的完整排查链路灰度期还有一个特别诡异的问题iOS 端扫码偶尔会“没反应”不是每次都复现但一周能碰到三四回。这类问题最让人头疼因为你不确定是共享层的问题还是宿主层的问题。我们的排查链路可以完整梳理一遍对大家以后排查跨端问题有参考价值。第一步先看共享层日志。扫码页每次点击都会记录 scan_start无响应的时候 scan_start 有没有触发结果发现触发了说明页面层没问题。第二步看桥接调用记录。扫码之后共享层会调用宿主层的 camera 模块启动取景。日志显示 callHost 已经发出但宿主层没有返回任何回调。这说明问题出在宿主层相机模块。第三步看宿主层相机模块的日志。发现 iOS 的 UIImagePickerController 在部分机型上启动时如果上一次相机实例没有正确释放新的实例会被系统拒绝。原因是一次扫码成功后页面快速退出相机的 dealloc 被延迟当下一次扫码刚好撞上这个窗口期新的相机实例就起不来。修复方式很直接共享层调用宿主层相机前先发一个 releaseCamera 动作确保上一个实例释放干净再启动新的取景会话。这个 bug 让我意识到跨端问题排查时一定要先把“共享层已经做了什么”和“宿主层是否真的执行了”这两件事查清楚否则很容易陷入“推到共享层改一版再推到宿主层改一版”的死循环。6.4 工程化保障用脚本卡住职责边界拆分的最大风险是边界不断漂移哪天某个开发图省事把一段业务逻辑直接写进了宿主层下一次迭代另一个开发看到也照猫画虎半年后边界就糊了。我们应对这件事靠的是两条工程手段。第一CI 检查。我们写了一个简单的脚本扫描宿主层代码目录如果发现包含“状态机”“业务阈值”“工单”等关键词的代码就自动在 PR 上打一个警告标签提醒开发者“这段逻辑可能应该放到共享层”。第二桥接接口文档和示例代码放在仓库的 docs 目录宿主层新增能力必须同步更新接口文档否则 CI 不让合并。这两条看着简单但真的能挡住大部分边界漂移。写在最后共享不是目的省下来的时间才是这次拆分最大的收获不是代码行数从潜在 8 万行变成了 4 万行而是我们终于不用在业务迭代时同时维护两套几乎一样的代码。现在改一个电池阈值、调一次 P 点围栏、加一个工单状态只动共享层测试只跑一份逻辑两端上线时间也完全一致。对于运维 App 这种业务规则迭代快、又依赖大量系统底层能力的场景“共享 81% 宿主 19%”是非常合理的结构。如果你也要做类似的跨端改造我的建议是先花一两天把现有代码的重复度摸清楚按“跟业务走还是跟系统走”这条标准把模块分成两堆不要一上来就定技术框架。架构永远是跟着业务形态走的业务不变架构就没有价值。
返回列表