7. update_engine源码分析:Action与ActionProcessor设计模式、Omaha协议与Payload生成、动态分区与快照机制

各位好,今天我们来啃一块硬骨头——update_engine的源码。说实话,我第一次看这个模块时也被绕晕过。但一旦理清了它的设计思路,你会发现Google这帮人确实有两下子。

update_engine是整个Android OTA的核心引擎。它负责下载、校验、写入更新包。我当年在做一个定制ROM项目时,就因为没搞懂ActionProcessor的调度逻辑,导致更新流程卡死——嗯,从那以后我老老实实把源码啃了一遍。

7.1 Action与ActionProcessor设计模式

update_engine的架构核心是ActionActionProcessor。说白了,这就是一个责任链模式的变体。每个Action负责一个独立步骤,ActionProcessor负责串联它们。

核心思想:每个Action只做一件事,做完就通知Processor,Processor再启动下一个Action。

我们来看一下典型的Action链:

// 典型的OTA更新Action链
InstallPlanAction → DownloadAction → FilesystemVerifierAction → PostinstallRunnerAction

// 每个Action继承自Action<T>模板类
class DownloadAction : public Action<DownloadAction> {
 public:
  void PerformAction() override {
    // 执行下载逻辑
    // 完成后调用 processor_->ActionComplete(this, Error::None());
  }
  
  void TerminateAction() override {
    // 清理资源
  }
};

我个人习惯把ActionProcessor看作一个导演,Action就是演员。导演喊"Action!",演员开始演,演完喊"Cut!",导演再喊下一个演员上场。

避坑指南:我曾经在自定义Action时忘记调用ActionComplete(),结果整个更新流程卡死在那里。记住:每个Action执行完毕后,必须显式通知Processor,否则链会断掉。

ActionProcessor的核心代码其实很简洁:

void ActionProcessor::StartProcessing() {
  if (actions_.empty()) {
    CompleteProcessing();  // 没有Action了,结束
    return;
  }
  
  // 取出第一个Action并执行
  current_action_ = actions_.front();
  actions_.pop_front();
  current_action_->PerformAction();
}

void ActionProcessor::ActionComplete(Action<T>* action, Error error) {
  if (error != Error::None()) {
    // 出错处理
    return;
  }
  // 继续下一个Action
  StartProcessing();
}

你想想看,这种设计有什么好处?解耦。每个Action不需要知道前后是谁,只需要知道自己该干什么。我在项目中经常利用这个特性,在测试时替换某个Action为Mock版本,非常方便。

7.2 Omaha协议与Payload生成

Omaha协议是Google内部使用的更新协议,Chrome和Android都在用。说白了,它定义了一套客户端-服务器的交互规范。

协议的核心流程是这样的:

  1. 客户端发送请求(包含当前版本、硬件信息等)
  2. 服务器返回响应(包含更新URL、版本号、哈希值等)
  3. 客户端根据响应下载Payload

Payload生成是另一个关键环节。update_engine使用delta生成器来创建增量包。我记得第一次接触delta update时,觉得这玩意儿太神奇了——只传输变化的部分,能省下80%的流量。

// Payload生成的核心逻辑(简化版)
class DeltaArchiveGenerator {
 public:
  bool GeneratePayload(const std::string& old_image,
                       const std::string& new_image,
                       const std::string& output_path) {
    // 1. 对比新旧镜像,找出差异块
    std::vector<BlockDiff> diffs = ComputeBlockDiffs(old_image, new_image);
    
    // 2. 生成操作列表
    std::vector<InstallOperation> operations;
    for (const auto& diff : diffs) {
      InstallOperation op;
      op.set_type(diff.type);  // REPLACE, MOVE, SOURCE_COPY等
      op.set_src_offset(diff.src_offset);
      op.set_dst_offset(diff.dst_offset);
      op.set_data(diff.data);
      operations.push_back(op);
    }
    
    // 3. 写入Payload文件
    return WritePayload(operations, output_path);
  }
};

注意:Payload的校验非常重要。每个操作都有对应的哈希值,客户端会逐条校验。我曾经遇到过因为Payload签名错误导致整批设备变砖的情况——从那以后,我每次生成Payload都会手动校验签名。

Omaha协议中还有一个重要概念——安装计划(InstallPlan)。它描述了整个更新过程需要执行的操作序列:

字段 说明 示例值
download_url Payload下载地址 https://update.example.com/payload.bin
hash_checksum Payload的SHA256哈希 a1b2c3d4e5f6...
metadata_size 元数据大小 1024
is_delta 是否为增量更新 true

7.3 动态分区与快照机制

动态分区是Android 10引入的重大变化。它允许系统在运行时调整分区大小,不再需要固定的分区表。说白了,就是按需分配

快照机制则是动态分区的安全保障。更新前先拍个快照,如果更新失败,可以回滚到快照状态。

我们来看一下动态分区的核心数据结构:

// 动态分区布局
struct LpMetadata {
  struct LpMetadataHeader header;  // 元数据头
  struct LpMetadataPartition partitions[MAX_PARTITIONS];  // 分区列表
  struct LpMetadataExtent extents[MAX_EXTENTS];  // 物理区域映射
  struct LpMetadataSlot slots[MAX_SLOTS];  // 槽位信息
};

// 分区描述
struct LpMetadataPartition {
  char name[36];           // 分区名,如"system_a"
  uint32_t attributes;     // 属性标志
  uint32_t first_extent_index;  // 第一个extent的索引
  uint32_t num_extents;    // extent数量
};

快照机制的核心是Copy-on-Write(COW)。更新时,系统会创建一个COW文件,记录所有被修改的块。如果更新成功,COW文件被合并;如果失败,COW文件被丢弃,系统恢复到更新前的状态。

关键点:快照机制保证了更新的原子性。要么全部成功,要么全部回滚。不会出现半更新状态。

我画了一张图来展示整个流程:

动态分区与快照机制流程图 更新前状态 创建COW快照 执行更新操作 成功 失败 合并COW到分区 丢弃COW,回滚快照 更新完成/回滚完成 说明:COW(Copy-on-Write)文件记录所有修改的块,保证更新原子性 绿色路径:更新成功,COW合并;红色路径:更新失败,COW丢弃

快照机制在代码中的实现:

// 快照管理器核心接口
class SnapshotManager {
 public:
  // 创建快照
  bool CreateSnapshot(const std::string& partition_name) {
    // 1. 创建COW文件
    // 2. 记录当前分区状态
    // 3. 返回快照ID
  }
  
  // 回滚到快照
  bool RollbackSnapshot(const std::string& snapshot_id) {
    // 1. 读取COW文件
    // 2. 恢复原始数据
    // 3. 删除COW文件
  }
  
  // 合并快照
  bool MergeSnapshot(const std::string& snapshot_id) {
    // 1. 将COW中的修改写入分区
    // 2. 删除COW文件
    // 3. 更新元数据
  }
};

实战经验:我在调试动态分区时,发现一个常见问题——COW文件空间不足。如果更新包太大,COW文件可能撑爆userdata分区。建议在生成Payload时,预留足够的COW空间,一般建议是更新包大小的1.5倍。

最后,我想强调一点:动态分区+快照机制是Android OTA从"蛮力更新"走向"智能更新"的关键。它让系统更新变得像数据库事务一样可靠——要么全部成功,要么回滚到原点。

嗯,今天的内容就到这里。这些知识点我在实际项目中反复验证过,希望能帮到正在研究update_engine的你。


公众号:蓝海资料掘金营,微信deep3321