# 移动应用 架构模板 > **代表产品**:绝大多数 iOS / Android 应用(社交、笔记、出行、银行、协作工具……) > **一句话定位**:在「网络说断就断、电量内存都吃紧」的手机上,做出一个永远立刻有反应、断网也能用的应用。 --- ## 1. 一句话定位 一个移动应用 = **一个跑在你口袋里的「半个系统」** + **一套让它和云端「悄悄对齐」的同步机制**。 架构上最反直觉的一点:它和你熟悉的「网页」最大的不同,不是屏幕小,而是**你不能假设网络存在**。网页可以「每次点击都问一次服务器」,因为它通常连着稳定的宽带;手机却随时在电梯、地铁、地库里掉线。于是整套架构的核心命题从「怎么把请求发给服务器」变成了「**网断了,这个 App 还能不能用、用户还感不感觉得到**」。答案就是这份模板的灵魂:**离线优先(offline-first)+ 数据同步**。 ## 2. 业务本质:它在解决什么问题 用户要的是一个**随手掏出来、点一下立刻有反应、不管有没有信号都能继续干活**的工具。它取代的是「打开网页 → 转圈等加载 → 网卡了重来」的糟糕体验。 手机这个载体带来两条铁打的现实: - **网络不稳定且昂贵**:信号会断、延迟会高、流量要钱。任何「必须等服务器回话才能往下走」的设计,在弱网下都会变成转圈和卡死。 - **设备资源受限**:电量、内存、存储、CPU 都有限,还要和几十个别的 App 抢。你不能像服务器那样「无脑多算」。 **关键事实:在手机上,「立刻响应」不是优化项,而是生存线。** 网页慢半秒用户可能忍了,手机 App 点一下没反应,用户的手指已经在划去关掉它了。这一条决定了后面几乎所有架构取舍——尤其是「**为什么要把数据先写在本地、让 UI 先动起来**」。 ## 3. 核心需求与约束 把需求拆成两类。**区分「功能」和「质量」是架构师的第一基本功。** **功能性需求(系统要能做什么):** - [ ] 即时响应的交互:点了就动,不等网络。 - [ ] 离线可用:没信号也能浏览、编辑、创建,联网后自动补上。 - [ ] 数据同步:本地改动同步到云端,云端改动拉回本地,多设备保持一致。 - [ ] 冲突处理:同一条数据在两个设备(或本地与云端)被同时改了,要能合理收敛。 - [ ] 推送通知:服务端有新消息/变更时,能主动唤醒 App。 - [ ] 媒体处理:拍照、上传图片视频,弱网下不阻塞主流程。 - [ ] 后台同步:App 切到后台或刚启动时,悄悄把数据对齐。 **非功能性需求 / 质量属性(这才是架构的主战场):** | 质量属性 | 目标 | 为什么对这类系统重要 | |---|---|---| | **交互响应延迟** | < 100ms(本地操作) | 用户的手指比网络快得多,反应慢一点就「觉得这 App 卡」。 | | **弱网/断网可用性** | 离线也能完成核心操作 | 地铁、电梯、地库是常态,不是边缘情况。 | | **同步正确性** | 不丢数据、不错乱 | 用户在两台设备改了同一条笔记,结果丢了一半——这是致命的信任崩塌。 | | **资源占用** | 省电、省内存、省流量 | 费电费流量的 App 会被用户卸载,也会被系统后台杀掉。 | | **启动速度** | 冷启动尽量快 | 第一屏要立刻出内容,哪怕先出缓存的旧数据。 | **关键约束(不可逾越的边界):** - 🔴 **网络不可靠,且不在你掌控之内**。这是头号约束,整套架构为它服务。 - 🔴 **你无法强制所有用户升级**。老版本客户端会在用户手机里**存活很久很久**(有人就是不更新)。这意味着**服务端 API 必须长期向后兼容**——这是移动端最容易被忽视、又最致命的约束。 - 🔴 **设备资源有限**,本地存储和计算都要省着用。 - 客户端代码一旦发布就**收不回来**了,带 bug 的版本会持续产生数据,服务端要能容忍。 ## 4. 架构全景图 ``` ┌──────────────────────────── 用户的手机(客户端 = 半个系统)─────────────────────────┐ │ │ │ ┌──────────────┐ ┌──────────────────┐ ┌──────────────────────────┐ │ │ │ UI 层 │◀────▶│ 状态管理 │◀────▶│ 本地数据库 / 缓存 │ │ │ │ (界面、交互) │ │ (内存中的应用状态) │ │ (持久化的「真相副本」) │ │ │ └──────────────┘ └──────────────────┘ └────────────┬─────────────┘ │ │ ▲ 写操作先落到本地,UI 立刻反映(乐观更新) │ │ │ │ ▼ │ │ │ ┌──────────────────────────┐ │ │ └──────────── 同步完成/失败后回灌 ──────────│ 同步引擎 + 待发队列 │ │ │ │ (排队、重试、拉取、合并) │ │ │ └────────────┬─────────────┘ │ └────────────────────────────────────────────────────────────────┼──────────────────┘ │ 弱网下异步、可中断、可重试 推送「有新变更,快来拉」 │ ┌───────────────────────┐ 唤醒 ┌──────────▼──────────────────┐ │ 推送服务 │ ─────────────────────────▶│ 移动端专属后端 (BFF) │ │ (系统级通道) │ │ • 为移动端裁剪/聚合数据 │ └───────────▲───────────┘ │ • 收上行变更、下发远端变更 │ │ 触发推送 │ • 鉴权、限流、API 版本兼容 │ │ └───┬───────────┬──────────┘ │ │ │ ┌───────────┴───────────────────────────────────────────▼──┐ ┌────▼──────────────┐ │ 同步服务 / 业务后端 │ │ 对象存储 │ │ • 持有云端「权威数据」 • 版本/时间戳 • 冲突裁决 │ │ (图片/视频等媒体) │ └───────────────────────────────────────────────────────────┘ └───────────────────┘ ``` > 灵魂部件是左侧那一整块**客户端**和中间的**同步引擎**——它们让「网络」从「每次操作都要等的拦路虎」退化成「后台默默对齐的可选项」。这就是「半个系统在你口袋里」的含义。 ## 5. 组件职责 逐个说明上图里每个关键部件**做什么 + 为什么需要它**(没有「为什么」的部件就是过度设计)。 - **UI 层**:渲染界面、响应手势。它**只读本地状态**,从不直接等网络。*为什么需要*:UI 与网络解耦,才能做到「点了就动」——这是即时响应的前提。 - **状态管理**:内存里那份「当前界面该显示什么」的应用状态,是 UI 和本地数据库之间的中介。*为什么需要*:界面要快速、一致地反映数据变化,需要一个集中、可预测的状态来源。 - **本地数据库 / 缓存**:手机上持久化的数据副本,是**离线时的「本地真相」**。所有读都先读它,所有写都先落它。*为什么需要*:没有本地持久化,就没有离线可用,也没有冷启动时「先出旧数据」的快。它是离线优先的物理基础。 - **同步引擎 + 待发队列**:把本地的写操作排进队列,在有网时异步发往服务端;同时拉取远端变更并合并进本地。负责重试、去重、断点续传、冲突处理。*为什么需要*:它是「本地」和「云端」之间的缓冲带,把不可靠的网络的所有麻烦(断、慢、重试、乱序)都吸收在这一层,不让它污染 UI。 - **移动端专属后端(BFF, Backend for Frontend)**:专门服务移动端的接入层。把后端多个服务的数据**裁剪、聚合成移动端正好需要的形状**,一次返回,减少往返和流量;承担鉴权、限流,并**守住 API 的版本兼容**。*为什么需要*:通用后端接口往往太「胖」(字段多、要多次请求),弱网下浪费流量和往返;BFF 替移动端把数据「揉好了再喂」。 - **同步服务 / 业务后端**:持有云端的**权威数据**,记录每条数据的版本/时间戳,在收到上行变更时做冲突裁决,并把变更广播给其它设备。*为什么需要*:多设备要对齐,必须有一个「谁说了算」的权威源和一套裁决规则。 - **推送服务**:通过系统级通道,在服务端有新变更时**主动唤醒**沉睡的 App 去拉取。*为什么需要*:App 不可能一直轮询(费电费流量),靠推送「叫醒」是省资源地保持新鲜度的关键。 - **对象存储**:存放图片、视频等大体积媒体。*为什么需要*:媒体又大又不变,不该塞进数据库或随同步消息走;客户端直传/直取对象存储,把媒体通道和数据同步通道分开。 ## 6. 关键数据流 挑 3 个最能体现这个系统特点的场景。 **场景一:用户创建一条数据(离线优先 + 乐观更新的核心路径)** ``` 1. 用户点「保存」这条笔记 ──▶ 同步引擎先把它写进【本地数据库】(标记为「待同步」) 2. ──▶ 状态管理立刻更新 ──▶ UI 立刻显示这条笔记 ★ 此刻用户已经看到结果了,完全没等网络。这就是「乐观更新」。 3. ──▶ 同步引擎把这次写操作丢进【待发队列】,后台慢慢处理 4. 有网时 ──▶ 异步发往 BFF ──▶ 同步服务落库、分配版本号 5. 成功 ──▶ 把本地这条标记从「待同步」改成「已同步」(UI 几乎无感) 失败 ──▶ 留在队列里,过会儿重试;期间 UI 照常可用 ``` > 注意第 2 步:**网络还没参与,用户已经在用了。** 这就是离线优先把「转圈等服务器」变成「立刻有反应」的魔法。网络从「主角」退居「后台收尾的配角」。 **场景二:拉取并合并远端变更(多设备对齐)** ``` 设备 B 改了某条数据 ──▶ 同步服务记下新版本 ──▶ 通过【推送服务】给设备 A 发一条「有变更」 设备 A 收到推送 ──▶ 唤醒 App ──▶ 同步引擎带上「我本地的版本/游标」去 BFF 拉增量 BFF ──▶ 只返回「比你新的那部分变更」(增量,不是全量,省流量) 同步引擎 ──▶ 把远端变更【合并】进本地数据库: · 本地没动过这条 → 直接覆盖 · 本地也改过这条 → 触发【冲突解决】(见决策 3) 合并完 ──▶ 状态管理刷新 ──▶ UI 更新 ``` > 架构要点:**拉增量而非全量**,靠的是双方都带「版本/游标」;**唤醒靠推送而非轮询**,省电省流量。 **场景三:上传一张图片(媒体与数据分流)** ``` 1. 用户选了一张图 ──▶ 客户端先在本地生成缩略图,UI 立刻显示(占位) 2. ──▶ 把「上传图片」任务丢进待发队列(可断点续传) 3. 有网时 ──▶ 客户端把图片直传【对象存储】,拿到一个引用(URL/key) 4. ──▶ 只把这个「引用」随数据同步发给后端(同步消息里不夹大文件) 5. 其它设备拉到这条数据 ──▶ 按引用按需从对象存储取图 ``` > 架构要点:**大媒体走对象存储、小元数据走同步通道**,两条路分开;弱网下图片传一半断了能续,且绝不卡住主流程。 ## 7. 数据模型与存储选择 核心实体:`用户` ─ `设备` ─ `领域对象(如 笔记/订单/消息)`;每个领域对象都挂着**同步元数据**:`版本号 / 时间戳`、`同步状态(待同步/已同步/冲突)`、`本地ID 与 服务端ID 的映射`。媒体则是独立的 `媒体对象`,数据里只存它的引用。 | 数据 | 存储类型 | 为什么 | |---|---|---| | 客户端的领域数据(可离线读写) | 设备本地的嵌入式数据库 | 要离线可用、要快速本地查询、要持久化「本地真相」 | | 待发操作 / 同步队列 | 本地持久化队列 | App 被杀掉重启后,没发出去的改动不能丢 | | 图片 / 视频等媒体 | 对象存储 | 大、不变、按引用取;不该塞进数据库或同步消息 | | 云端权威数据 + 版本信息 | 服务端数据库(支持按版本/游标增量查询) | 要做冲突裁决和「只发增量」,必须能按版本检索 | | 访问令牌 / 密钥 | 设备的安全密钥库(系统级加密区) | 敏感凭证,绝不能明文落普通存储(见第 10 节) | > 教学点:**移动端的「本地数据库」不是缓存,而是「真相的一份副本」。** 把它当成「读不到再去服务器拿」的临时缓存,就退回了在线优先的老路;把它当成「本地权威、后台再和云端对齐」,才是离线优先。**客户端持有真实状态,正是它是「半个系统」而非「瘦客户端」的体现。** ## 8. 关键架构决策与权衡 ⭐ **(本模板最值钱的一节。)** 移动端的所有岔路口,几乎都绕着「网络不可靠」和「客户端持有多少状态」打转。 **决策 1:离线优先,还是在线优先?** - 在线优先(瘦客户端):每次操作都请求服务器,客户端几乎不存东西。实现简单、永远是最新数据、不用处理同步与冲突。**但弱网/断网下直接卡死、不可用**,每次交互都要等一个网络往返,体验慢。 - 离线优先(厚客户端):本地先写、UI 先动,后台再同步。**断网也能用、点哪都快**。代价是要自建本地存储、同步引擎、冲突解决,复杂度高了一个数量级。 - **取向**:**任何「交互频繁、希望顺滑、可能在弱网下使用」的 App,都该离线优先。** 这是移动端区别于网页的根本选择。代价是同步与冲突的全部复杂度——但这正是移动架构的核心价值所在,躲不掉。纯查询类、强一致要求极高(如实时余额)的场景可保留在线优先或混合。 **决策 2:同步策略——全量、增量、还是双向?** - 全量同步:每次把所有数据拉一遍。实现最简单,但**数据一多就费流量、费电、费时间**,弱网下根本拉不完。 - 增量同步:双方各记「版本/游标」,只传「上次之后变了的部分」。省流量、可断点续传。代价是要在两端维护版本状态、处理乱序和丢包。 - 双向同步:本地改的往上推、云端改的往下拉,两个方向都要。功能最完整(支持多设备协作),但**冲突几乎必然发生**,必须配冲突解决。 - **取向**:**增量 + 双向**是成熟移动应用的标配。先用「拉增量」把流量打下来,再用「双向 + 冲突解决」支撑多设备。代价是同步引擎是整个 App 最难写对、最容易藏 bug 的地方——值得投入最多的设计和测试。 **决策 3:冲突怎么解决?(双向同步的必答题)** - **后写赢(Last-Write-Wins)**:谁的时间戳/版本新,就用谁的。实现最简单,但**会悄悄丢掉另一边的改动**,对重要数据危险。适合「不那么重要、覆盖了也没关系」的字段(如「最后阅读位置」)。 - **版本向量 / 因果追踪**:记录每条数据「在哪些设备上改过几次」,据此判断是「真冲突」还是「一方包含另一方」。能精确识别冲突,代价是元数据更重、逻辑更复杂。 - **CRDT 思想(无冲突合并的数据结构)**:把数据设计成「无论以什么顺序合并,结果都一致」的结构(如计数器、集合、协作文档)。能做到**自动合并、永不丢改动**,但只适用于能套进这类结构的数据,且实现门槛高。 - **取向**:**按数据的重要性分级**——无关紧要的用后写赢,重要的列表/集合考虑 CRDT 思想或语义合并,真冲突无法自动解的就**抛给用户手动选**(「保留这个还是那个」)。**没有放之四海皆准的策略,关键是想清楚「这条数据被覆盖丢了,用户能不能接受」。** **决策 4:客户端持有多少状态?(厚 vs 薄)** - 薄:只缓存少量数据,业务逻辑尽量放服务端。客户端轻、好维护、逻辑改了不用发版,但离线能力弱、交互慢。 - 厚:本地存大量数据、跑大量业务逻辑(校验、计算、排序),离线能力强、响应快。代价是逻辑分散在客户端和服务端两处、**容易不一致**,且客户端逻辑发出去就难改。 - **取向**:**「能本地算的就本地算,但权威裁决留服务端」。** 客户端可以乐观地算结果让 UI 先动,但最终对错以服务端为准。注意:客户端逻辑一旦发布就改不动,所以**别把『可能频繁变』的规则(如风控、定价)硬编进客户端**——那些要么放服务端,要么做成可下发的配置。 **决策 5:数据怎么喂给移动端——通用 API 还是 BFF 裁剪?** - 通用 API:后端提供一套通用接口,移动端自己组合调用。后端省事,但移动端常要**发好几次请求、收回一堆用不上的字段**,弱网下往返和流量都浪费。 - BFF(为移动端裁剪聚合):专设一层,把移动端某个页面需要的数据**一次性聚合、裁剪好**再返回。 - **取向**:移动端值得上 BFF。**「一次往返拿到一屏正好需要的数据」对弱网体验是巨大的优化。** 代价是多维护一层,且 BFF 要跟着客户端的页面需求演化。 **决策 6:API 版本兼容——能不能假设用户都升级了?** - 假设都升级:服务端只伺候最新版,接口想改就改。**但现实中老版本会在用户手机里活很多年**,一旦服务端改了它依赖的接口,这些老客户端就**直接崩溃或功能失灵**。 - 永远向后兼容:新增字段只增不删、不改老字段语义、用版本协商,让老客户端继续能跑。 - **取向**:**移动端 API 必须向后兼容,这是铁律,没有商量余地。** 因为你**无法强制升级**,任何「破坏性变更」都等于主动让一批用户的 App 崩掉。代价是接口只能「向前长」、要长期背着历史包袱、要有清晰的版本与弃用策略。**这是移动架构区别于「内部服务间调用」最关键的纪律。** ## 9. 规模化与瓶颈 和普通后端不同:移动端的瓶颈往往不在「服务器扛不扛得住」,而在**「最后一公里的网络」和「同步本身」**。 - **第一个瓶颈:同步的冲突与数据量。** 用户数据越多、设备越多,全量同步就越拉不动,冲突也越频繁。 破解:① 全量改增量(只传变更);② 分页/分块同步,别一口气拉完;③ 按「最近/最常用」优先同步,冷数据按需拉;④ 冲突策略分级,把能自动合并的都自动掉,减少打扰用户。 - **第二个瓶颈:弱网体验。** 信号差时同步慢、媒体传不动、操作像是「没反应」。 破解:① 乐观更新让 UI 永远先动,把网络藏到后台;② 队列化 + 断点续传,断了能续、重启不丢;③ 媒体走对象存储直传、可压缩、可延后;④ 请求合并与去抖,别在弱网里狂发小请求。 - **第三个瓶颈:推送可达性。** 推送可能延迟、被系统限流、用户关了通知,导致客户端「不知道有新数据」。 破解:① 推送只当「提示去拉」,不当「数据本身」(推送可能丢,数据不能丢);② 配合「App 回到前台/定时」的兜底拉取;③ 关键变更用「下次同步必然能拉到」来保证最终一致,不依赖单次推送送达。 - **第四个瓶颈(移动端独有):客户端版本碎片化。** 用户停留在五花八门的老版本上,服务端要同时伺候它们全部。 破解:① API 严格向后兼容;② 把易变规则做成「服务端可下发的配置」,绕开发版;③ 监控各版本占比,对实在太老、无法兼容的版本做「优雅的强制升级提示」;④ 服务端要容忍老/带 bug 客户端产生的脏数据,做防御性校验。 ## 10. 安全与合规要点 - 🔴 **设备会丢、会被偷、会被越狱/Root。** 你必须假设「攻击者能物理接触这台手机、能读到 App 的本地文件」。 - **本地敏感数据要加密**:存在手机上的个人信息、业务数据,该加密就加密;高敏感数据(密码、密钥)只放系统提供的**安全密钥库**,绝不明文落普通数据库或文件。 - **令牌安全存储**:登录令牌放安全密钥库,不放普通存储;设计**短时令牌 + 可刷新/可吊销**,这样设备丢了能在服务端让它失效。 - **传输安全 + 证书校验**:所有通信加密;对关键接口做**证书绑定(pinning)**,防止有人在中间用伪造证书窃听/篡改流量。 - **越狱 / Root 风险**:在这类设备上,App 的本地防护(包括本地存的密钥)都可能被绕过。架构上的对策是**「不把信任放在客户端」**——真正重要的校验、风控、扣费判断都在服务端做,客户端的结论一律当「待服务端确认」。 - **服务端不信任客户端**:任何来自客户端的数据都可能被篡改(包括被改过的老版本/破解版客户端发来的)。服务端必须重新校验权限、参数、配额。 ## 11. 常见误区 / 反模式 - ❌ **把 App 当瘦客户端,每个操作都同步请求服务器** → ✅ 默认离线优先:本地先写、UI 先动,弱网下才不会卡死。 - ❌ **不做乐观更新,点了之后转圈等服务器回话** → ✅ 用户操作立刻反映到本地与 UI,网络在后台收尾。**感知响应比真实往返更重要。** - ❌ **API 想改就改,假设用户都升级了** → ✅ API 永远向后兼容;老客户端会长期存在,破坏性变更等于让它们崩溃。 - ❌ **把敏感数据明文存在本地** → ✅ 加密存储,凭证进安全密钥库;假设设备会被攻破。 - ❌ **每次同步都拉全量数据** → ✅ 带版本/游标做增量同步,省流量、省电、弱网下才拉得完。 - ❌ **把本地数据库只当『读缓存』,真相永远在服务器** → ✅ 本地是「真相的一份副本」,这才撑得起离线可用。 - ❌ **把推送当成『数据本身』,推送丢了数据就丢了** → ✅ 推送只是「提示去拉」,数据靠同步保证最终一致。 - ❌ **把易变的业务规则(风控/定价)硬编进客户端** → ✅ 放服务端或做成可下发配置;客户端发出去就改不动。 - ❌ **把大媒体塞进同步消息一起传** → ✅ 媒体走对象存储直传,只同步它的引用。 ## 12. 演进路线:MVP → 成长期 → 成熟期 架构是会长大的。**别拿成熟期的图去套 MVP。** | 阶段 | 用户/规模量级 | 架构长什么样 | 此时该操心什么 | |---|---|---|---| | **MVP** | 验证想法 | **在线优先的薄客户端**:界面直接调后端接口,几乎不存本地数据,网好就能用 | 先验证「有没有人要这个 App」,别一上来就建同步引擎 | | **成长期** | 万~百万用户 | 加**本地缓存 + 乐观更新**:核心数据缓存到本地,操作先落本地让 UI 先动,后台单向同步;引入推送、BFF;API 开始讲究向后兼容 | 把弱网体验和响应速度做上去;盯住「点了没反应」的卡点 | | **成熟期** | 千万级以上 / 多设备 | **完整离线优先 + 双向增量同步**:本地权威数据库、健壮的同步引擎、分级冲突解决、断点续传、媒体分流、严格的 API 版本治理与配置下发 | 同步正确性、冲突收敛、版本碎片化治理、资源占用与省电 | ## 13. 可复用要点 - 💡 **先问「这个系统能不能假设网络存在」,答案决定一切。** 不能,就必须把状态放到离用户最近的地方(本地),让远端通信变成可选的后台行为。这条思想也适用于任何「边缘计算」「弱连接 IoT」场景。 - 💡 **感知响应常比真实延迟更重要。** 乐观更新没让数据更早到服务器,但让用户「觉得快」。任何「让用户更早看到反馈、把等待藏到后台」的设计都值钱。 - 💡 **把不可靠的东西(网络)的所有麻烦,收敛到一个专门的缓冲层(同步引擎)。** 别让重试、乱序、冲突这些脏活漏到 UI。这等同于「把易变/不可靠的依赖隔离在一层之内」的通用思想。 - 💡 **凡是『发布出去就收不回来』的东西,接口都必须向后兼容。** 移动客户端如此,对外开放的 API、被别人依赖的数据格式也如此。无法强制对方升级时,兼容性就是纪律。 - 💡 **不要把信任放在你控制不了的地方。** 客户端运行在用户(可能是攻击者)的设备上,它的任何结论都得由你能掌控的服务端来确认。 ## 🎯 随堂检验 --- ## 参考原型与延伸阅读 > 本模板基于以下**官方架构指南**与**真实开源项目**整理。 **📖 官方指南 / 理念:** - [Android: Build an offline-first app](https://developer.android.com/topic/architecture/data-layer/offline-first) — Google 官方:本地数据源作为唯一真相、UI 读本地、后台同步引擎。 - [Local-first software (Ink & Switch)](https://www.inkandswitch.com/essay/local-first/) — 提出 "local-first" 概念的原始长文:本地优先、离线可用与冲突合并。 **🔧 开源原型(可直接读代码):** - [automerge/automerge](https://github.com/automerge/automerge) — 经典 CRDT 库,无需中心服务器即可自动合并多设备并发改动。 --- > 📌 一句话记住移动应用:**它不是「屏幕变小的网页」,而是「一个揣在口袋里、随时可能掉线的半个系统」——所有架构取舍,最终都在回答『网断了,它还顺不顺手、数据还对不对得齐』。**