跳至正文
Starone

能力

为真实设备、真实网络与发布约束而构建的移动应用

移动应用是一个长期存在的客户端,团队无法随时重新部署它。它运行在时断时续的网络上,会被操作系统不加通知地终止,旧版本还会在用户设备上停留数月。我们围绕这些事实来设计应用本身、它的 API 以及发布流水线。

概览

移动端工程与 Web 工程有三点结构性差异。网络时断时续,因此应用在没有网络时也必须可用,并在网络恢复后与服务端重新协调一致。进程生命周期由操作系统掌控,系统会为了保护内存和电量而挂起或终止应用。发布要经过应用商店审核,之后还取决于用户是否更新,因此总有多个版本同时在用,缺陷也无法靠一次重新部署撤回。

这些约束塑造的是整个系统,而不只是客户端。API 必须与数月前发布的版本保持兼容,响应数据必须适合按流量计费的连接,认证机制必须能应对应用在后台期间令牌过期的情况。在设备上,凭证应当放在硬件支持的存储中,本地数据和任何数据库一样需要 schema 与迁移,启动时间、帧率、内存和能耗都是需要度量的属性。

我们在 iOS 上使用 Swift 与 SwiftUI,在 Android 上使用 Kotlin 与 Jetpack Compose;当共享代码库是更划算的取舍时,使用 Flutter 或 React Native。具体如何选择,依据的是产品需求和团队结构;后端契约、离线模型和发布流程则与最初的几个页面同步设计。

我们构建什么

技术领域

01

原生与跨平台开发

原生开发可以第一时间用上所有平台 API,拥有最好的启动和渲染表现以及符合平台规范的行为,代价是维护两套代码库。Flutter 通过自带的引擎渲染自己的 widget,跨平台视觉一致性很高;React Native 由 JavaScript 或 TypeScript 驱动原生视图,可与 Web 团队共享技能。两者在某些集成上仍然需要编写原生代码。

  • Swift、SwiftUI 与 UIKit
  • Kotlin、Jetpack Compose 与协程
  • Flutter 与 Dart
  • 采用 New Architecture 与 Hermes 的 React Native
  • 以 Kotlin Multiplatform 共享业务逻辑
  • 原生模块与 platform channel
02

离线优先的数据与同步

在离线优先的设计中,界面读写的是本地数据库,由同步引擎在后台与服务端协调一致。本地写入以带幂等键的变更操作形式排队,在网络恢复后按顺序重放。冲突处理规则按实体分别定义,因为适合一条笔记的规则并不适合一笔支付。

  • SQLite 搭配 Room、GRDB、Drift 或 WatermelonDB
  • 带幂等键的变更发件箱
  • 基于游标或变更令牌的增量同步
  • 最后写入胜出、字段级合并与 CRDT
  • 金融类状态以服务端为准的规则
  • 基于 WorkManager 与 BGTaskScheduler 的后台同步
03

安全、存储与认证

机密数据存放在 iOS Keychain 和 Android Keystore 中,在设备支持的情况下,密钥生成于 Secure Enclave 或由 StrongBox 支持的安全元件内。生物识别认证把守的是硬件所持密钥的使用权:匹配成功后放行的是一次密码学操作,而不是返回一个被篡改的应用可以绕过的布尔值。登录通过系统浏览器,采用带 PKCE 的 OAuth 2.0 授权码流程。

  • Keychain 与 Android Keystore
  • Face ID、Touch ID 以及配合 CryptoObject 的 BiometricPrompt
  • 带 PKCE 与刷新令牌轮换的 OAuth 2.0
  • 带轮换方案的证书锁定(certificate pinning)
  • App Attest 与 Play Integrity
  • 越狱、root 与篡改信号
  • 敏感数据的截屏与备份排除
04

移动端 API 架构、推送与深度链接

BFF 按页面来组织响应,减少高延迟网络上的往返次数。经由 APNs 和 FCM 的推送通知只是尽力而为的投递,因此只把它当作拉取状态的信号,绝不让它承载状态本身。Universal Links 和 Android App Links 通过托管在已验证域名上的文件,把 URL 与应用绑定,从而防止其他应用冒领这些链接。

  • 按客户端类型划分的 BFF
  • 游标分页与稀疏字段选择
  • 压缩、ETag 与条件请求
  • APNs 与 Firebase Cloud Messaging
  • Universal Links 与 Android App Links
  • 最低支持版本与强制升级
  • 远程配置与功能开关
05

性能、分析与崩溃上报

性能工作针对冷启动、帧节奏、内存和能耗。在 60 Hz 下一帧必须在约 16.7 毫秒内生成,在 120 Hz 下则是 8.3 毫秒,因此耗时工作不放在主线程上,列表采用虚拟化。崩溃和卡顿报告借助上传的 dSYM 与 R8 mapping 文件完成符号化,并按发布版本以无崩溃会话率进行跟踪。

  • 冷启动与首帧耗时剖析
  • Xcode Instruments 与 Android Studio Profiler
  • Baseline Profiles 与 R8 优化
  • 图片缓存与降采样
  • 合并请求以减少射频唤醒
  • Firebase Crashlytics 与 Sentry
  • 注重隐私的产品分析
06

CI/CD 与应用商店交付

移动端流水线在干净的机器上以可复现的方式完成构建、签名、测试和分发,签名身份保存在密钥存储中,绝不放在某台笔记本上。构建产物依次经过内部测试、TestFlight 和 Play 测试轨道,之后才在 App Store 上分阶段发布,或在 Google Play 上分阶段放量。当崩溃率或 ANR 率出现回归时,放量会被自动或手动叫停。

  • Fastlane、Xcode Cloud、GitHub Actions 与 Bitrise
  • 代码签名、provisioning profile 与 Play App Signing
  • 单元测试、快照测试与设备农场 UI 测试
  • TestFlight 与 Play 的内部、封闭和开放测试轨道
  • 分阶段发布与分阶段放量
  • 商店元数据、隐私声明与审核备注

移动应用如何连接云原生后端

移动客户端经由边缘层到达后端:先是 CDN 和 Web 应用防火墙,然后是 API 网关,由它终结 TLS、校验令牌、按设备和用户限流并路由请求。在网关之后,BFF 聚合对运行在 Kubernetes 或 serverless 平台上的各领域服务的调用,返回一个为页面量身定制的响应。这样,零碎的编排调用留在数据中心内部,那里一次调用只花一毫秒;而不是发生在移动网络上,那里一次调用要花上几百毫秒。

身份通过 OpenID Connect 流转。应用打开系统浏览器,完成带 PKCE 的授权码流程,拿到一个短有效期的访问令牌和一个会轮换的刷新令牌,并保存在 Keychain 或 Keystore 中。网关验证令牌签名,把经过验证的声明传给下游;各服务依据这些声明对每个请求进行授权。还可以加入来自 App Attest 或 Play Integrity 的证明,作为调用方是未经修改的正版构建的一个信号;它对高风险操作很有用,但永远不能替代服务端授权。

源自服务端的状态变化通过三条途径到达设备。经由 APNs 和 FCM 的推送通知唤醒应用或提示用户,并触发一次拉取。应用在前台时,由 WebSocket 或 Server-Sent Events 连接承载实时更新,进入后台后则关闭连接,以遵守操作系统的限制。启动和恢复时的增量同步补上所有缺口,因此正确性从不依赖某条推送是否送达。在服务端,这些途径由驱动其他消费者的同一条事件流供给,再由通知服务把事件扇出到各个设备令牌。

  • 版本兼容:应用在每个请求中携带自身版本和平台信息;API 只做增量式变更,后端可以要求低于最低支持版本的客户端升级
  • 韧性:客户端超时、带退避的重试和幂等键,使变更操作在网络中断后可以安全重放
  • 效率:经过压缩和分页的响应、条件请求以及按设备尺寸提供的图片变体,可以减少流量和电量消耗
  • 媒体:上传通过预签名、可断点续传的 URL 直达对象存储,而不经过应用服务器
  • 可观测性:在设备上生成的 trace 标识符经由网关和各服务一路传播,一个加载缓慢的页面可以被端到端追踪

架构

移动应用后端
  1. 移动应用

    • iOS (Swift)
    • Android (Kotlin)
    • Flutter
    • 离线存储
  2. 边缘

    • CDN
    • API 网关
    • 身份认证
    • 限流
  3. BFF / 移动端 API

    尽量做到每个界面一次往返

    • 按界面裁剪的响应
    • API 版本管理
    • 数据同步接口
    • 功能开关
  4. 服务

    • 账户
    • 内容
    • 支付
    • 媒体处理
  5. 数据与通知

    • PostgreSQL
    • Redis
    • 对象存储
    • APNs 与 FCM 推送
  6. 可观测性

    • 崩溃上报
    • API 链路追踪
    • 发布健康度
    • 客户端分析
移动客户端与 BFF(backend-for-frontend)通信,由它按每个界面裁剪响应,并让应用不受后端服务变更的影响。旧版本应用会长期留在用户手中,因此 API 版本管理与发布健康度也是架构的一部分。

离线优先架构与同步冲突

离线支持是一个渐进的谱系。读缓存在没有连接时展示最后一次已知的数据,成本很低。写入排队让用户可以离线操作,需要有序的发件箱、幂等的服务端端点和可见的待处理状态。完整的离线优先,即以本地数据库作为界面的事实来源并进行双向同步,能力最强也最复杂,应当在用户会长时间处于无信号环境下工作时才选择,例如现场服务、物流和巡检。

当同一条记录在两台设备上,或者在设备与服务端上,于两次同步之间各自发生变化时,冲突就会出现。按服务端时间戳的最后写入胜出很简单,但会悄无声息地丢弃其中一次编辑,这对偏好设置可以接受,对共享文档则不行。字段级合并可以保留互不重叠的编辑。CRDT 能让协作文本和列表自动收敛,代价是元数据膨胀和更难处理的数据模型。对于余额、库存和预订,服务端会依据当前状态校验每一个排队的操作,并可能拒绝它,应用必须把这次拒绝清楚地呈现给用户。

同步还需要处理几类情况:通过墓碑(tombstone)处理删除,对尚未上传的数据做 schema 迁移,以及一批操作进行到一半时的部分失败。设备时钟不可靠,因此排序使用服务端分配的版本号或逻辑时钟,绝不使用本地的墙上时钟时间。

原生还是跨平台:如何选择

这个决策取决于产品中有多大比例是平台特有的。以表单、列表和 API 驱动内容为主的应用很适合共享代码,一套 Flutter 或 React Native 代码库可以消除大部分重复的功能开发,并让两个平台保持同步。而围绕相机管线、蓝牙外设、后台定位、端侧机器学习、小组件、手表配套应用或最新操作系统特性构建的应用,大量精力都花在原生边界上,跨平台层在那里只增加了一层间接性,并没有省下工作量。

团队结构与技术同样重要。熟悉 TypeScript 和 React 的 Web 团队可以很快在 React Native 上产出,而 Flutter 需要改用 Dart,换来的是对渲染更精细的控制。原生团队掌握完整的工具链,新的操作系统版本发布时,不必受制于某个框架的发布节奏。Kotlin Multiplatform 是一条中间路线:共享网络、持久化和领域逻辑,界面则完全原生。

无论怎么选,都需要一定的原生开发能力。签名、推送配置、商店提交、深度链接关联、性能剖析以及许多第三方 SDK 都是平台特有的,缺乏这些知识的跨平台团队恰恰会卡在这些环节上。

我们如何构建

我们的方法

我们把客户端、API 和发布流程放在一起设计,因为只要旧版本还装在用户设备上,其中任何一处的决策都会一直制约另外两处。

  1. 01

    界定平台范围与约束

    我们确定目标操作系统版本、设备等级、离线需求、监管与应用商店政策方面的约束,以及产品所依赖的设备能力。原生还是跨平台,由这些因素决定。

  2. 02

    设计 API 契约与同步模型

    与后端团队共同明确端点、分页、错误语义、幂等以及对旧版本的兼容策略。如果需要离线写入,则在编写任何存储代码之前,先定下每个实体的冲突规则。

  3. 03

    尽早确立架构与流水线

    搭建 MVVM 或 MVI 这类单向数据流、依赖注入、模块化的工程结构和本地持久化层,同时建立带签名的 CI 构建,从开发的第一周起就能把构建交到测试人员手中。

  4. 04

    把安全内建于基础层

    认证流程、安全存储、生物识别把关、传输安全和设备证明都作为共享基础设施来实现,并在功能开发依赖它们之前,对照 OWASP MASVS 完成评审。

  5. 05

    在真实设备和真实网络上测试

    单元测试和快照测试在每次提交时运行,UI 测试在覆盖较旧硬件和操作系统版本的设备矩阵上运行。网络条件模拟用于检验丢包、高延迟和请求中途断连的情况,进程终止测试用于验证状态恢复。

  6. 06

    分阶段发布并紧盯数据

    构建经由内部和 beta 轨道进入分阶段的正式放量,在扩大放量范围之前,把无崩溃率、ANR 率、启动时间和关键漏斗事件与上一版本逐项对比。

取舍

决定设计走向的决策

在确定架构之前,我们会与您的团队逐一厘清的问题。

原生、Flutter 还是 React Native?
当产品依赖深度平台集成、高要求的图形能力,或需要第一时间采用新的操作系统特性,并且你能为两个平台都配齐人手时,选择原生。希望由一个团队做出跨平台一致的自定义界面,选择 Flutter;已有 TypeScript 和 React 经验,并希望与 Web 共享代码和人员,选择 React Native。不要仅仅为了把成本减半而选择跨平台,因为平台特有的工作并不会消失。
产品需要多大程度的离线能力?
默认使用读缓存:成本低,能消除大部分空白页面。当用户必须在信号不佳的环境下完成操作,且这些操作天然幂等时,再加上写入排队。只有当长时间离线工作是核心使用场景时,才投入做带双向同步的完整离线优先,因为冲突解决、未同步数据的迁移和支持工作的复杂度都是持续存在的成本。
移动应用是否应该有自己的 BFF?
当页面需要来自多个服务的数据、移动端与 Web 展示的内容出现分化,或通用 API 返回的数据远多于设备所需时,应该有。BFF 还提供了一个集中处理旧版本应用的位置。对于只有一个 API 和一个客户端的小型产品则可以省去,那样它只是多出来的一个可部署单元,却没有什么可聚合的。
是否应该使用证书锁定?
对于处理支付、健康数据或凭证的应用,如果威胁模型中包含在被攻陷的网络或设备上遭到拦截,就应当做证书锁定。锁定中间证书的公钥,或锁定一组包含备用密钥的公钥,并准备一条经过测试的轮换路径,因为一个过期又没有回退方案的锁定值,会把所有已安装的应用都挡在后端之外。对于风险较低的应用,平台信任库加上 Certificate Transparency 是合理的基线。
如何支持仍在使用中的旧版本应用?
让 API 只做增量式变更,让客户端忽略未知的字段和枚举值,并用服务端下发的开关控制新行为。由后端发布最低支持版本,应用据此提示或强制用户更新;只有当分析数据显示使用量低于约定的阈值之后,才下线旧的 API 行为。这件事要从第一个版本起就规划好,已经发出去的版本是无法事后补上的。
能否不等应用商店审核就发布修复?
服务端驱动的配置和功能开关可以在两个平台上即时关闭或调整某项行为。React Native 应用还可以在商店政策允许的范围内,以 OTA 方式下发 JavaScript bundle 更新,范围涵盖修复和小改动,但不包括对应用主要用途的更改。原生代码的改动始终需要新的二进制包并经过审核,因此有风险的功能应当带着远程 kill switch(紧急关闭开关)上线。

生产就绪

怎样才算生产就绪

如何保障安全

令牌和密钥保存在有硬件支持的 Keychain 与 Keystore 中,访问受生物识别或设备凭证控制;任何敏感内容都不会写入日志、明文偏好设置、备份或任务切换器的截图。传输使用 TLS,并在威胁模型有此要求时启用证书锁定;发布构建经过混淆,并检查是否残留调试标志。客户端被视为不可信:每一条重要的规则都在服务端再执行一遍。

如何扩展

移动端的规模,意味着设备数量不断增长且分布在众多版本上,因此负载由后端来承担:CDN、网关上按设备的限流,以及分页、可缓存的响应。推送扇出由基于队列的通知服务处理,它批量发送并清理失效的令牌。代码库则通过接口明确的功能模块来扩展,在团队扩大时把构建时间和合并冲突控制在合理范围内。

如何监控

崩溃、卡顿和 ANR 报告经过符号化并按发布版本分组,附带操作轨迹(breadcrumb)和设备上下文。性能监控从真实设备上记录冷启动、慢帧与冻结帧以及网络请求延迟,trace 标识符则把一次客户端请求与它在后端的 span 关联起来。分析事件遵循带版本的 schema,并遵守用户同意和平台的跟踪规则。

如何部署

流水线从打了标签的提交生成带签名、带版本号的构建,上传符号文件,并自动提交到 TestFlight 和 Play 测试轨道。正式环境使用 App Store 的分阶段发布和 Google Play 的分阶段放量,两者都可以暂停。审核时间没有保证,因此发布日程要为它留出余量,紧急止损则依靠远程开关。

如何集成

应用通过 OpenID Connect 与身份提供方集成,支付、地图、消息和分析类 SDK 则封装在内部接口之后,每一个都可以被替换,或在测试中用桩替代。每个第三方 SDK 都要评估其体积、启动开销、权限和数据收集情况,因为它的行为必须在 App Store 隐私标签和 Play 数据安全表单中申报。

技术

根据工作负载和您现有的技术资产来选择。

  • Swift and SwiftUI
  • Kotlin and Jetpack Compose
  • Flutter
  • React Native
  • Kotlin Multiplatform
  • SQLite
  • Firebase Cloud Messaging
  • Apple Push Notification service
  • Firebase Crashlytics
  • Sentry
  • Fastlane
  • GitHub Actions
  • TestFlight
  • Google Play Console

从零到生产

工程生命周期

  1. 01

    想法

  2. 02

    探索

  3. 03

    系统架构

  4. 04

    UX / 产品设计

  5. 05

    应用工程

  6. 06

    AI / 数据层

  7. 07

    云基础设施

  8. 08

    安全

  9. 09

    质量保证与性能

  10. 10

    部署

  11. 11

    可观测性

  12. 12

    持续演进

常见问题

常见问题

我们应该做原生还是跨平台?

这取决于产品中有多少是平台特有的,也取决于你现有的团队。内容类和流程类应用很适合用 Flutter 或 React Native 共享代码,而建立在相机、蓝牙、后台处理或新操作系统特性之上的产品,通常用 Swift 和 Kotlin 更合适。针对风险最高的那项设备集成做一次简短的技术验证(spike),比一张对比表更能可靠地回答这个问题。

推送通知究竟是如何工作的?

应用在 iOS 上向 APNs、在 Android 上向 FCM 注册,获得一个设备令牌,并把它发送给你的后端。后端请求平台服务向该令牌投递消息,平台在设备可达时完成投递。投递只是尽力而为,可能被延迟、合并或丢弃,令牌也会变化,因此后端必须处理失效的令牌,应用也必须从 API 拉取权威状态。

应用商店审核需要多久,哪些原因会导致被拒?

审核时间并不固定,两家商店都不作保证,因此安排发布时不应假设一个固定的周转时间。常见的被拒原因包括隐私声明不完整、缺少账号删除功能、申请权限却没有说明明确用途、审核人员无法正常登录,以及支付流程与应用内购买规则相冲突。每次提交时附上测试账号和审核备注,可以避免其中的很多问题。

设备上的敏感数据是如何保护的?

凭证和密钥放在带硬件级保护的 Keychain 或 Keystore 中,体量较大的敏感数据集则用保存在其中的密钥加密。生物识别校验与一个密码学密钥绑定,因此无法通过修改一处条件判断来绕过。敏感页面被排除在截屏和备份之外,服务端也从不依赖客户端已经执行过某条规则。

一套后端能否同时服务 Web 和移动客户端?

可以。领域服务和数据是共享的,每种客户端各有一层轻量的 BFF 来做适配:移动端的 BFF 倾向于更少、更小、能容忍版本差异的响应,Web 端的 BFF 则可以随 Web 部署同步变更。这样业务规则只保留在一处,每种客户端也可以按各自的发布节奏演进。

有一套必须在生产环境中正常运行的系统?

告诉我们您正在构建什么。工程师会阅读您的留言,并在两个工作日内回复。