客户端架构:为什么、什么时候、怎么做
一、为什么需要架构
代码量涨上去之后,没有架构的系统会:
- 改一处崩三处------耦合太重,牵一发动全身
- 新来的人看不懂------没有边界,没有规则,每个类都能碰任何东西
- 没人敢重构------不知道动了会影响谁
- 越迭代越慢------加功能的成本指数级上升
架构本质就是在回答一个问题:怎么让系统在变大的过程中,改东西的成本不跟着变大?
具体到客户端:
|-------|----------------------|
| 问题 | 架构解法 |
| 团队互相踩 | 模块拆边界,各管各的 |
| 改不动 | 接口隔离,内部变了不影响外部 |
| 改崩了 | 分层约束,UI 不能直接调底层实现 |
| 各写各的 | 统一机制,消息/生命周期/跳转各一套规则 |
一句话:架构就是让你改代码的时候心里有底。
二、什么时候需要架构
需要
- 多人协作------规则不立,就是互相踩
- 长期迭代------活三年的项目,不架就越来越慢
- 复杂的业务域------逻辑缠绕的必须理清
- 有性能要求------冷启 2 秒内、帧率 60fps,没约束做不到了才补就晚了
- 跨团队依赖------模块被十几个团队调,接口没定义好就是灾难
不需要
- 一次性工具/脚本------用完就扔
- 原型验证/PoC------验证想法的阶段,架构是浪费
- 需求还没定形------方向都没定,先架构就是先入为主
- 一个人写、500 行代码------过度设计比没设计还坑
判断标准
改代码的成本会不会失控?
- 不会失控 → 不需要架构,别提前设计
- 已经觉得改东西心虚了 → 该架构了
- 还没写但预判会复杂 → 提前定边界,不用定死细节
架构是解决问题的,不是证明自己专业的。 200 行代码搞六层架构,不叫架构,叫自嗨。
三、架构怎么分
3.1 三种分法
按类的类型分(MVC/MVP/MVVM)
ui/ → Activity、Fragment
vm/ → ViewModel
repo/ → 数据层
model/→ 数据模型
- 解决:同一功能的 UI 和逻辑解耦,方便单元测试
- 适用:单人或小团队,模块内部组织
- 坑:业务一多,所有业务的 vm 全挤在一个包里
按业务分
chat/ → 聊天
contacts/ → 通讯录
discover/ → 发现
me/ → 我的
- 解决:团队边界清晰,互不干扰
- 适用:多业务线并行迭代
- 坑:公共能力放哪?每个业务自己写一套就重复了
按组件分
network/ → 网络层
image/ → 图片加载
badge/ → 角标组件
navigation/ → 路由
storage/ → 存储
- 解决:公共能力复用
- 适用:跨业务的基础设施
- 坑:业务逻辑不能放这里,否则组件变成大杂烩
3.2 真实项目:三层混合
┌─────────────────────────────────┐
│ 业务层(按业务分) │ ← 团队边界
│ chat / contacts / discover / me │
├─────────────────────────────────┤
│ 组件层(按组件分) │ ← 复用边界
│ badge / network / image / router│
├─────────────────────────────────┤
│ 模块内部(按类型分) │ ← 代码组织
│ ui / vm / repo / model │
└─────────────────────────────────┘
规则:
- 对外看业务/组件------负责哪个模块就只动哪个
- 跨模块调接口不调实现------用 api 模块,不直接依赖实现
- 模块内部才按类型分
宏观按业务/组件划分团队边界,微观按类型组织模块内部代码。 分法只是手段,关键是让每个人知道自己的边界在哪。
四、架构与设计
|------|--------|-------|
| | 架构 | 设计 |
| 回答什么 | 规则和边界 | 怎么实现 |
| 粒度 | 模块级 | 类/方法级 |
| 变的原因 | 业务方向变了 | 需求变了 |
| 变的影响 | 牵一发动全身 | 改了就改了 |
| 关注点 | 不能做什么 | 怎么做 |
架构定规则,设计定实现。架构是骨架,设计是肌肉。骨架再好,肌肉长歪了也跑不动。
五、架构规则(按常用程度排序)
5.1 模块与依赖
依赖规则
依赖方向只能从上往下,不能反过来
业务层 → 组件层 → 基础层
✅ ❌ 基础层不能调业务层
- 不能循环依赖------A 依赖 B,B 又依赖 A,就是没架构
- 接口所有权在调用方------谁用谁定义接口,实现方去实现,不是实现方塞接口给你
- 用 api/implementation 控制依赖传递------implementation 改了不触发下游编译
通信规则
模块之间怎么说话,架构必须定死:
|---------------|-----------|---------|
| 方式 | 适用场景 | 坑 |
| 直接接口调用 | 同进程、强依赖 | 耦合 |
| 回调/Listener | 一对一、异步结果 | 回调地狱 |
| EventBus | 一对多、松耦合 | 链路不可追踪 |
| LiveData/Flow | UI 响应数据变化 | 生命周期要管好 |
| 路由 | 跨模块跳转 | 参数类型不安全 |
规则:一个模块只用一种方式和外部通信,别混着来
路由与导航
- 统一路由表------所有页面注册在路由表,不允许硬编码类名跳转
- 路由拦截器------登录拦截、降级拦截、埋点拦截,统一在链路上插
- 路由降级------目标页面不存在(版本低/模块未加载),跳兜底页不是崩溃
- 路由参数校验------必填参数缺失要在路由层拦住,不能到了目标页才崩
- Deep Link------外部跳转必须过路由,不能绕过拦截器
演进规则
- 防腐层------老接口不能直接暴露给新模块,中间包一层适配
- 绞杀者模式------新功能走新架构,老功能逐步迁移,别一次性重写
- API 版本化------对外接口加版本号,老版本至少兼容两个大版本
5.2 状态与数据
状态管理
Single Source of Truth------一个状态只有一个地方是权威的
- 两个地方都写同一个状态 → 必出 bug
- 例:聊天 Tab 的未读数,权威数据源在 MessageManager,别的都是读
- 例:Tab 的选中状态在 TabBarManager,不能谁都能改
缓存策略
缓存不是想加就加的,架构要定规则
- 层级------内存 → 磁盘 → 网络,每层失效策略不同
- 一致性------磁盘缓存和内存状态不一致怎么办?以谁为准?
- 淘汰------LRU / TTL / 容量上限,不能无限涨
- 穿透------读不到要不要兜底值?兜底值要不要缓存?
离线与同步
没网不是空白页
- 离线可用------核心功能必须有离线数据兜底
- 数据同步策略------增量还是全量?推还是拉?冲突怎么解?
- 写后同步------离线操作先存队列,有网后自动同步,不是丢掉
- 同步状态可见------用户知道当前数据是不是最新的
状态保存与恢复
被系统杀了杀回来不能是空白
- onSaveInstanceState------关键状态必须保存,不能丢了
- 恢复顺序------先恢复框架状态,再恢复业务状态,再恢复 UI 状态
- 任务栈恢复------深任务栈被杀后回来,要能恢复到之前的页面
- 防抖防重------恢复过程中触发的操作要做防抖,不能重复执行
5.3 稳定性与容错
异常处理
|------------|------------------|
| 错误类型 | 策略 |
| 可恢复错误 | 重试 + 降级,用户无感 |
| 不可恢复错误 | 上报 + 降级到安全状态,不崩溃 |
| 第三方 SDK 错误 | 隔离 + 兜底,不拖垮宿主 |
| 数据格式错误 | 兜底值续命,不让异常传播 |
红线:任何模块的异常不能穿透到上层
降级与熔断
防止一人炸全锅
- 熔断------某个模块崩了不能拖垮整体
- 超时------跨模块调用必须设超时,不能无限等
- 降级------图片加载失败 → 占位图 → 灰色占位 → 不展示,每层都有兜底
防止自己炸自己
- 防重入------同一个方法不能并发进入
- 防泄漏------注册的监听必须能反注册
- 防雪崩------批量操作不能同步全量,要分批+异步
容灾
- 模块健康检测------连续崩溃 N 次自动禁用
- 兜底页------加载失败不是白屏
- 热修复通道------线上 bug 不等发版
- 数据修复------本地脏数据启动时检测 + 修复
资源管理
资源不是用完就算的,架构要管生命周期
- 注册必反注册------监听器、广播、Observer,泄漏就是 bug 源头
- 大对象主动回收------Lottie 动画资源什么时候释放?不能等 GC
- 连接/流的生命周期------HTTP 连接、长链接,谁负责关?
5.4 性能与资源
性能预算
架构要分配资源配额,不然就是丛林法则
- 启动时间预算------整体冷启上限多少?每个模块分多少?
- 内存预算------图片缓存上限多少?Lottie 同时播放几个?
- 帧率预算------Tab 切换动画不能卡,哪个环节是瓶颈要提前定
- 线程池配额------核心线程数、队列大小,不能一个模块占满
线程模型
谁跑哪个线程,架构必须定义
- 每个模块的线程归属------UI 操作必须在主线程,IO 操作必须有独立线程池
- 线程池是共享还是独占------共享省资源但会互相影响
- Handler 用谁的------不能用 new Handler(),必须明确是主线程 Handler 还是模块私有 Handler
- 并发边界------哪些操作是原子的,哪些需要加锁,加什么锁
包体积治理
架构阶段不考虑体积,后面减不动
- 模块体积预算------每个模块多大,增量不能超阈值
- 资源管控------图片/字体/Lottie 资源统一管理,不能重复引入
- 代码裁剪------ProGuard/R8 规则统一,反射要有 keep 规则
- 动态下发------非核心资源走动态加载,不打包进 APK
动画架构
不是想怎么动就怎么动
- 动画统一管理------所有动画走统一调度,不能多个动画同时抢主线程
- 动画降级------低端机禁用复杂动画,架构层控制
- 动画中断------页面退出时正在播的动画必须能立即停止
- 动画资源管控------Lottie/属性动画资源统一管理,不能无限加载
5.5 初始化与启动
启动顺序是架构问题,不是编码问题
- 依赖拓扑必须有向无环------A 初始化依赖 B,B 依赖 C,不能 C 又依赖 A
- 懒加载 vs 预加载必须明确策略------哪些启动时必须 ready,哪些等到用的时候再初始化
- 初始化失败要有兜底------不能因为一个非核心 SDK 初始化失败,整个 App 起不来
- 初始化时序可观测------每个阶段耗时打点,启动慢了能定位
5.6 配置与灰度
配置中心化
所有开关必须有一个统一入口
- 开关不能散落各处------今天一个 isXxxEnabled,明天一个 XxxSwitch,找都找不到
- 配置必须有默认值------服务端没下发、网络超时,App 不能挂
- 配置变更要能推送------开关改了,正在跑的逻辑怎么响应?轮询还是推送?
- 配置要有粒度------全量开关、灰度开关、AB 分流,不同粒度不同机制
灰度与实验体系
不是上线就完事,架构要支持灰度
- 实验维度------按用户/地区/版本/设备分级
- 实验互斥------两个实验不能同时影响同一段逻辑
- 实验回滚------出问题要秒级关掉,不是等发版
- 实验结果可归因------指标变化能对应到具体实验
5.7 可观察性
埋点架构
不是打一个 Log 就叫埋点
- 事件模型统一------所有埋点遵循同一 schema:who/when/where/what/how
- 埋点和业务解耦------不能在业务逻辑里到处插埋点代码,走 AOP 或拦截器
- 埋点可靠性------关键埋点必须保证上报成功,丢了要有补发机制
- 埋点去重------快速切 Tab 不能同一事件上报 10 次
日志体系
不是 Log.d 就完了
- 结构化日志------每条日志带模块名/级别/上下文,不是一坨字符串
- 日志分级------开发日志/关键路径日志/线上诊断日志,级别不同策略不同
- 线上日志隐私------用户数据脱敏,不能明文打出来
- 日志采样------不是每条都上报,高频日志要采样,否则流量炸
APM 与监控
性能不是出问题了才查
- 启动监控------冷启/温启/热启各阶段耗时,自动上报
- 帧率监控------掉帧率、卡顿堆栈,线上可追溯
- 内存监控------内存水位、GC 频率、大对象分配
- 网络监控------请求成功率、耗时分布、错误码分布
- 卡顿检测------主线程 > 300ms 的操作自动抓堆栈
- ANR 预警------主线程阻塞趋势,ANR 发生前预警
5.8 安全与隐私
安全边界
- 模块间数据不能互读------你的 SP 我不能直接访问,要走接口
- 敏感操作必须鉴权------功能能不能展示、操作能不能执行,不只靠 UI 判断
- 序列化边界------跨进程/跨模块传递的数据要校验,不能信任对方
隐私合规
不是法务的事,架构必须支持
- 数据采集审批------任何用户数据采集必须过审批流程,架构上要卡住
- 敏感数据脱敏------日志、埋点、上报中的手机号/身份证必须脱敏
- 权限使用审计------每次调摄像头/定位/通讯录要有审计记录
- 数据生命周期------用户数据什么时候创建、什么时候删除,架构要管
- 合规开关------不同地区不同法规(GDPR/个保法),架构要支持按区配置
权限模型
谁能动什么,架构要管
- 功能权限------这个 Tab 能不能展示、这个按钮能不能点
- 数据权限------模块 A 能不能读模块 B 的数据
- 运行时权限------Android 权限申请流程统一封装,不能各写各的
5.9 设计系统
UI 一致性不是靠设计师盯,是靠架构兜底
- Design Token------颜色/字号/间距/圆角全部 token 化,不能硬编码
- 组件标准化------按钮/卡片/列表项统一组件,不允许各写各的
- 主题切换------暗色/亮色/品牌换肤,架构要支持一层切换全局生效
5.10 工程化
编译架构
模块怎么拆影响编译速度,编译速度影响开发体验
- 模块粒度------太粗改一行全量编译,太细依赖关系爆炸,要平衡
- api vs implementation------依赖传递必须控制,一个模块 api 暴露多了,改内部实现全量编译
- 编译缓存------哪些模块稳定不改,可以预编译成 aar
- 构建变体------debug/release/minimap 要隔离,不能 debug 代码泄露到线上
SDK 接入规范
第三方 SDK 是最容易出事的
- 隔离层------所有第三方 SDK 必须封装一层,业务不能直接调 SDK API
- 初始化管控------SDK 初始化统一在初始化中心管理,不能散落在各处
- 版本锁定------SDK 升级必须全量回归,不能悄悄升
- 崩溃隔离------SDK 崩溃不能拖垮宿主,try-catch 包在最外层
- 权限最小化------SDK 申请的权限必须审核,不能要什么给什么
模块版本与发版
模块独立不是口号,要靠版本管理落地
- 语义化版本------大版本/小版本/补丁,什么时候升什么
- 兼容承诺------ api 模块的接口改了,多少个版本内必须兼容老接口
- 发版节奏------模块独立发版还是跟宿主走,架构决定
- 依赖声明------模块 A 声明依赖模块 B 的 [2.0, 3.0),不能隐式依赖
插件化与动态化
- 模块能不能热加载------哪些必须内置,哪些可以动态下发
- 版本兼容------宿主升级后,插件老版本能不能跑?兼容几代?
- 降级兜底------动态模块加载失败,用内置兜底还是白屏?
5.11 多进程架构
不是所有东西都能跑同一个进程
- 进程划分------主进程、push 进程、webview 进程、工具进程,为什么这么分
- IPC 边界------跨进程怎么通信?ContentProvider / AIDL / Messenger,选什么
- 进程间状态同步------主进程状态改了,其他进程怎么知道?
- 进程复活------拉起来之后状态恢复谁管?
5.12 测试策略
- 模块可测试性------依赖注入,不能 new 死实现
- 接口可 mock------跨模块依赖必须有接口层
- 关键路径有集成测试------启动链路、状态切换、关键业务流
六、架构规则速查表
|----|--------|---------|------------------------------|
| 排序 | 大类 | 小类 | 核心规则 |
| 1 | 模块与依赖 | 依赖规则 | 单向不循环,接口在调用方 |
| 2 | 模块与依赖 | 通信规则 | 每模块一种方式,不混用 |
| 3 | 模块与依赖 | 路由与导航 | 统一路由表 + 拦截器 + 降级 |
| 4 | 模块与依赖 | 演进规则 | 防腐层 + 绞杀者 + API 版本化 |
| 5 | 状态与数据 | 状态管理 | Single Source of Truth |
| 6 | 状态与数据 | 缓存策略 | 层级 + 一致性 + 淘汰 + 穿透 |
| 7 | 状态与数据 | 离线与同步 | 离线可用 + 同步策略 + 写后同步 |
| 8 | 状态与数据 | 状态恢复 | 保存/恢复顺序 + 任务栈 + 防抖 |
| 9 | 稳定性与容错 | 异常处理 | 不穿透 + 分级处理 + 隔离第三方 |
| 10 | 稳定性与容错 | 降级与熔断 | 降级/熔断/超时/防重入 |
| 11 | 稳定性与容错 | 容灾 | 健康检测 + 兜底页 + 热修复 + 数据修复 |
| 12 | 稳定性与容错 | 资源管理 | 注册必反注册 + 大对象主动回收 |
| 13 | 性能与资源 | 性能预算 | 时间/内存/帧率/线程池配额 |
| 14 | 性能与资源 | 线程模型 | 归属明确 + 池化 + 并发边界 |
| 15 | 性能与资源 | 包体积 | 模块预算 + 资源管控 + 代码裁剪 + 动态下发 |
| 16 | 性能与资源 | 动画架构 | 统一调度 + 降级 + 中断 + 资源管控 |
| 17 | 初始化与启动 | --- | 拓扑无环 + 懒预加载策略 + 失败兜底 |
| 18 | 配置与灰度 | 配置中心化 | 集中管理 + 默认值 + 推送 |
| 19 | 配置与灰度 | 灰度与实验 | 分级 + 互斥 + 秒级回滚 |
| 20 | 可观察性 | 埋点架构 | 事件模型 + 解耦 + 可靠 + 去重 |
| 21 | 可观察性 | 日志体系 | 结构化 + 分级 + 脱敏 + 采样 |
| 22 | 可观察性 | APM 与监控 | 启动 + 帧率 + 内存 + 网络 + 卡顿 + ANR |
| 23 | 安全与隐私 | 安全边界 | 数据隔离 + 鉴权 + 序列化校验 |
| 24 | 安全与隐私 | 隐私合规 | 采集审批 + 脱敏 + 审计 + 生命周期 + 合规开关 |
| 25 | 安全与隐私 | 权限模型 | 功能 + 数据 + 运行时三级管控 |
| 26 | 设计系统 | --- | Token 化 + 组件标准 + 主题切换 |
| 27 | 工程化 | 编译架构 | 模块粒度 + 依赖控制 |
| 28 | 工程化 | SDK 接入 | 隔离层 + 初始化管控 + 崩溃隔离 + 权限最小化 |
| 29 | 工程化 | 模块版本与发版 | 语义版本 + 兼容承诺 |
| 30 | 工程化 | 插件化与动态化 | 热加载 + 版本兼容 + 降级 |
| 31 | 多进程架构 | --- | 进程划分 + IPC + 状态同步 + 进程复活 |
| 32 | 测试策略 | --- | DI + 接口可 mock + 关键路径集成测试 |