移动应用跨 Android 和 iOS 的共同决策:原生、跨平台框架和 Web/PWA 怎么选,跨平台工程规范,离线与同步,推送,崩溃与性能监控,发布与热更新的边界,权限与隐私的工程做法,以及移动端 E2E 工具。
选定移动端技术栈前写决策记录,写明候选方案、取舍理由和推翻条件
怎么检查:CI 检查决策记录文件存在;评审时看是否比较了原生、跨平台和 Web/PWA
为什么、来源
为什么:移动端技术栈换起来代价很大,没有记录的决定事后说不清为什么,也不知道什么时候该重新评估。
后台定位、蓝牙、相机实时处理、系统扩展是核心功能时,先在目标机型上验证原生能力,验证通过前不承诺跨端实现
怎么检查:评审时检查有没有能力清单和真机验证记录(机型、系统版本、权限、前后台切换)
为什么、来源
为什么:这些能力受系统权限和生命周期限制最严,跨端框架的插件常常只覆盖一部分,项目后期才发现做不到最难补救。
PWA 和小程序只用于轻量入口;需要可靠推送、后台运行或深度硬件访问时,不用它们替代原生 App
怎么检查:选型评审时对照需求里的后台、推送、硬件、存储容量要求
为什么、来源
为什么:浏览器和小程序宿主对后台执行、推送、存储回收的限制比原生更严,iOS 上差异尤其大,承诺能力对等会落空。
跨端共享接口契约、设计令牌和纯领域逻辑;不把代码共享率当指标,不强求各平台界面像素一致
怎么检查:评审共享模块:不依赖 DOM、Node、Android Context 等平台对象;交互遵循各平台的设计规范
为什么、来源
为什么:跨语言时共享接口定义比共享源码划算;强推共享率会让代码迁就框架,用户体验变差。
- 接口类型:从同一份 OpenAPI 或 JSON Schema 生成各端代码,不手写各端的数据结构。
- 设计:共享颜色、字体、间距等语义令牌(见 08),不复制 Web 的 CSS 组件。
- 交互:共享产品意图和验收标准,导航、返回手势、系统弹窗遵循 Material Design 和 Apple HIG。
要做鸿蒙原生应用时,单独建立 ArkTS 工程规范和构建流程,只共享接口契约、资源和设计令牌
怎么检查:评审时检查鸿蒙端是否有独立的依赖、签名、权限和推送方案,以及 Android 依赖的逐项替换清单
为什么、来源
为什么:鸿蒙原生应用不运行 Android 安装包,Google 服务、推送、地图、支付等依赖都要逐项替换,不能假定跨端框架能直接覆盖。
React Native 项目使用新架构;引入原生依赖前确认它支持新架构
怎么检查:Expo 项目在 CI 运行 npx expo-doctor;裸 React Native 项目对照 React Native Directory 的新架构标记
为什么、来源
为什么:React Native 0.82 起旧架构已移除,Expo SDK 55 起新架构始终开启且不能关闭;不支持新架构的库无法使用。
依赖用锁文件固定,CI 用冻结安装;Expo 项目用 npx expo install 添加依赖,并在 CI 运行 npx expo install --check
怎么检查:CI 用 npm ci / pnpm install --frozen-lockfile / flutter pub get --enforce-lockfile;Expo 项目加跑 npx expo install --check
为什么、来源
为什么:移动端构建链长、原生依赖多,版本漂移会让同一份代码在不同时间打出不同的安装包;Expo 的依赖版本要与 SDK 配套。
创建项目时也不用 @latest 模板,固定到团队审过的 SDK 版本。通用依赖规则见 10。
引入跨平台插件或原生模块前,检查维护状态、Android 和 iOS 两端的支持情况和许可证
怎么检查:依赖评审清单:最近发布时间、未解决问题、两端是否都有实现、许可证;工具辅助可用 React Native Directory、pub.dev 评分
为什么、来源
为什么:插件停更或只实现一端,问题往往到发布前真机测试才暴露;插件里的原生代码同样受许可证约束。
导航、状态管理、本地数据库各只用一套;引入第二套要写理由
怎么检查:依赖审查脚本列出同类库,命中多套时要求决策记录
为什么、来源
为什么:两套导航或两套数据库会让状态不一致、包变大,迁移也只做一半。
业务数据存在 SQLite 等结构化数据库里,不用 AsyncStorage 这类键值存储存业务主数据
怎么检查:代码扫描键值存储的写入,评审是否存了列表或实体数据
为什么、来源
为什么:键值存储没有事务、索引和迁移,数据一多就慢,出错时也无法局部恢复。
登录用系统浏览器 + 授权码 + PKCE;不在 WebView 里收集用户密码
怎么检查:代码扫描登录流程是否用系统认证会话(AppAuth、expo-auth-session、ASWebAuthenticationSession、Custom Tabs);E2E 覆盖登录
为什么、来源
为什么:内嵌 WebView 让 App 能读到用户密码,也绕过了浏览器已有的登录状态;RFC 8252 要求原生应用用外部用户代理。
访问令牌、刷新令牌等凭据只存系统安全存储(Keychain、Android Keystore 的封装),不明文落盘、不写日志
怎么检查:代码扫描令牌写入普通存储和日志的调用;日志断言测试里查找令牌格式
为什么、来源
为什么:普通文件和键值存储在备份、越狱或 root 设备上可被读取,日志会被收集和转发。
客户端不内置永久 API 密钥或服务端密钥;需要调用第三方服务时经自己的服务端转发或换取短期凭据
怎么检查:对仓库和构建产物跑密钥扫描
为什么、来源
为什么:安装包可以被任何人下载和反编译,打进去的密钥等于公开。
生物识别只用于解锁本机保存的凭据,不作为服务端的身份验证依据
怎么检查:评审认证设计:服务端是否只认令牌或签名,而不是客户端上报的「已通过生物识别」
为什么、来源
为什么:客户端上报的布尔值可以被篡改;生物识别只能证明「这台设备上有人通过了验证」。
退出登录、切换账户、切换租户时,清除凭据、本地敏感缓存和推送令牌绑定
怎么检查:E2E 用例:退出后检查安全存储、本地库、图片缓存为空,服务端已解除推送令牌
为什么、来源
为什么:共用设备或换号后,残留数据会让下一个用户看到上一个人的内容,残留绑定会把通知推到旧设备。
推送令牌被系统轮换、用户关闭通知时,也要及时更新服务端的绑定(服务端处理见 21)。
启用证书固定时,必须固定多个公钥(含备用)并有轮换和应急恢复方案;没有这些就不启用
怎么检查:安全评审检查固定的公钥数量、轮换流程和固定失败时的恢复手段(远程配置关闭或强制升级)
为什么、来源
为什么:证书一换,所有固定了旧证书的客户端同时连不上,只能靠发新版补救;私有化客户自己的 CA 轮换更难预料。
需要离线写入时,先把本地实体和待发操作一起写入持久化队列(outbox),界面标出「待同步」,不显示为已提交
怎么检查:集成测试:断网写入后杀进程重启,检查数据和待发操作都还在,联网后被提交
为什么、来源
为什么:只放内存或只靠请求缓存的写入,进程一被系统杀掉就丢;把待同步显示成已提交会误导用户。
最小数据结构(示意):
- 实体:id、服务端版本、更新时间、删除时间、内容
- 待发操作:操作 id、实体 id、操作类型、基于的版本、内容、创建时间、重试次数、状态
- 同步游标:范围、游标、最近成功时间
每个离线操作带唯一的幂等键,服务端据此去重;重试用指数退避加随机抖动
怎么检查:契约测试:同一操作重复提交、乱序提交,服务端结果只生效一次
为什么、来源
为什么:移动网络经常出现「请求到了、响应丢了」,没有幂等键的重试会重复下单或重复审批。
按数据类型写明冲突策略;金额、审批、不可逆操作不得用「最后写入者胜」
怎么检查:评审同步设计文档:每类实体的冲突策略(拒绝覆盖、字段级合并、转人工)和对应测试
为什么、来源
为什么:「最后写入者胜」会静默覆盖别人的修改,对金额和审批来说就是数据错误。
删除用墓碑记录同步;增量同步的游标失效时能回退到全量
怎么检查:同步测试覆盖删除、游标过期、时钟偏差、重复和乱序事件
为什么、来源
为什么:直接物理删除时离线设备不知道该删什么,会把旧数据再同步回来。
不在需求或验收中承诺固定间隔的后台同步;以进入前台、网络恢复、用户操作作为同步时机
怎么检查:评审需求和验收标准里有没有「每 N 分钟后台同步」之类的表述
为什么、来源
为什么:Android 和 iOS 都会按电量、使用频率推迟或跳过后台任务,跨平台框架的定时器在后台不会准点运行。
后台任务接口(WorkManager、BGTask)的用法见 29、30。
有本地数据库的应用,每次升级用旧版本的数据样本测试迁移
怎么检查:CI 里保留各历史 schema 版本的数据样本,逐个跑迁移到当前版本并校验
为什么、来源
为什么:服务端迁移出错可以修库,用户手机上的数据迁移失败往往只能清空重来。
不承诺推送必达;重要业务提供应用内待办或消息列表,打开应用时主动拉取
怎么检查:验收文档检查:关闭通知权限、断网、应用被系统终止时,用户打开应用仍能看到待处理事项
为什么、来源
为什么:用户可能关掉通知,系统可能限流,内网环境可能连不到 APNs 和 FCM;推送只能作为提醒。
面向中国大陆 Android 用户时,接入主要厂商推送通道(华为、荣耀、小米、OPPO、vivo 等)或经评估的聚合推送 SDK;鸿蒙原生用 Push Kit
怎么检查:发布前在各厂商真机上验证通知送达和点击跳转
为什么、来源
为什么:中国大陆的 Android 设备多数没有 Google 服务,收不到 FCM;各厂商通道要单独申请、单独适配。
聚合推送 SDK 属于第三方 SDK,要进隐私政策和 SDK 清单(见 27)。只用 Expo 的推送服务无法覆盖国内厂商通道。
推送标题和正文不含敏感业务内容,只带标识和中性提示,打开应用后再拉取详情
怎么检查:推送载荷的 schema 校验禁止正文字段;安全测试检查锁屏显示
为什么、来源
为什么:锁屏通知谁都能看到,推送内容还要经过第三方推送服务。
接入崩溃上报,覆盖原生崩溃和 JS/Dart 未捕获异常;发布时上传符号表和 source map
怎么检查:发布流水线检查符号表和 source map 已上传,并用测试崩溃验证能还原堆栈
为什么、来源
为什么:用户手机上的崩溃无法复现,没有符号化的堆栈基本看不懂。
崩溃和性能数据上传前脱敏,不含令牌、用户正文、定位原始值;私有化部署时能关闭第三方上报或改指向客户自建的服务
怎么检查:出站流量测试或抓包检查上报内容;配置检查上报地址可配置
为什么、来源
为什么:崩溃日志常常带上请求内容和用户输入;私有化客户通常不允许数据发到公网 SaaS。
在 Release 构建上监控启动耗时、卡顿和应用无响应(ANR)、同步积压和待发队列失败率
怎么检查:监控看板有这些指标;性能测试用 Release 包而不是 Debug 包
为什么、来源
为什么:Debug 包的性能和 Release 差别很大;同步积压是离线应用特有的故障信号。
热更新(OTA)只更新脚本和资源;改原生代码、新增权限或原生依赖时必须发新安装包并过商店审核
怎么检查:发布流水线比对原生部分的指纹,有变化时阻止走 OTA
为什么、来源
为什么:Apple 审核指南 2.5.2 禁止下载会引入或改变功能的代码;Google Play 禁止从 Play 以外下载 dex、so 等可执行代码,只放行在虚拟机或解释器里运行的代码;把原生变更推给旧安装包还会直接崩溃。
热更新不得改变应用的主要用途,不得绕过审核上线新的主要功能
怎么检查:发布评审检查每次 OTA 的改动说明,涉及新功能的走安装包发版
为什么、来源
为什么:应用商店允许用解释执行的脚本修 bug 和调整内容,但用 OTA 偷换功能会导致应用下架。
使用 EAS Update 时用 runtimeVersion 约束兼容性,原生部分变化必须改 runtimeVersion
怎么检查:用 runtimeVersion.policy 设为 fingerprint,由工具按原生部分自动计算
为什么、来源
为什么:runtimeVersion 不变而原生部分变了,更新包会下发到不兼容的安装包上导致启动崩溃。
OTA 更新分批发布、可回滚,每次记录对应的安装包版本、runtimeVersion 和提交编号
怎么检查:发布记录检查这三项;演练过一次回滚
为什么、来源
为什么:OTA 一推就是全部在线用户,出问题时要能快速定位和撤回。
不使用 App Center CodePush 托管服务
怎么检查:依赖扫描 react-native-code-push 及指向 App Center 的配置
为什么、来源
为什么:Microsoft App Center 已于 2025-03-31 停服,托管的 CodePush 随之关闭,客户端收不到更新。
服务端删除接口或改变语义前,按仍在使用的客户端版本做兼容计划,并预留强制升级通道
怎么检查:CI 用 OpenAPI 比对工具发现破坏性变化,要求 PR 附兼容计划;客户端有最低版本检查
为什么、来源
为什么:已安装的客户端不会同时升级,服务端一改旧版本就坏;没有强制升级通道就只能等用户自己更新。
支持多少个旧版本由产品和客户约定;能力按接口协商,不按 User-Agent 猜测。接口版本策略见 16。
要求完全断网构建时,在隔离环境里从干净状态完成依赖恢复、编译、签名和安装验证
怎么检查:在无公网的 CI runner 上执行完整构建并安装到测试设备
为什么、来源
为什么:「业务接口走内网」不等于构建能离线;eas build --local 仍要校验 Expo 账户,Gradle、CocoaPods、npm 都会联网下载。
发布前在真机上用 Release 构建跑关键流程 E2E,包括断网、弱网、应用被杀后恢复和旧数据升级
怎么检查:发布检查表要求附真机测试报告(机型、系统版本、构建号)
为什么、来源
为什么:Debug 包和模拟器不代表正式包:代码压缩、签名、厂商系统的权限和后台管理只在 Release 真机上出现。
E2E 用可访问性标识或测试 ID 定位元素,不靠屏幕坐标和截图比对代替断言
怎么检查:评审 E2E 脚本:查找坐标点击和只截图不断言的步骤
为什么、来源
为什么:坐标随机型和字体大小变化,截图对比不能说明功能对不对;可访问性标识还能同时服务读屏。
核心流程在系统最大字体和读屏(TalkBack、VoiceOver)下真机验证
怎么检查:发布前的无障碍检查记录
为什么、来源
为什么:跨平台框架的控件不一定正确映射到系统无障碍接口,只有真机读屏能发现。
系统权限在用到对应功能时再请求,请求前说明用途;被拒绝后功能降级,不退出、不反复弹窗
怎么检查:评审首次启动流程和每个权限的请求时机;E2E 覆盖拒绝权限后的路径
为什么、来源
为什么:启动时一次要全部权限,用户多半全部拒绝;Apple 审核指南要求尊重用户的权限设置,不得诱导或强迫授权。
只申请功能必需的权限;用户同意隐私政策前,不初始化会采集数据的第三方 SDK
怎么检查:构建产物的权限清单与需求比对;启动阶段抓包,检查同意前没有第三方上报
为什么、来源
为什么:多余权限和同意前的采集是应用商店和监管检查的常见问题,具体法规要求见 27。
设备标识、剪贴板、定位、通讯录等敏感接口,使用前核对必要性;日志不记录定位原始值。
没有符合条件的规则。
双端业务 App 框架
React Native + Expo · React Native、Expo、Flutter 的框架主体为宽松许可(MIT / BSD-3-Clause);插件逐个核查
备选:Flutter(重视觉、要各端渲染一致,或团队熟悉 Dart);Kotlin Multiplatform + 原生 UI(只想共享业务逻辑,界面保持各平台原生);双原生(Kotlin + Swift)(系统集成很深,或后台、硬件能力是核心);React Native CLI(不用 Expo)(有大量自有原生工程,且不需要 Expo 的工具链)
不推荐:纯 WebView 包壳
共享业务逻辑
Kotlin Multiplatform · Apache-2.0
备选:Compose Multiplatform(共享 UI)(能接受 iOS 上非原生控件观感,想同时共享界面;iOS 支持已稳定);TypeScript 工作区包(Web 和 React Native 共享纯逻辑)
不推荐:在共享模块里直接调用平台对象或厂商 SDK
轻量入口
PWA
备选:Capacitor(已有 Web 代码,但需要上架或调用少量原生能力);微信小程序官方框架(只做微信渠道);Taro / uni-app(多个小程序渠道且要复用 TypeScript 代码,先做验证)
不推荐:用 PWA 承担强后台、可靠推送或深度硬件功能
React Native 导航与状态
Expo Router;服务端数据用 TanStack Query;界面局部状态用 Zustand
备选:React Navigation(不用 Expo 或需要它特有的配置方式)
不推荐:自制路由;把请求缓存当主数据源
Flutter 导航与状态
go_router + Riverpod
本地数据库
SQLite(React Native 用 expo-sqlite;Flutter 用 drift;KMP 用 SQLDelight 或 Room) · SQLite 为公有领域;外围库另核
备选:MMKV 等键值库(只存少量配置)
不推荐:AsyncStorage 存业务数据
凭据存储
系统安全存储的封装(expo-secure-store、flutter_secure_storage)
不推荐:明文文件、键值存储、代码常量
移动端 E2E
Maestro · Maestro 为 Apache-2.0(Maestro Cloud 是另行收费的服务);Detox、Patrol、Appium 的许可证待核
备选:Detox(React Native 项目要灰盒同步(自动等待应用空闲));Patrol(Flutter 项目要在测试里操作系统弹窗和权限);Appium(要用多种编程语言写测试、已有 WebDriver 基建或接真机云)
不推荐:只截图不断言;把含客户数据的包传到公有真机云
推送客户端
iOS 用 APNs;海外 Android 用 FCM;中国大陆 Android 接各厂商通道;鸿蒙用 Push Kit
备选:聚合推送 SDK(如极光、个推)(厂商通道多、团队不想逐个适配;要做 SDK 合规评估);前台长连接 + 增量拉取 + 应用内待办(内网私有化、连不到公共推送网络)
不推荐:只靠 Expo 推送服务覆盖国内设备
热更新(React Native)
EAS Update(runtimeVersion 用 fingerprint 策略)
备选:自托管兼容 expo-updates 协议的更新服务(私有化或不能用 Expo 云服务)
不推荐:App Center CodePush;用 OTA 改原生能力
崩溃与性能监控
Sentry SDK · Sentry 客户端 SDK 为 MIT;自托管服务端为 FSL,不是 OSI 认可的开源许可
备选:Sentry 自托管(数据不能出客户网络;注意服务端许可证);Firebase Crashlytics(只面向有 Google 服务的海外用户);本地崩溃转储 + 企业自有收集器(完全断网或规模很小)
不推荐:私有化项目默认上传到公有 SaaS
✕ 错误做法:只加一个请求缓存插件就叫离线优先
✓ 应该:本地库 + 持久化待发队列 + 幂等键 + 冲突策略 + 删除同步
✕ 错误做法:用定时器保证每 5 分钟后台同步
✓ 应该:以进入前台、网络恢复、用户操作为同步时机
✕ 错误做法:一个公共推送 SDK 解决所有中国 Android 设备
✓ 应该:按厂商通道在真机上逐个验证
✕ 错误做法:跨平台插件跑不通就用 WebView 绕过
✓ 应该:先验证原生能力和权限边界,必要时写原生模块
✕ 错误做法:把 eas build --local 当成完全离线构建
✓ 应该:在无公网环境从干净状态实测完整构建
✕ 错误做法:强制 Web 和移动端共用 UI 组件
✓ 应该:共享语义和设计令牌,交互遵循各平台习惯
✕ 错误做法:默认开启证书固定,只固定一张证书
✓ 应该:默认不开;要开就固定多个公钥并准备轮换方案
✕ 错误做法:用 OTA 上线新功能或改原生能力
✓ 应该:原生变更发新安装包,OTA 只修脚本和资源
✕ 错误做法:应用启动时一次请求所有权限
✓ 应该:用到时再请求,先说明用途
✕ 错误做法:只用模拟器验证兼容性
✓ 应该:关键流程在真机 Release 包上测,覆盖主要厂商机型
✕ 错误做法:自己实现 OAuth 登录、令牌存储、扫码
✓ 应该:用 AppAuth、系统安全存储和维护良好的框架模块