客户端设计(下):场景流派与实战设计方式

客户端架构:为什么、什么时候、怎么做https://blog.csdn.net/mix39/article/details/161257993客户端设计(上):MVC/MVP/MVVM 与高内聚低耦合https://blog.csdn.net/mix39/article/details/161257807客户端设计(中):OOP、SOLID 与设计模式https://blog.csdn.net/mix39/article/details/148036409

客户端设计(下):场景流派与实战设计方式https://blog.csdn.net/mix39/article/details/161322269?spm=1001.2014.3001.5501

前言

ok,我们前面三张文章讲了为什么要做架构、什么时候需要架构和怎么做、常用设计原则和设计模式,感兴趣可以点击上方链接再看看,再讲一下这些年遇到的具体的问题和解决策略。

本文涉及到代码的地方均已微信为例,提供想象空间,并不是真的业务代码,只抽象出这些年见过的一些常见公共通用架构设计能力。写过安卓app或者客户端app代码的人都会,俺做一下整理归纳。

8.6 大型互联网 App

核心矛盾:多团队并行 + 快速迭代 + 稳定性要求高

一句话:互联网 App 的设计重点是"怎么让几百人同时改一个 App 还不炸"。

|----------|--------------------------------------|
| 设计重点 | 怎么做 |
| 组件化 | 业务组件 + 基础组件,每个组件独立工程,api 模块隔离 |
| 路由方案 | 统一路由表 + 拦截器链,跨组件跳转降级可控 |
| 配置中心 | 所有开关动态下发,支持灰度/AB/秒级回滚 |
| 降级体系 | 每个关键路径多层兜底,动画→图→红点→无 |
| 启动治理 | 有向无环拓扑排序 + 懒加载 + 分级初始化 |
| 监控体系 | 启动/帧率/内存/网络/卡顿/ANR 全链路 |
| 通信机制 | 接口 + 路由 + 事件总线,不直接引用实现类 |
| 发版节奏 | 组件独立发版 + 宿主集成,热修复兜底(有范围限制:不能改资源和类结构) |

互联网 App 的设计核心

  • 解耦------几百人改同一个 App,改动不能互相影响
  • 可控------任何功能都能远程开关,任何异常都能降级
  • 可观测------线上发生了什么必须看得见,不然就是完全不可观测
  • 可恢复------出问题能秒级回滚,不用发版

8.7 厂商类型内置 App

核心矛盾:一套代码跑几十个机型/品牌/渠道 + 系统限制多 + 资源受限

一句话:厂商 App 的设计重点是"一套代码跑几十个项目,改一次全部生效"。

|---------------|---------------------------------------------------------------|
| 设计重点 | 怎么做 |
| 软件共版 | 抽象平台差异层,一套核心代码 + 多套平台适配 |
| 多渠道 | 构建变体 + productFlavors,渠道差异用配置驱动 |
| 多 App | 同一系统上运行多个内置/外置应用,厂商自行开发内置应用,共享基础能力 |
| subModule | Gradle 自定义构建变体(flavorDimensions / productFlavors),同一模块构建出不同变体 |
| 音源策略 | 不同设备/场景选不同音源,策略模式 + 配置驱动 |
| 资源管控 | 设备分级,低端机减功能减动画减资源 |
| 兼容性 | API Level 适配封装层,上层不感知版本差异 |
| 包体积极致控制 | 能内置就内置(断网也要可用),手绘图片/动画替代预制资源,精打精算每个字节 |

厂商 App 的设计核心

  • 通用------套代码适配所有差异,不是每个场景写一套

  • 可插拔------功能模块可以按需组合,不同 App 不同配置

  • 可配置------同一个二进制跑在不同渠道,行为由配置驱动

  • 极致省------包体积、内存、功耗,每一点都要抠

8.8 具体设计方式详解

8.6 和 8.7 定义了两种场景流派的设计重点,下面展开每个设计方式的详细做法。

21 种设计方式总览

|----------|--------------------|----------------------|
| 分类 | 设计方式 | 一句话 |
| 编程范式 | 8.8.1 现代化编程 | 声明式 UI + 状态管理 + 异步编程 |
| 解耦通信 | 8.8.2 生产者消费者 | 队列解耦生产与消费 |
| | 8.8.9 事件驱动设计 | 发出事件的人不知道谁在监听 |
| | 8.8.16 通信设计 | 强依赖用接口,弱依赖用事件 |
| 并发资源 | 8.8.3 线程池隔离 | 不同业务用不同线程池 |
| | 8.8.10 内存防御设计 | 对外不信任,对内信任 |
| 网络缓存 | 8.8.4 抗弱网 | 没网不是空白页,慢网不是转圈页 |
| | 8.8.12 缓存设计 | 请求→内存→磁盘→网络 |
| 架构跨端 | 8.8.5 Native壳+跨端组件 | 壳稳定、肉灵活 |
| | 8.8.6 场景切面(AOP) | 横切逻辑集中管理 |
| | 8.8.7 动态化设计 | 编译期定死的越少越好 |
| | 8.8.8 组件化vs模块化 | 按业务拆vs按功能拆 |
| 稳定性 | 8.8.11 降级设计 | L0→L5,必须尽一切努力避免崩溃 |
| | 8.8.15 防御式编程 | 假设所有外部输入都可能失败 |
| | 8.8.13 启动设计 | 很大程度上取决于设计而非编码 |
| 状态流程 | 8.8.14 状态机设计 | 行为取决于状态,转换有规则 |
| | 8.8.18 设计思维 | 从需求推导到设计 |
| 厂商特有 | 8.8.17 通用能力设计 | 一次改动,后续加需求改动量最小化 |
| | 8.8.19 音频焦点策略 | 出声前申请焦点,拿到才播放 |

8.8.1 现代化编程

三件套:声明式 UI + 状态管理 + 异步编程

声明式 UI

命令式:告诉 UI 怎么做(setText、setVisibility、setColor......)

声明式:告诉 UI 是什么样(state → UI 映射,框架帮你刷新)

复制代码
命令式(Android View):
if (hasUnread) { badgeView.setVisibility(VISIBLE); badgeView.setText("3"); }
else { badgeView.setVisibility(GONE); }

声明式(ArkTS):
if (this.hasUnread) {
  Badge({ count: this.unreadCount })
}
// state 变了 UI 自动变

声明式(Vue):
<Badge v-if="hasUnread" :count="unreadCount" />
// state 变了 UI 自动变

好处:不用手动同步 UI 和数据------数据变了 UI 自动跟着变,不会出现"数据更新了 UI 忘了刷"的 bug。

代表:ArkTS(鸿蒙)、Compose(KMP)、React、Vue

状态管理

本质是观察者模式的工程化------UI 观察状态,状态是唯一权威。

复制代码
原则:
1. Single Source of Truth------一个状态只有一个写入者
2. 状态不可变------改状态 = 创建新状态,不是原地改
3. 状态下沉,事件上浮------子组件只读父组件的状态,事件回调给父组件处理

ArkTS:@State / @Prop / @Link 装饰器,AppStorage 全局状态
React:useState + useReducer + Context,Redux / Zustand 全局状态
Vue:ref / reactive + Pinia 全局状态
KMP Compose:MutableStateFlow + StateFlow, ViewModel + stateIn

异步编程

|----------------|----------------------|----------------------|
| 方式 | 特点 | 代表 |
| 回调 | 简单但容易嵌套地狱 | 通用 |
| Promise/Future | 链式调用,比回调好,但异常处理分散 | Vue/React 的 async 操作 |
| async/await | 看起来像同步,本质是协程 | ArkTS、Kotlin、JS/TS |
| 协程/Flow | Kotlin 首选,结构化并发,自动取消 | KMP |

链式编程 + 闭包

链式的本质是流畅接口(Fluent Interface)------方法返回 this 实现链式调用,Builder 模式是链式的一种应用。

复制代码
badgeManager
    .tab("chat")
    .show()
    .withNum(3)
    .withAnim(true)
    .onComplete { log("done") }

闭包让行为参数化------把"做什么"当参数传进去,而不是写死。

代表:ArkTS、React Hooks、KMP Compose、Vue Composition API

8.8.2 生产者消费者模式

生产者和消费者互不知道对方存在,中间靠队列解耦。

复制代码
Producer → Queue → Consumer
  (未读数数据源)    (消息队列)   (未读数展示)

|---------|---------------------------|
| 要素 | 说明 |
| 生产者 | 只管生产数据,不管谁消费、什么时候消费 |
| 消费者 | 只管消费数据,不管谁生产的、什么时候生产的 |
| 队列 | 缓冲区,解耦生产和消费的速度差异 |
| 背压 | 生产速度 > 消费速度时的策略:丢、等、批量消费 |

客户端场景

|---------|---------|-----------------|----------|
| 场景 | 生产者 | 消费者 | 队列 |
| 未读数更新 | 产品/营销推送 | BadgeManager 展示 | 未读数事件队列 |
| 消息通知 | 消息推送服务 | 通知栏展示 | 通知队列 |
| 预加载任务 | 启动流程 | 各 Tab 初始化 | 线程池任务队列 |
| 埋点上报 | 业务代码 | 埋点上报服务 | 埋点缓冲队列 |
| 流式 RPC | 服务端流式推送 | 客户端逐字消费 | 流式数据缓冲队列 |
| 会话消息 | 长链接/推送 | 聊天界面展示 | 消息缓冲队列 |
| 编解码相关应用 | 音视频采集 | 编码/解码模块 | 帧/数据缓冲队列 |

为什么用生产者消费者

  • 解耦------生产者和消费者各自独立演进
  • 异步------生产不等消费,不阻塞主流程
  • 缓冲------峰值时队列兜住,消费端按自己节奏处理
  • 可控------队列大小、消费速率、背压策略都可配置

8.8.3 线程池隔离

不同业务用不同线程池,一个炸了不拖垮其他。

有规模的 App 一般都需要,特别是启动优化那块------不然一启动就噶或者请求个数据一直搁那转圈圈。需要线程管控、资源调度。

大型 App 启动时如果不同业务同时开线程去进行耗时操作,有可能整个 App 都在抢夺资源,抢夺资源失败的业务直接 return 没有处理线程任务。且数量太多 App 可能直接噶掉。

复制代码
❌ 一个共用线程池:
   未读数加载 + 图片下载 + 数据同步 + 埋点上报 全挤在一起
   图片下载阻塞了 → 未读数加载不了 → 用户体验炸

✅ 隔离线程池:
   IO_POOL:图片、文件
   BADGE_POOL:未读数计算
   UPLOAD_POOL:埋点上报
   DB_POOL:数据库读写
   互相不影响

线程切换应有且仅有一条规则

  • 主线程 → 后台:统一一个入口(如 Dispatchers.io() 或统一的 ThreadScheduler)
  • 后台 → 主线程:统一一个入口(如 Dispatchers.main() 或统一的 UiHandler)
  • 不允许业务代码直接 new Thread、直接 runOnUiThread

|-----------|--------------------------------------|
| 设计要点 | 说明 |
| 核心线程数 | CPU 密集型 = CPU 核数,IO 密集型 = CPU 核数 × 2 |
| 队列大小 | 有界队列,不能无限排------内存会炸 |
| 拒绝策略 | 队列满了怎么办?丢弃 + 打日志 / 调用者线程跑 / 丢弃最老 |
| 线程命名 | 必须命名!出了问题看线程栈知道是谁 |
| 监控 | 队列积压量、活跃线程数、拒绝次数 |

线程池分配原则

  • 关键路径独占:启动、未读数展示这种用户可感知的用独立池
  • 非关键路径共享:埋点上报、日志这种可以共享一个大池
  • 第三方 SDK 隔离:SDK 用自己的池,别跟你的业务挤在一起

8.8.4 抗弱网

核心思想:没网不是空白页,慢网不是转圈页

|------------|---------------------------|
| 层级 | 策略 |
| 网络层 | 超时分级、重试策略(指数退避)、多路复用 |
| 缓存层 | 强缓存 + 协商缓存、离线数据兜底、过期数据先展示 |
| 降级层 | 接口失败 → 缓存 → 兜底默认值 |
| 预加载 | WiFi 下预拉取关键数据 |
| 增量更新 | 只拉 diff,减少传输量 |
| 数据压缩 | 请求/响应压缩 |
| 请求合并 | 多个请求合成一个,减少 RTT |
| 断点续传 | 大文件/长列表支持断点 |
| 网络状态感知 | WiFi/4G/3G/无网,策略自动切换 |

客户端弱网设计的黄金法则

复制代码
每次网络请求都要问自己:
1. 失败了怎么办?→ 兜底值
2. 超时了怎么办?→ 缓存值
3. 没网了怎么办?→ 离线数据
4. 数据过期了怎么办?→ 先展示旧的,后台拉新的

推荐:TTL 过期 + 读时刷新 组合

8.8.5 Native 壳 + 跨端组件业务

壳稳定、肉灵活------平台能力在 Native,业务逻辑在跨端。

|--------------|------------------------------|
| 设计要点 | 说明 |
| 壳只做壳的事 | 生命周期、权限、平台能力,不掺业务 |
| 业务只做业务的事 | 不直接调平台 API,通过壳暴露的接口 |
| 通信协议统一 | 不管哪种跨端方案,Native 侧接口统一封装 |
| 业务可插拔 | 不同业务选不同跨端方案 |
| 降级兜底 | 跨端页面加载失败?降级到 Native 或 H5 兜底页 |
| 调试体系 | 跨端调试链路要通 |

跨端框架的代价:跨端框架省的是编写成本,不省验证成本。选型时要问:双端验证成本、能力边界、抽象泄漏、工具链成熟度、代码归属、三端差异文档化、语法基因。

选型决策线

复制代码
业务复杂度低 + 组件够用 + 静态页面多 → 跨端框架收益大
业务复杂度高 + 需要动态能力 + 页面交互复杂 → 跨端框架成本 ≥ 原生

8.8.6 场景切面(AOP)

横切逻辑------日志、埋点、权限、性能监控、登录检查------从业务里抽出来集中管理。

复制代码
✅ 有 AOP:
@RequireLogin
@Trace("chat_open")
@Log
fun openChat() {
    chatManager.open(...)             // 只写业务
}

客户端怎么做 AOP

|----------------------|------------|----------|-----------|
| 方式 | 原理 | 适用场景 | 限制 |
| 注解 + APT | 编译期生成代码 | 路由、依赖注入 | 编译期,不能改逻辑 |
| 注解 + ASM | 编译期字节码插桩 | 埋点、性能监控 | 编译期,构建变慢 |
| 动态代理 | 运行期代理接口调用 | 接口层的横切逻辑 | 只能代理接口 |
| Gradle Transform | 编译期改 class | 全埋点、方法耗时 | 构建链路复杂 |
| Kotlin 协程拦截器 | 协程上下文插入逻辑 | 异步链路的横切 | 只在协程内 |
| Lifecycle 感知 | 生命周期回调 | 页面级的横切 | 只在生命周期内 |

8.8.7 动态化设计

编译期定死的越少越好,运行时可变的越多越好。

动态化三层

  • L1 资源动态化:运行时加载图片/动画/字体 → 最简单
  • L2 布局动态化:服务端下发 UI 描述,客户端渲染 → 中等
  • L3 逻辑动态化:服务端下发脚本/规则,客户端执行 → 最强但风险最大

动态化设计原则:降级兜底、版本兼容、安全校验、大小控制、离线可用、回滚能力。

8.8.8 组件化 vs 模块化

|----------|----------------|----------------|
| | 模块化 | 组件化 |
| 粒度 | 按业务拆 | 按功能拆 |
| 关系 | 模块之间有上下级依赖 | 组件之间平级,可独立运行 |
| 复用 | 模块通常只在一个 App 用 | 组件跨 App 复用 |
| 独立运行 | 模块一般不能单独跑 | 组件可以单独跑(开发调试用) |

组件化设计要点:组件可独立运行、可插拔、组件间通信(路由+接口+事件总线)、组件隔离(api/implementation分离)、资源隔离、组件生命周期。

8.8.9 事件驱动设计

发出事件的人不知道谁在监听,甚至不知道有没有人监听。

解耦程度:直接调用 < 观察者 < 事件驱动

设计原则

  1. 事件命名表达"发生了什么",不是"要做什么"
  2. 事件携带数据,不携带行为
  3. 事件要有生命周期------谁注册谁反注册
  4. 事件总线不是万能的------一对一强依赖用接口,链路需要可追踪用接口
  5. 防事件风暴------事件 A 触发 B,B 触发 C,C 又触发 A,循环了

8.8.10 内存防御设计

内存泄漏不是写错了一行代码,是架构设计没管好生命周期。

|------|------------------------------------|
| 防御点 | 手段 |
| 空值 | 非空断言 + 兜底值 |
| 越界 | 集合操作前检查 size |
| 类型 | JSON 反序列化 try-catch |
| 并发 | 共享变量加锁或用并发容器 |
| 生命周期 | 异步回调前判 isAdded / isFinishing |
| 配置 | 所有远端配置配本地默认值 |
| 第三方 | SDK 调用包 try-catch |
| 内存泄漏 | 对称原则:register 必有 unregister |
| OOM | 图片降采样 + LRU 上限 + onTrimMemory 主动释放 |

防御原则:对外不信任,对内信任------模块边界全加防御,模块内部正常写。

8.8.11 降级设计

复制代码
L0: 完美体验 → 全部功能正常
L1: 功能降级 → 核心功能可用,非核心关闭
L2: 内容降级 → 实时数据不可用,用缓存数据
L3: 展示降级 → 富媒体不可用,用文字/占位图
L4: 优雅失败 → 功能完全不可用,提示用户
L5: 崩溃     ← 必须尽一切努力避免

降级设计原则:

  1. 每个关键路径都要有降级方案
  2. 降级是配置驱动的,不是代码写死的
  3. 降级要可观测(埋点上报)
  4. 降级要可恢复(开关关了自动恢复)

8.8.12 缓存设计

缓存三层:请求 → 内存缓存 → 磁盘缓存 → 网络

|------|---------------------------------------------|
| 决策点 | 怎么选 |
| 缓存什么 | 只读数据、更新频率低的、请求成本高的 |
| 缓存多久 | TTL 根据业务容忍度:未读数 5 分钟,配置 1 小时 |
| 缓存大小 | 内存 LRU 上限根据业务定(图片缓存的经验值是可用内存 1/8,其他场景需单独评估) |
| 一致性 | 大部分场景最终一致就行 |
| 穿透 | 加空值缓存防穿透 |
| 击穿 | 热点 key 加锁,只让一个请求穿透 |
| 雪崩 | TTL 加随机偏移 |

客户端推荐:TTL 过期 + 读时刷新 组合------先返回旧数据,后台静默拉新。

8.8.13 启动设计

启动很大程度上取决于设计,而非编码。

|-------|-------|------|
| 类型 | 什么时候 | 可否延迟 |
| P0 必须 | 同步阻塞 | ❌ |
| P1 首帧 | 首帧渲染前 | ❌ |
| P2 首屏 | 首屏展示后 | ✅ |
| P3 后台 | 空闲时 | ✅ |

设计原则:

  1. 有向无环拓扑排序------依赖不能成环
  2. 核心路径预加载 + 非核心懒加载 + 空闲时预热
  3. 每个阶段耗时打点,启动慢了能定位
  4. 任何任务失败都不能卡住启动

8.8.14 状态机设计

当一个对象的行为取决于它当前的状态,且状态之间有严格的转换规则时。

设计原则:

  1. 状态是有限的、明确的------枚举所有状态,不允许"未知"

  2. 转换是受限的------定义合法转换,非法转换直接拒绝

  3. 副作用在转换上,不在状态上

  4. 状态机优先单调(只能往前走,除非显式重置)

8.8.15 防御式编程

假设所有外部输入都是恶意的,所有外部调用都可能失败。

过度防御:每个方法每个参数都判空,代码 50% 是防御逻辑 → 可读性炸

适度防御:模块边界防御,内部信任 → 清晰 + 安全

原则:对外不信任,对内信任。

8.8.16 通信设计

强依赖用接口,弱依赖用事件。接口调用要超时。事件不能代替接口。跨进程通信最小化。

8.8.17 通用能力设计

一次改动,后续再加需求改动量最小化。

这是厂商类型 App 最看重的设计哲学------通用能力一旦写好,新的 App、新的渠道、新的场景来了,改动量最小化。

通用能力 vs 专用能力

|---------|----------------------------------------|--------------------|
| | 专用能力 | 通用能力 |
| 写法 | if (渠道A) { ... } else if (渠道B) { ... } | config.drive(mode) |
| 新渠道 | 加 else if | 加一条配置 |
| 测试 | 每加一个渠道回归所有 | 只测新配置 |
| 维护 | 代码越改越长 | 配置表维护 |

通用能力设计四步法

复制代码
第 1 步:抽象共性
   共性抽象成接口,差异抽象成配置。
第 2 步:配置驱动
   所有差异用配置表达,不用代码表达。
   配置来源:本地默认值 → 远程下发 → 运行时环境感知
第 3 步:扩展点预留
   不确定未来会不会变的地方,留扩展点(回调/策略/插件)
   确定不变的地方,坚决不抽象
第 4 步:验证套件
   配置校验 → 兜底值 → 异常上报 → 日志追踪

通用能力设计原则

|---------------|---------------------|--------------------------------------|
| 原则 | 说明 | 反例 |
| 配置 > 代码 | 差异用配置,不用 if-else | 10 个渠道写 10 个 if |
| 接口 > 实现 | 通用能力定义接口,具体实现可替换 | 直接 new 一个实现类 |
| 策略 > 分支 | 不同行为用策略模式,不用 switch | switch(type) case A: ... case B: ... |
| 注册 > 硬编码 | 新功能通过注册接入,不修改通用代码 | 通用代码里加新模块的引用 |
| 事件 > 调用 | 通用能力通知外部用事件,不直接调用外部 | 通用模块 import 业务模块 |
| 扩展点 > 修改 | 加功能通过扩展点,不修改通用代码 | 改通用代码加新逻辑 |

过度通用的信号:配置项比业务代码还多、为了通用引入了三层抽象、新接一个 App 还是要改通用代码、通用能力的开发比业务开发还慢。出现这些信号,说明抽象过头了,该收缩边界。

8.8.18 设计思维

从需求推导到设计

需求 → 理解问题 → 识别变化点 → 选择模式 → 定义接口 → 写实现

设计评审的核心问题

|-------------------|----------------|
| 问题 | 回答不了 = 设计有问题 |
| 这个类只有一个原因变吗? | 回答不了 → 职责不单一 |
| 加一种新类型要改几个文件? | >3 → 扩展性差 |
| 这个方法可能失败吗?失败了怎么办? | 不知道 → 没有错误处理 |
| 这个方法跑在哪个线程? | 不知道 → 线程模型缺失 |
| 这个类的依赖能 mock 吗? | 不能 → 不可测试 |
| 这个模块挂了整不崩溃? | 会的 → 没有容错 |
| 这个配置写死了吗? | 写死了 → 不够灵活 |
| 三个月后你自己看得懂吗? | 看不懂 → 命名/结构有问题 |

8.8.19 音频焦点策略

这是厂商类型 App 的核心设计问题之一,也是观察者 + 状态 + 策略的综合实战。

为什么需要音频焦点

系统可能有几十种音源------音乐、导航、电话、倒车提示、系统通知......如果每种音源都自己播放,几秒内可能变化十几次,造成混音 chaos。

音频焦点策略就是让所有音源遵守同一套规则:出声前申请焦点,拿到才播放。

长焦点 vs 短焦点

|----------|------------------|---------------------|
| | 长焦点 | 短焦点 |
| 何时申请 | 需要持续播放时 | 需要短暂提示时 |
| 持续时间 | 持续持有,直到主动释放 | 播放完自动归还 |
| 释放时机 | 声音暂停后主动释放 | 播放完成后系统自动归还给上一焦点持有者 |
| 典型场景 | 音乐、视频、电台、CarPlay | 提示语、电话铃声、倒车影像、开机音 |
| 优先级 | 一般较低 | 一般较高(短焦点可抢长焦点) |

混音排查流程

复制代码
1. 两个音源同时播放了 → 混音了
2. 检查两个音源是否都申请了焦点
   ├─ 没申请焦点就播放 → 那个业务的问题
   └─ 都申请了焦点
3. 检查最后一次焦点在谁手里
   ├─ 焦点在A,但B也在播放 → B 的业务问题
   └─ 焦点只有一个,另一个没拿到焦点
4. 拿到焦点的音源正常播放,但另一个音源也在响
   ├─ 另一个业务调了播放接口 → 那个业务的问题
   └─ 另一个业务根本没调播放接口 → Audio框架的问题

音频焦点设计用到的模式

|---------|------------------------------------------------|
| 模式 | 怎么用的 |
| 观察者 | 焦点变化通知:AudioManager.OnAudioFocusChangeListener |
| 状态 | 音频焦点状态机:持有 / 丢失 / 暂时丢失 / Duck |
| 策略 | 不同音源类型申请不同焦点策略:长焦点 / 短焦点 / Duck |
| 责任链 | 焦点请求按优先级传递,高优先级拦截 |

8.8.20 Pipeline(流水线编排)

把一个复杂流程拆成多个阶段,每个阶段独立处理、独立线程池、阶段间用队列连接。

生产者消费者是"一个生产一个消费",Pipeline 是"一串阶段串起来,每个阶段既是消费者又是生产者"。

复制代码
输入 → Stage1(解析) → Queue1 → Stage2(校验) → Queue2 → Stage3(处理) → Queue3 → Stage4(输出) → 结果

三要素:Stage(阶段)、Queue(队列)、ThreadPool(线程池)

实战例子:直播推流管线

复制代码
Camera 采集 → OpenGL 渲染 → MediaCodec 编码 → 封包 → RTMP 推流
   30fps        GPU 处理       硬编耗时        快       受网络影响

每个阶段速度不一样------Camera 30fps 稳定输出,编码可能跟不上,推流看网络。如果一条线程串下来,推流卡了编码也卡了,编码卡了相机也卡了。用 Pipeline:每个阶段自己的队列兜住,推流慢了编码队列积压,背压传导回相机降帧,而不是全线崩溃。

背压:下游处理不过来,压力向上游传导。

背压不是自动的------队列满了 BlockingQueue.put() 会阻塞生产者线程导致线程饥饿,实际工程中需要显式选择背压策略:丢帧(offer 失败就丢)、等待限时(offer + timeout)、降速(通知上游降频)。不同场景选不同策略,不能无脑用阻塞队列。

什么时候用

|---------------|----------------|
| 适合 | 不适合 |
| 多阶段处理,阶段速度不一样 | 简单请求-响应,用不着 |
| 需要背压保护 | 阶段间速度差异很小,收益不大 |
| IO 密集型流水线 | 纯计算且阶段均匀的管线 |
| 音视频/直播管线 | CRUD |

8.8.21 Hook(钩子)

不修改原始代码,在运行时插入自己的逻辑或者替换原始行为。

Hook 不是 GoF 23 种设计模式之一,但它是一种广泛使用的设计思想------预留扩展点,外部插入逻辑。

从温和到暴力,Hook 分很多层

|-----|----------------|-----------------------------------------|
| 层级 | 方式 | 例子 |
| 设计层 | 继承覆写 / 回调接口 | Activity.onCreate()、OnClickListener |
| 框架层 | 拦截器 / 模板方法 | OkHttp Interceptor、RecyclerView.Adapter |
| 运行时 | 反射替换实例 | Hook AMS 跳过 Activity 注册检查 |
| 加载时 | ClassLoader 拦截 | 插件化加载未注册的类 |

实战例子1:反射替换(运行时 Hook)

复制代码
正常流程:App → IActivityManager.startActivity() → 系统检查 Manifest → 拒绝
Hook 后:  App → AMSProxy.startActivity() → 替成占坑 Activity → 系统检查通过 → 换回真实 Activity

三步走:找到目标 → 构造替代品 → 偷梁换柱

  1. 反射拿到系统的 AMS 代理对象
  2. 创建自己的代理类(持有原始对象),在 startActivity 里把 Intent 替掉
  3. 把代理塞回去,以后系统调的都是你的代理

本质上就是代理模式,只不过代理对象是运行时偷偷塞进去的。

实战例子2:ClassLoader 拦截(加载时 Hook)

场景:插件化框架加载插件的类。

复制代码
正常加载:ClassLoader.loadClass("PluginActivity") → 找不到 → 崩溃
Hook 后:  ClassLoader.loadClass("PluginActivity") → 拦截 → 用插件 ClassLoader 加载 → 成功

两种 Hook 对比

|---------|-----------|----------------|
| | 反射替换 | ClassLoader 拦截 |
| Hook 时机 | 运行时,对象创建后 | 类加载时,对象创建前 |
| Hook 粒度 | 替换一个实例/方法 | 替换整个类 |
| Hook 范围 | 精确,只改一个对象 | 全局,所有用到这个类的地方 |
| 代表应用 | 插件化跳转、保活 | 插件化加载、双开助手 |

Hook 的设计模式本质:Hook 是目的(插入逻辑改变行为),各种设计模式是实现手段。你在日常开发中写的 onCreate()、setOnClickListener()、Interceptor,都是 Hook------只是你没叫它 Hook。

相关推荐
乌恩大侠4 小时前
基站正在成为 AI 计算节点:NVIDIA Aerial 推动 RAN 架构重构
人工智能·重构·架构
码点滴5 小时前
CRI-O选型与容器运行时标准
开发语言·人工智能·架构·kubernetes·cri-o
Joy T6 小时前
【Web3】跨链 NFT 工程化实战:多环境配置与自动化状态查询机制
架构·web3·区块链·智能合约·hardhat·hardhat 3.x·跨链测试
500846 小时前
ATC 做了什么:从 ONNX 到 .om
分布式·架构·开源·wpf·开源鸿蒙
雨辰AI6 小时前
完整版信创微服务国产化架构实战:Nacos+Seata+Redis + 人大金仓(生产可落地)
数据库·redis·微服务·架构·政务
AI_大白7 小时前
DeepSeek Function Calling 接入实时行情:从工具定义到多轮查询的完整示例
后端·架构
ting94520007 小时前
Fere AI 技术深度解析:面向加密货币与预测市场的自主交易智能体架构
人工智能·架构
Yeats_Liao7 小时前
物联网接入层技术剖析(四):当epoll遇见MQTT
java·linux·服务器·网络·物联网·架构
Swift社区8 小时前
AI + 鸿蒙 App:下一代应用架构
人工智能·架构·harmonyos