16、升级包兼容性检测:ro.build.fingerprint校验、vendor与system版本匹配、hwID检测

升级包能不能刷进去,不是看心情的。系统在真正开始升级之前,会做一套严格的「安检」。这套安检如果没过,升级流程直接中止,不会给你任何商量的余地。

说白了,这就是Android系统的自我保护机制。你想想看,如果随便一个包都能刷,那手机变砖的概率得多高?我早期做OTA的时候,就吃过这个亏——一个版本号没对齐的包,刷进去直接导致系统服务起不来,最后只能强刷救砖。

今天我们就来拆解这三道安检:ro.build.fingerprintvendor与system版本匹配hwID检测

16.1 ro.build.fingerprint:系统的「身份证」

先说说这个fingerprint。它是什么?它是系统编译时生成的一个唯一标识字符串。你可以把它理解成系统的身份证号。

在Android里,fingerprint的格式一般是这样的:

ro.build.fingerprint=品牌/产品/设备/Android版本/构建ID/构建类型/构建日期

举个例子:

ro.build.fingerprint=google/pixel6/pixel6:12/SP1A.210812.016/12345678:user/release-keys

这个字符串里包含了:品牌、产品名、设备名、Android版本、构建ID、构建类型、构建日期。每一个字段都是校验的依据。

升级包在编译时,会把自己的fingerprint写进包里的META-INF目录下。设备端在升级前,会拿当前系统的fingerprint和包里的做对比。

校验逻辑:

  • 如果包里的fingerprint和当前系统完全一致——说明是同一个版本,不需要升级。
  • 如果包里的fingerprint和当前系统不一致——说明版本不同,允许升级。
  • 但如果包里的fingerprint比当前系统还旧——那就不行,系统会拒绝降级。

嗯,这里要注意一点:降级保护。Android默认不允许系统降级,因为降级可能导致数据格式不兼容、安全补丁回退等问题。我在项目中遇到过,测试人员想刷回旧版本验证bug,结果死活刷不进去,就是因为这个降级保护机制在起作用。

代码层面,这个校验在RecoverySystem.java里:

// 读取当前系统的fingerprint
String currentFingerprint = SystemProperties.get("ro.build.fingerprint", "");

// 从升级包中读取期望的fingerprint
String packageFingerprint = readFromPackage("META-INF/com/android/metadata");

// 比较
if (!currentFingerprint.equals(packageFingerprint)) {
    // 检查是否是降级
    if (isDowngrade(currentFingerprint, packageFingerprint)) {
        throw new DowngradeException("不允许降级");
    }
}

这里有个小技巧:有些厂商会自定义fingerprint的格式,但核心逻辑不变——就是比字符串。我个人习惯在编译脚本里把fingerprint的生成规则写清楚,避免不同分支之间出现混淆。

16.2 vendor与system版本匹配:两个模块的「联姻」

从Android 8.0开始,Google引入了Treble架构。system分区和vendor分区被解耦了。system里放的是Android框架,vendor里放的是硬件相关的驱动和HAL。

这两个分区可以独立升级,但前提是——版本必须匹配

为什么?因为system里的服务会调用vendor里的HAL接口。如果system升级了,但vendor还是旧版本,接口可能对不上,轻则功能异常,重则系统崩溃。

我记得有一次,我们升级了system里的CameraService,但vendor里的HAL实现还是老版本。结果相机打开就闪退,log里全是接口找不到的错误。后来查了半天,才发现是版本不匹配的问题。

那怎么保证匹配呢?Android引入了VNDK(Vendor Native Development Kit)版本号。每个vendor分区都会声明自己支持的VNDK版本,system分区在升级时会检查这个版本。

具体来说,升级包里的META-INF/com/android/metadata文件会包含:

post-build=google/pixel6/pixel6:12/SP1A.210812.016/12345678:user/release-keys
pre-build=google/pixel6/pixel6:11/RP1A.201105.001/98765432:user/release-keys
post-build-incremental=12345678
pre-build-incremental=98765432
post-sdk-level=32
pre-sdk-level=31
post-security-patch-level=2022-01-05
pre-security-patch-level=2021-10-05

其中post-sdk-levelpre-sdk-level就是用来校验API级别的。如果当前系统的SDK级别是31,但升级包要求32,那说明vendor需要更新。

但vendor能不能更新呢?这取决于升级包的类型:

  • 全量包:会同时更新system和vendor,所以版本匹配问题不大。
  • 增量包:只更新system,vendor保持不变。这时候就必须保证当前vendor版本和升级包里的system版本是兼容的。

我的建议:在做增量升级时,一定要在升级脚本里显式检查vendor版本。不要依赖默认的兼容性判断。我曾经见过一个案例,vendor版本号没变,但内部HAL接口改了,结果增量升级后一堆功能异常。

16.3 hwID检测:硬件的「指纹」

hwID,全称是Hardware Identifier。它用来标识设备的硬件配置。比如屏幕分辨率、传感器型号、内存大小、存储芯片型号等等。

为什么要检测hwID?因为同一个型号的手机,可能因为供应链不同,用了不同的硬件。比如屏幕,可能这批是三星的,下批是京东方的。如果升级包里的驱动只支持三星屏,刷到京东方屏上,那屏幕就直接黑掉了。

hwID检测的逻辑是这样的:

  1. 设备在启动时,会读取硬件信息,生成一个hwID。
  2. 这个hwID会保存在/proc/device-tree/或者/sys/firmware/devicetree/里。
  3. 升级包在编译时,会指定支持的hwID列表。
  4. 升级前,系统对比当前hwID是否在包的支持列表里。

代码层面,这个检测通常在updater二进制里实现:

// 读取当前设备的hwID
std::string current_hwid = read_file("/proc/device-tree/hwid");

// 读取升级包支持的hwID列表
std::vector<std::string> supported_hwids = read_list_from_package("hwid_list.txt");

// 检查
bool hwid_match = false;
for (const auto& hwid : supported_hwids) {
    if (current_hwid == hwid) {
        hwid_match = true;
        break;
    }
}

if (!hwid_match) {
    error("hwID不匹配,当前设备不支持此升级包");
}

这里有个坑:hwID的生成方式各厂商不同。有的用设备树里的compatible字段,有的用自定义的硬件版本号。我建议在项目初期就把hwID的生成规则定下来,并且写进文档里。不然到了后期,不同批次的产品hwID对不上,升级包就得打好几个版本。

注意事项:hwID检测不能太死板。如果某个硬件只是小改动(比如换了同型号不同批次的传感器),hwID不应该变。否则每次换硬件都要重新发版,维护成本太高。

16.4 三道安检的协作流程

这三道安检不是独立执行的,它们有先后顺序。我画了一张图,帮你理清这个流程:

升级包兼容性检测流程 开始升级 第1关:ro.build.fingerprint校验 检查版本是否一致、是否降级 通过? 升级中止 第2关:vendor/system版本匹配 检查VNDK版本、SDK级别 第3关:hwID检测 检查硬件配置是否匹配 升级中止 升级中止 全部通过 → 开始升级

流程很清晰:先过fingerprint,再过vendor/system匹配,最后过hwID。任何一关没过,升级直接中止。

这里有个细节:增量包和全量包的检测策略不同。全量包因为要刷写所有分区,所以fingerprint校验可以放宽一些——只要不是降级就行。但增量包必须严格校验,因为增量包只更新部分文件,对版本依赖非常强。

16.5 实战中的避坑指南

做了这么多年OTA,我总结了几条经验,分享给你:

  • fingerprint的生成要自动化:不要手动拼接,容易出错。用编译脚本自动生成,确保每次构建都有唯一标识。
  • vendor版本号要语义化:比如vendor.vndk.version=30.1.2,主版本号、次版本号、修订号分开。这样兼容性判断更灵活。
  • hwID要预留扩展位:比如用32位整数表示,高16位表示硬件平台,低16位表示具体配置。这样换硬件时只需要改低16位。
  • 测试要覆盖所有硬件变体:同一个型号的不同批次,hwID可能不同。测试时一定要拿所有变体都测一遍。

一个小技巧:在升级包的metadata里,可以加一个自定义字段,比如required-hwid=0x1234。这样升级脚本可以直接读取这个字段做校验,不用再去解析其他文件。我自己的项目里就是这么做的,维护起来方便很多。

好了,这一章的内容就到这里。兼容性检测是OTA升级的第一道防线,做扎实了,后面的升级流程才能顺利推进。


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