客户端架构:为什么、什么时候、怎么做

客户端架构:为什么、什么时候、怎么做

一、为什么需要架构

代码量涨上去之后,没有架构的系统会:

  • 改一处崩三处------耦合太重,牵一发动全身
  • 新来的人看不懂------没有边界,没有规则,每个类都能碰任何东西
  • 没人敢重构------不知道动了会影响谁
  • 越迭代越慢------加功能的成本指数级上升

架构本质就是在回答一个问题:怎么让系统在变大的过程中,改东西的成本不跟着变大?

具体到客户端:

|-------|----------------------|
| 问题 | 架构解法 |
| 团队互相踩 | 模块拆边界,各管各的 |
| 改不动 | 接口隔离,内部变了不影响外部 |
| 改崩了 | 分层约束,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          │
└─────────────────────────────────┘

规则

  1. 对外看业务/组件------负责哪个模块就只动哪个
  2. 跨模块调接口不调实现------用 api 模块,不直接依赖实现
  3. 模块内部才按类型分

宏观按业务/组件划分团队边界,微观按类型组织模块内部代码。 分法只是手段,关键是让每个人知道自己的边界在哪。


四、架构与设计

|------|--------|-------|
| | 架构 | 设计 |
| 回答什么 | 规则和边界 | 怎么实现 |
| 粒度 | 模块级 | 类/方法级 |
| 变的原因 | 业务方向变了 | 需求变了 |
| 变的影响 | 牵一发动全身 | 改了就改了 |
| 关注点 | 不能做什么 | 怎么做 |

架构定规则,设计定实现。架构是骨架,设计是肌肉。骨架再好,肌肉长歪了也跑不动。


五、架构规则(按常用程度排序)

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 + 关键路径集成测试 |

相关推荐
shandianchengzi14 小时前
【科普】安卓|安卓手机上如何简便实现Ctrl+Z(需要键盘或一台Windows电脑)
android·windows·智能手机·计算机外设·安卓·科普·记录
多加点辣也没关系1 天前
设计模式-解释器模式
设计模式·解释器模式
Asurplus1 天前
23中设计模式
设计模式·创建型·结构型·行为型
geovindu1 天前
go: Semaphore Pattern
开发语言·后端·设计模式·golang·企业级信号量模式
aqi001 天前
一文理清 HarmonyOS 6.0.2 涵盖的十个升级点
android·华为·harmonyos·鸿蒙·harmony
写了20年代码的老程序员1 天前
写了 20 年 Java,我发现 90% 的 if-null 和 try-catch 其实是因为缺了一条原则
设计模式·ai编程
货拉拉技术1 天前
私域转化率翻倍的秘密:我们把多模态Agent融进了私域营销
人工智能·算法·设计模式
看山是山_Lau1 天前
抽象工厂模式:一整套对象族如何统一创建?
设计模式·抽象工厂模式
木易 士心1 天前
深入理解 OKHttp:设计模式、核心机制与架构优势
android·设计模式·架构