一个好的框架,不是让代码变多,而是让代码变简单。
上一篇我们拆开了 payload.bin 的二进制结构——CrAU 头、protobuf manifest、data blob,知道了 OTA 包"长什么样"。
但知道包的格式还不够。谁来下载这个包?谁来解析 manifest?谁来执行里面成百上千个 InstallOperation?谁来校验、谁来切 slot?
这些事情不是靠一个函数就能搞定的。一个 OTA 流程涉及十几个步骤,每个步骤的逻辑完全不同,但它们必须有序执行、共享数据、优雅失败。
Google 的解法是:用一套基于模板的 Action 框架,把每个步骤封装成独立的、可组合的、类型安全的 Action,然后用一个调度器按顺序串联它们。
为什么不用简单的函数调用?
最直觉的做法:
bool DoOTA() {
if (!DownloadPayload()) return false;
if (!VerifyFilesystem()) return false;
if (!RunPostinstall()) return false;
if (!SwitchSlot()) return false;
return true;
}
看起来简洁,但有几个致命问题:
❌ 简单函数调用
• 无法暂停恢复
• 无法取消
• 无法扩展
• 类型不安全
✅ Action 框架
• 随时暂停/恢复
• 优雅终止
• 开闭原则扩展
• 编译时类型检查
1. 无法暂停恢复 — 网络断了,DownloadPayload() 卡住了怎么办?函数调用是同步的,调用者线程被阻塞,什么都做不了。
2. 无法取消 — 用户点了"取消更新",但函数还在跑,怎么通知它停下来?函数没有标准的"终止"接口。
3. 无法扩展 — 想加一个步骤(比如预校验),得改 do_update() 函数本身。改一个地方,可能影响其他步骤。
4. 类型不安全 — 每个函数的输入输出是什么?靠函数签名说不清楚,全靠程序员的"记忆"。
Action 框架用一套模板机制,优雅地解决了这些问题。
Action 的继承体系
先看整体类图:
AbstractAction(纯虚基类)
+ PerformAction() : void
+ SuspendAction() : void
+ ResumeAction() : void
+ TerminateAction() : void
+ Type() : string
⬇
Action<T>(模板派生类 · CRTP)
T = 子类自身(编译时类型萃取)
InputType = ActionTraits<T>::InputType
OutputType = ActionTraits<T>::OutputType
+ GetInputObject() / GetOutputObject()
⬇
DownloadAction
下载 payload
FilesystemVerifier
验证文件系统
PostinstallRunner
收尾 + 切 slot
AbstractAction:统一接口
class AbstractAction {
public:
virtual ~AbstractAction() = default;
// 生命周期四件套
virtual void PerformAction() = 0; // 开始执行
virtual void SuspendAction() = 0; // 暂停
virtual void ResumeAction() = 0; // 恢复
virtual void TerminateAction() = 0; // 终止
// 类型信息(用于日志和调试)
virtual std::string Type() const = 0;
};
这是所有 Action 的根接口。ActionProcessor 只认这个接口,不关心具体实现。
Action<T>:模板 + CRTP
template <class T>
class Action : public AbstractAction {
public:
// CRTP:T 是子类自身
using DerivedType = T;
// 输入输出类型声明
using InputType = typename ActionTraits<T>::InputType;
using OutputType = typename ActionTraits<T>::OutputType;
// 获取输入/输出对象
InputType* GetInputObject() { return input_; }
OutputType* GetOutputObject() { return output_; }
protected:
InputType* input_ = nullptr;
OutputType* output_ = nullptr;
};
什么是 CRTP?
CRTP(Curiously Recurring Template Pattern)是 C++ 的经典技巧:基类用模板参数拿到子类的类型信息。好处是编译时类型检查——如果两个 Action 的类型不匹配,编译器直接报错,不用等到运行时才发现。
ActionTraits:类型萃取
// DownloadAction 的 traits
template <>
struct ActionTraits<DownloadAction> {
using InputType = InstallPlan;
using OutputType = InstallPlan;
};
// FilesystemVerifierAction 的 traits
template <>
struct ActionTraits<FilesystemVerifierAction> {
using InputType = InstallPlan;
using OutputType = InstallPlan;
};
ActionTraits 是一个类型萃取器。每个 Action 通过特化它来声明"我需要什么输入"和"我产出什么输出"。ActionProcessor 在编译时就能检查 Action 链的类型是否兼容。
Action 之间的数据传递
Action 之间怎么传数据?不是通过函数参数,而是通过共享 InstallPlan 对象。
InstallPlan 的字段被逐步填充
📦 InstallPlan(共享对象)
download_url · payload · partitions · signatures · verity_hash · switch_slot
① InstallPlanAction
→ download_url
→ source_slot / target_slot
② DownloadAction
→ partitions
→ signatures
③ VerifierAction
→ verity_hash
→ verified = true
④ PostinstallRunner
→ switch_slot_on_reboot
每个 Action 往 InstallPlan 里添加自己的结果,下一个 Action 读取前一个写入的字段
为什么用共享对象而不是管道?
OTA 的数据流是单向线性的:没有分支,没有并发,共享对象就够了。每个 Action 只修改 InstallPlan 中自己负责的字段,互不干扰。
ActionProcessor 的调度机制
上一篇详细讲过 ActionProcessor 的代码实现。这里从设计角度补充几个关键点。
状态机视角
委托模式
ActionProcessor 通过 ActionProcessorDelegate 接口通知上层:
class ActionProcessorDelegate {
public:
// 所有 Action 完成(成功或失败)
virtual void ProcessingDone(
ActionProcessor* processor,
ErrorCode code) = 0;
};
UpdateAttempter 实现这个接口,收到通知后更新状态、上报指标、通知用户。
好处:ActionProcessor 不知道也不关心上层是谁。它只认 Delegate 接口。你可以用同一套框架跑不同的流程——只要提供不同的 Delegate 实现。
单线程 + 消息循环
整个 update_engine 运行在单线程上。异步操作通过消息循环实现非阻塞:
// 不是阻塞等待网络数据
// 而是注册回调,数据到了由消息循环触发
http_fetcher_->SetReceivedBytesCallback(
[this](const void* data, size_t length) {
return OnReceivedBytes(data, length);
});
三种 Action 实现模式
每个 Action 都遵循相同的接口,但根据自身特性选择不同的实现模式。
① 同步 Action
代表:InstallPlanAction
特点:做完直接回调
场景:准备工作、数据填充
PerformAction()
→ 直接 ActionComplete()
② 异步 Action
代表:DownloadAction
特点:启动后等回调
场景:网络请求、I/O 操作
PerformAction()
→ BeginTransfer()
→ OnComplete() 才回调
③ 可暂停 Action
代表:FilesystemVerifier
特点:支持暂停/恢复
场景:长时间运行、资源调度
PerformAction()
SuspendAction() 暂停
ResumeAction() 恢复
错误处理:一票否决
ActionProcessor 的错误策略极其简单:任何一个 Action 失败,整条流水线立即终止。
void ActionProcessor::ActionComplete(
AbstractAction* action, ErrorCode code) {
if (code != ErrorCode::kSuccess) {
// 一票否决:清理所有剩余 Action
for (auto* remaining : actions_) {
remaining->TerminateAction();
}
actions_.clear();
delegate_->ProcessingDone(this, code);
return;
}
StartNextAction();
}
为什么不在 ActionProcessor 层面做重试?
每个 Action 内部自己处理重试:DownloadAction 网络抖动自动重试,VerifierAction 验证失败无重试意义,PostinstallRunner 脚本失败重试大概率还是失败。把重试策略下放到 Action 内部,让每个 Action 根据自身特性选择最合适的策略。
GoF 设计模式对照表
| 设计模式 |
在 Action 框架中的体现 |
| Chain of Responsibility |
ActionProcessor 按顺序调用每个 Action |
| Template Method |
AbstractAction 定义接口,子类实现具体逻辑 |
| Observer |
ActionProcessorDelegate 监听生命周期事件 |
| Command |
每个 Action 封装了一个独立的操作 |
| CRTP |
Action<T> 的模板参数是子类自身,编译时多态 |
不是刻意套模式,而是问题的本质决定了这些模式自然地出现。
框架的扩展性
想加一个新的步骤?只需要三步:
// 1. 定义新 Action
class MyNewAction : public Action<MyNewAction> {
public:
void PerformAction() override {
// 做你的事情
processor_->ActionComplete(this, ErrorCode::kSuccess);
}
};
// 2. 特化 ActionTraits
template <>
struct ActionTraits<MyNewAction> {
using InputType = InstallPlan;
using OutputType = InstallPlan;
};
// 3. 加入流水线
processor_->EnqueueAction(
std::make_unique<MyNewAction>());
开闭原则 — 对扩展开放,对修改关闭。不需要改 ActionProcessor,不需要改其他 Action。
写在第三篇结尾
Action 框架的设计教给我们一个道理:
复杂系统的优雅,不在于用了多花哨的模式,而在于对问题本质的深刻理解。
OTA 就是一系列步骤,按顺序执行,每步都可能失败。Action 框架用不到 300 行核心代码完美地建模了这个问题:模板保证类型安全,委托模式解耦上下文,单线程 + 异步 I/O 避免锁的复杂性。
下一篇,我会深入 DownloadAction 和 DeltaPerformer,看看 OTA 包是怎么从服务器安全地边下载边写入 inactive slot 的。
💡 思考题
如果要让 ActionProcessor 支持并行执行(比如同时下载和验证),框架需要做哪些改动?
📚 系列文章
第一篇:update_engine 的灵魂骨架
第二篇:Payload 格式 — 一个 OTA 包的二进制解剖
第三篇:谷歌如何用设计模式编排复杂流程 ← 你在这里
第四篇:DownloadAction 与 DeltaPerformer(即将发布)
关注我,持续分享 Android 系统底层知识 👇
我的灵魂是一片海