简介:这是一份面向 Android 系统开发者的 A/B 分区 OTA 升级应用层调用 UpdateEngine 的实战源码,适合已具备一定系统开发基础、正在折腾 OTA 升级却卡在参数与权限问题上的工程师参考。资源包共 115 个文件,约 16.39MB,以 64 个 xml 配置、6 个 java 源码、8 个 bin 编译产物及若干 gradle、properties、jar 等构建与依赖文件为主,结构完整,可直接对照工程理解调用链路。作者结合系统代码封装了解析类,只需传入 update.zip 包即可完成解析与升级,并在代码中备注了容易踩坑的关键点,对定位莫名错误、排查权限与参数问题很有帮助。目前已有 7993 人学习下载,适合需要快速跑通 A/B 分区 OTA 升级流程、少走弯路的开发者借鉴。
1. 从一次“升级完重启还是老版本”说起:这套 A/B 分区 OTA 应用层源码到底解决什么
如果你做过 Android 系统升级,大概率遇到过这种场景:后台推了 OTA 包,设备也提示“重启安装”,结果重启完一看,版本号纹丝不动,ro.build.version.incremental还是旧的。排查半天发现不是包的问题,而是应用层根本没把升级请求正确交给UpdateEngine——要么 payload 路径传错,要么没监听onStatusUpdate回调,要么 slot 切换后没做markBootSuccessful。这套Android A/B 分区 OTA 系统升级应用层调用 UpdateEngine Apk 源码,解决的就是“系统底层有 A/B 升级能力,但应用层不会调”的断层问题。
它面向的是做车机、TV 盒子、定制平板、工业终端的 Android 系统开发与集成人员。A/B 分区(也叫无缝更新)本身是 Android 7.0 之后引入的机制:系统有两套 slot(a/b),升级时把新镜像写到非当前 slot,重启切过去,失败还能回滚。但UpdateEngine是系统服务,普通 App 拿不到权限,必须系统签名或预置到/system/priv-app。这份源码的价值在于:它把applyPayload、进度回调、错误码处理、slot 校验、重启触发这一整条链路用可编译的 Apk 工程串起来了,你拿到后改包名、改签名、改 payload URL 就能接进自己的项目。下面按“能跑起来 → 能看懂 → 能改对 → 不踩坑”的顺序拆。
2. UpdateEngine 调用链路拆解:从 bindService 到 applyPayload 的完整参数
2.1 为什么必须用系统签名和 priv-app 预置
UpdateEngine的 AIDL 接口定义在android.os.UpdateEngine,它对应的服务是android.os.UpdateEngineService。这个服务在frameworks/base里注册,权限是android.permission.RECOVERY或系统级signature|privileged。普通第三方 App 直接bindService会返回null或者抛SecurityException。常见做法有两种:一是把 Apk 用平台签名(platform.x509.pem/platform.pk8)重签后推到/system/priv-app/YourOta/;二是在AndroidManifest.xml里声明android:sharedUserId="android.uid.system",但这种方式在 Android 10 之后对UpdateEngine的访问限制更严,推荐第一种。
源码工程里通常包含一个Android.mk或Android.bp,用来把 Apk 编进系统镜像。如果你只是调试,可以先用adb root && adb remount,把编译好的 Apk push 到/system/priv-app/,然后adb reboot。注意:remount在 Android 10+ 的动态分区上可能失败,需要先adb disable-verity再adb reboot,这是第一个容易翻车的地方。
2.2 bindService 与 UpdateEngineCallback 的注册顺序
调用UpdateEngine的第一步是绑定服务。源码里一般会封装一个UpdateEngineManager类,核心代码如下:
// 绑定 UpdateEngine 服务,必须在 applyPayload 之前完成 Intent intent = new Intent(); intent.setClassName("android", "android.os.UpdateEngineService"); boolean bound = bindService(intent, mConnection, Context.BIND_AUTO_CREATE); private ServiceConnection mConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { // 拿到 UpdateEngine 的 AIDL 代理对象 mUpdateEngine = UpdateEngine.Stub.asInterface(service); try { // 注册回调,必须在 applyPayload 之前调用 mUpdateEngine.bind(mCallback); } catch (RemoteException e) { Log.e(TAG, "bind callback failed", e); } } @Override public void onServiceDisconnected(ComponentName name) { mUpdateEngine = null; } };逻辑说明:bindService是异步的,onServiceConnected回调触发后才能拿到mUpdateEngine。参数BIND_AUTO_CREATE保证服务不存在时自动创建。mUpdateEngine.bind(mCallback)这一步注册UpdateEngineCallback,用来接收进度和状态。顺序不能反:如果先applyPayload再bind,你会漏掉最初的IDLE和CHECKING_FOR_UPDATE状态,进度条会从中间开始跳。
UpdateEngineCallback需要实现两个方法:
private UpdateEngineCallback mCallback = new UpdateEngineCallback() { @Override public void onStatusUpdate(int status, float percent) { // status 取值见 UpdateEngine.UpdateStatusConstants // percent 是 0.0~1.0 的浮点数,不是 0~100 runOnUiThread(() -> mProgressBar.setProgress((int)(percent * 100))); } @Override public void onPayloadApplicationComplete(int errorCode) { // errorCode == 0 表示成功,其他值见 UpdateEngine.ErrorCodeConstants if (errorCode == UpdateEngine.ErrorCodeConstants.SUCCESS) { // 升级写入完成,可以提示用户重启 } } };参数说明:onStatusUpdate的status常见值有IDLE(0)、CHECKING_FOR_UPDATE(1)、DOWNLOADING(2)、VERIFYING(3)、FINALIZING(4)、UPDATED_NEED_REBOOT(5)、REPORTING_ERROR_EVENT(6)、ATTEMPTING_ROLLBACK(7)。percent是 float,直接乘 100 取整即可。onPayloadApplicationComplete的errorCode为 0 才是成功,1是ERROR,2是FILESYSTEM_VERIFIER_ERROR,3是NOT_ENOUGH_SPACE,4是PAYLOAD_MISMATCHED_TYPE,5是PAYLOAD_VERIFICATION_FAILED。这些错误码在源码里通常有对应的中文提示,改的时候别只改 UI 不改判断。
2.3 applyPayload 的四个关键参数怎么填
applyPayload是真正触发升级的方法,签名如下:
mUpdateEngine.applyPayload( payloadUrl, // payload 的 URL,支持 file:// 和 http(s):// offset, // payload 在文件中的偏移,通常为 0 size, // payload 的字节数,必须准确 headers // 传给 payload 的 HTTP 头,String[] 形式 );源码里一般会从Intent或本地配置读取这些值。常见做法是:OTA 包下载到/data/ota_package/update.zip,然后解压出payload.bin,用file:///data/ota_package/payload.bin作为payloadUrl。offset填 0,size用new File(path).length()获取。headers如果走本地文件,传new String[0]即可;如果走 HTTP,需要传{"Authorization: Bearer xxx"}这类头。
这里有个血泪经验:size必须和实际文件大小完全一致,差一个字节都会报PAYLOAD_VERIFICATION_FAILED。另外,payload.bin的路径不能放在/sdcard,因为UpdateEngine运行在系统进程,没有外部存储权限,必须放在/data下且权限为0644或0600。
3. 把源码跑起来:编译、签名、预置、触发升级的完整步骤
3.1 工程结构与关键文件说明
拿到源码包后,先看目录。典型的工程结构如下:
| 文件/目录 | 作用 |
|---|---|
AndroidManifest.xml | 声明sharedUserId、权限、Service 和 Activity |
src/com/example/ota/UpdateEngineManager.java | 封装 bind、applyPayload、回调 |
src/com/example/ota/MainActivity.java | UI 入口,进度条和按钮 |
res/layout/activity_main.xml | 布局,含 ProgressBar 和 TextView |
Android.mk或build.gradle | 编译配置,决定是否编进系统 |
libs/ | 可能包含update_engine.jar或 AIDL 生成的类 |
如果源码里没有update_engine.jar,你需要从 Android 源码的out/target/common/obj/JAVA_LIBRARIES/下找,或者直接用adb shell dumpsys package确认系统是否支持。常见做法是:在Android.bp里加static_libs: ["update_engine"],但不同 Android 版本路径不同,Android 11 之后UpdateEngine的 AIDL 有变化,需要对应版本的framework.jar。
3.2 用 Android Studio 编译并手动签名
如果你不想编进系统镜像,可以先用 Android Studio 编译出未签名 Apk,再用平台签名工具重签。步骤如下:
# 1. 用 Android Studio 或 gradle 编译出 app-debug.apk ./gradlew assembleDebug # 2. 用平台签名工具重签(需要 platform.x509.pem 和 platform.pk8) # 这两个文件在 Android 源码的 build/target/product/security/ 下 java -jar signapk.jar platform.x509.pem platform.pk8 app-debug.apk app-signed.apk # 3. 推送到系统 priv-app 目录 adb root adb remount adb push app-signed.apk /system/priv-app/YourOta/YourOta.apk adb shell chmod 644 /system/priv-app/YourOta/YourOta.apk adb reboot逻辑说明:signapk.jar是 Android 源码里自带的签名工具,用平台密钥重签后,Apk 就拥有了platform签名权限。adb remount在 Android 10+ 上可能提示remount failed,这时需要先执行adb disable-verity,重启后再adb remount。推送后chmod 644保证系统能读取。重启后可以用adb shell pm list packages | grep your.ota确认包已安装。
参数说明:/system/priv-app/下的 Apk 会被授予privileged权限,但还需要在AndroidManifest.xml里显式声明android.permission.RECOVERY。如果源码里没声明,加上:
<uses-permission android:name="android.permission.RECOVERY" /> <uses-permission android:name="android.permission.REBOOT" />3.3 触发升级的三种方式与验证
源码里通常提供三种触发方式:按钮点击、Intent广播、adb命令。最直接的是按钮点击,调用UpdateEngineManager.startUpdate(payloadPath)。验证是否成功,可以看logcat:
adb logcat -s UpdateEngineService UpdateEngineManager正常流程会打印onStatusUpdate: status=2, percent=0.0到percent=1.0,然后onPayloadApplicationComplete: errorCode=0。如果卡在status=1不动,说明 payload 校验失败,检查size和文件路径。如果直接报errorCode=5,说明 payload 签名和当前系统不匹配,需要用同一套密钥签名的 OTA 包。
另一个验证点是 slot 状态:
adb shell update_engine_client --status输出里会显示current_slot和boot_slot。升级写入完成后,boot_slot会变成另一个 slot,但current_slot不变,直到重启。重启后current_slot切换,如果新系统启动失败,bootloader 会自动回滚到旧 slot。这个机制是 A/B 分区的核心,源码里一般会有一个markBootSuccessful的调用,但注意:这个调用需要在系统启动完成后由SystemServer触发,应用层不要手动调,否则会破坏回滚逻辑。
4. 避坑与排查:五个让升级卡住或回滚的典型问题
4.1 现象:applyPayload 返回成功但进度条不动
原因:bind(mCallback)在applyPayload之后才调用,导致回调没注册上。或者mUpdateEngine为null,applyPayload抛了RemoteException但被 catch 后没打印。
解决:把bind放到onServiceConnected里,applyPayload放到bind成功后的回调里。可以在UpdateEngineCallback的onStatusUpdate里加一行Log.d(TAG, "status=" + status + " percent=" + percent),确认回调是否触发。
4.2 现象:报错 errorCode=3(NOT_ENOUGH_SPACE)
原因:/data分区剩余空间不足。A/B 升级需要把整个新系统镜像写到/data下的payload临时目录,通常需要 2~3GB 空闲空间。
解决:升级前检查StatFs,如果剩余空间小于 payload 大小的 2 倍,先提示用户清理。源码里一般有checkSpace()方法,改的时候把阈值调对。
4.3 现象:重启后版本没变,但 update_engine_client 显示 boot_slot 已切换
原因:新 slot 的系统镜像有问题,bootloader 回滚了。或者markBootSuccessful没被调用,系统认为新 slot 不稳定。
解决:看adb shell dmesg | grep -i slot和adb shell logcat -b all | grep -i boot。如果是镜像问题,重新生成 OTA 包;如果是markBootSuccessful问题,检查SystemServer里的UpdateEngineService是否正常。
4.4 现象:payload 路径用 /sdcard 时报权限拒绝
原因:UpdateEngine运行在system_server进程,没有READ_EXTERNAL_STORAGE权限,无法读取/sdcard。
解决:把 payload 放到/data/ota_package/下,用file:///data/ota_package/payload.bin。如果必须从外部存储读,先用FileInputStream拷贝到/data下再调applyPayload。
4.5 现象:Android 10 以上 remount 失败,push 不进去
原因:动态分区和verity校验导致/system只读。
解决:adb disable-verity→adb reboot→adb remount。如果还不行,用adb shell mount -o rw,remount /手动挂载。注意:disable-verity会清除用户数据,调试机无所谓,量产机别这么干。
5. 进阶:把进度回调做成状态机,并验证 slot 切换的可靠性
源码里最常见的写法是直接在onStatusUpdate里更新 UI,但这样在status跳变时会闪。我一般会加一个状态机,把status映射成枚举,只有状态变化时才更新 UI,percent单独走进度条。核心代码如下:
private int mLastStatus = -1; @Override public void onStatusUpdate(int status, float percent) { if (status != mLastStatus) { mLastStatus = status; runOnUiThread(() -> mStatusText.setText(mapStatus(status))); } runOnUiThread(() -> mProgressBar.setProgress((int)(percent * 100))); } private String mapStatus(int status) { switch (status) { case UpdateEngine.UpdateStatusConstants.IDLE: return "空闲"; case UpdateEngine.UpdateStatusConstants.CHECKING_FOR_UPDATE: return "检查更新"; case UpdateEngine.UpdateStatusConstants.DOWNLOADING: return "下载中"; case UpdateEngine.UpdateStatusConstants.VERIFYING: return "校验中"; case UpdateEngine.UpdateStatusConstants.FINALIZING: return "写入中"; case UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT: return "等待重启"; case UpdateEngine.UpdateStatusConstants.REPORTING_ERROR_EVENT: return "上报错误"; case UpdateEngine.UpdateStatusConstants.ATTEMPTING_ROLLBACK: return "回滚中"; default: return "未知"; } }参数说明:mLastStatus初始为 -1,保证第一次回调一定更新。mapStatus里的常量来自UpdateEngine.UpdateStatusConstants,不同 Android 版本可能多一个DISABLED,用default兜底。
验证 slot 切换可靠性,可以写一个脚本循环触发升级并重启:
for i in 1 2 3; do adb shell am start -n com.example.ota/.MainActivity --es payload /data/ota_package/payload.bin sleep 30 adb reboot sleep 60 adb shell getprop ro.boot.slot_suffix done逻辑说明:每次升级后重启,打印当前 slot 后缀。正常情况下_a和_b会交替出现。如果连续两次都是同一个 slot,说明升级没生效或回滚了。这个脚本我一般在量产前跑 10 次,确认没有偶发回滚。
最后说个习惯:从那以后我每次接 OTA 项目,都强制先跑一遍update_engine_client --status和logcat -s UpdateEngineService,确认底层服务正常再动应用层代码。希望帮到你。
本文还有配套的精品资源,点击获取