ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

从Chrome OS到安卓车机:A/B双槽位OTA更新机制实战解析

从Chrome OS到安卓车机:A/B双槽位OTA更新机制实战解析 做车载安卓开发的朋友应该都清楚给车机做 OTA 更新和给手机做 OTA 完全不是一个难度等级。手机更新挂了用户顶多吐槽两句进 recovery 再刷一次就能救回来车机要是更新到一半断电、断网或者引导程序被写坏用户面对的是一块黑屏的中控导航、倒车影像、空调控制全部失效。这种现场处理成本比手机高一个量级因为车主不会自己拆机刷机也没人愿意为了一个系统版本跑两趟维修点。我前两年接手一个基于安卓系统的车机项目在更新流程上反复栽跟头最后把 Chrome OS 的更新机制完整过了一遍才算把思路彻底理顺。Chrome OS 的更新流程和安卓系统在底层是同源的核心思想是“双槽位 原子切换”永远不修改正在运行的系统而是把新版本完整写到另一个槽位分区块校验通过后再切换过去启动失败还能自动回退。这篇文章把这条流程从版本检查到重启回滚逐段拆开讲重点落在怎么把它落地到安卓系统的车载设备、手机这类场景里。适合看这篇内容的有三类人正在设计车机或智能座舱 OTA 方案的系统工程师想让安卓设备更新更可靠的 ROM 开发者以及单纯想弄明白“系统更新在后台到底做了什么”的进阶用户。我不画那些漂亮的架构图只讲实际跑通这条链路需要知道的事以及我踩过的坑。1. 车载更新为什么容易翻车三个绕不开的现实约束1.1 电力不可控更新做到一半用户可能直接熄火车机没有“充电宝”这种后备方案。手机的更新策略可以简单粗暴地要求“电量大于 50%接上充电器再更新”车机不行它依靠整车电源。用户可能在地库里待着无聊点了一下“检查更新”下载完刚进入写入阶段就关掉电源下车走了。整车断电意味着 SoC 瞬间失去供电正在写 eMMC 的进程直接被拍死。更麻烦的是车机系统里往往还挂着仪表联动、电源管理、网络唤醒等一堆域控制器逻辑。你很难用一个简单的“电量阈值”判断能不能写分区因为你不知道下一秒用户会不会启动发动机。我的做法是加一个“电源挡位 电压兜底”双条件整车电源处于 ON 挡且电瓶电压高于某个阈值比如 12V才允许进入写入阶段下载阶段则不受此限制。这样至少把“熄火瞬间写入”的概率降到最低。1.2 网络不可靠地库、隧道、高速移动下的下载体验车机的网络环境比手机差得多。手机在地库里没信号可以等回家连 Wi-Fi车机经常就是在地库或者户外停车场停着的时候才有人想起更新。再加上很多车机的蜂窝模块硬件带宽本身就有限一个几 GB 的镜像包下载几个小时是常态下载过程中进出隧道、跨基站、信号从满格变成无服务都是家常便饭。所以车机 OTA 的第一条铁律就是下载必须支持断点续传而且进度要持久化。Chrome OS 的更新引擎在这一点上做得很扎实它把已下载数据的偏移量记录在持久分区里重启后还能接着下。很多车机项目第一次做 OTA 时没有意识到这一点把下载缓存放在容易被清掉的内存文件系统里结果断一次电80% 的进度直接清零用户心态直接爆炸。1.3 救援手段缺失车机没有“刷机自由”手机用户遇到系统坏了还能自己进 recovery 双清、线刷、卡刷社区教程一抓一大把。车机呢大部分车主不知道什么是 recovery中控面板上也不存在“音量键 电源键”这种组合键想进刷机模式得靠诊断仪或者特殊指令。就算工程师能远程进入诊断模式车机本身黑屏的情况下用户连车都开不顺体验极其糟糕。这些约束叠加起来指向同一个结论车机的更新方案必须默认“更新过程中随时可能被打断”而且打断之后系统还要能自己恢复。这正是 Chrome OS 更新机制的设计前提也是我把它作为参考蓝本的原因。2. Chrome OS 更新机制的核心骨架双槽位、状态机与更新引擎2.1 双槽位设计不是“覆盖”而是“写完再切”Chrome OS 的存储里同时保留两份系统KERNEL-A/ROOT-A 和 KERNEL-B/ROOT-B这就是所谓的 A/B 双槽位。系统当前从 A 槽启动时B 槽是空闲的一次更新做的事情就是往 B 槽写一份完整的新系统而不是去改动 A 槽的任何字节。写完并校验通过之后才把“下次启动优先用 B 槽”这个标记写进引导程序能读到的位置。这个设计可以类比成搬家你不是住在正在装修的房子里而是在隔壁先装好一套样板间验收满意之后再搬过去旧房子留着作为退路。对车机这种“不允许现场救砖”的场景来说这个“退路”是刚需而不是锦上添花。2.2 更新引擎与状态机整个流程的大脑Chrome OS 里的更新动作由一个常驻后台的守护进程负责源码里通常叫 update_engine。它负责跟更新服务器通信、下载镜像、写分区、触发重启所有操作都有一个清晰的状态机空闲、检查更新、发现新版本、下载中、校验中、收尾、等待重启。每个状态都会落日志排查问题的时候第一步永远是看它停在哪个状态。这一点对移植到安卓非常重要。安卓系统从支持 A/B 无缝更新开始也把同一个更新引擎接进了自己的框架车机 ROM 里能看到这个进程再正常不过。它的状态机和 Chrome OS 基本同源你只要理解了这套状态机车机 OTA 的调试工作就成功了一半。2.3 分区布局内核、根文件系统、用户数据各自有不同的待遇Chrome OS 的分区不是所有内容都做双份。内核和根文件系统走 A/B 双槽位因为它们决定了系统能不能起来用户数据分区是单份的两个槽位共用因为它要跨版本保留。这个分工很有讲究系统代码可以随时替换用户数据不能随便丢。落到安卓车机上对应关系大概是这样内核对应 boot 分区根文件系统对应 system/vendor 这些系统分区用户数据对应 data 分区。你可以把 Chrome OS 的分区哲学理解为“把不可变的东西做成双份把可变的东西保持单份”这个原则在移植时依然成立。3. 一次完整更新的五步流从版本协商到失败回滚3.1 第一步设备“报家底”服务器决定给不给包更新开始前设备要向更新服务器汇报自己的身份和现状硬件标识哪一款车机、哪个型号的设备、当前系统版本号、所属更新通道稳定版还是测试版、固件版本等。服务器根据这些信息判断这个设备当前版本离最新版本差多远该推全量包还是增量包以及要不要限制灰度范围。这一步经常被车机团队忽略。很多项目直接把更新服务器做成“有包就发”完全没有版本协商的概念。结果就是一台长期断网的设备三个月后终于连上网服务器给它推了一个基于三个月前版本的增量包而它的当前版本比增量包的基准还老写入时校验直接失败。所以版本协商不是可选项是整个流程的地基。3.2 第二步全量包还是增量包网络决定策略更新包分两种。全量包包含目标版本的完整镜像体积大好处是不依赖设备当前版本写起来最稳妥增量包只包含新旧版本之间的差异体积可能只有全量包的十分之一但要求设备当前版本正好在基准范围内。两者的取舍在车机场景里尤其明显。我给车机项目定的策略很简单默认优先增量包前提是设备当前版本距离目标版本不超过两个版本超过两代或找不到对应基准直接转全量包。宁可下载慢一点也不让用户在“基准版本不匹配”的报错里打转。对比项全量包增量包体积大完整镜像小版本间差异适用条件任意旧版本指定旧版本范围生成复杂度低高需要基准版本失败风险低中基准不匹配则失败车机弱网场景后备方案首选3.3 第三步下载、断点续传与逐块校验下载不是简单地把一个文件拉到本地。更新引擎会把镜像包切成很多块每一块都有自己的哈希值下载过程中边下边验验不过的块直接重下。整个镜像包下载完成后还会对包签名和整体哈希再做一次校验任何一步不通过更新都不会进入写分区阶段。断点续传的实现方式是把下载进度记录到持久分区的偏好文件里分块信息和偏移量都留在那里。断电重启后引擎读到原来的进度从断点继续下而不是从头再来。我在测试里模拟过十几次“下载中拔电源”只要暂存目录和进度文件放在持久分区恢复率是百分之百反过来说只要这两个东西放错地方一次断电就前功尽弃。3.4 第四步写入非活动槽位写完才算数下载校验通过后才是真正的“安装”阶段。更新引擎把镜像内容按块写入非活动槽位比如当前从 A 槽启动就写 B 槽的内核和根文件系统分区。每一块写完之后立刻做哈希比对不一致就中止并把这个槽位标记为不可用。全部写完后还会对目标镜像做一次整体校验。这一步给车机方案提了个醒写入阶段是最怕断电的区间。虽然 A/B 双槽位能保证“写一半断电后 A 槽还能正常启动”但 B 槽会残留一个半成品下次更新必须先清掉重写。所以我在车机上加了“写入阶段禁止整车下电”的提示同时在策略上允许“下载可以断写入必须一口气完成”。3.5 第五步重启切换、尝试计数与自动回退写完并校验通过后更新引擎把“优先启动 B 槽”的标记写进引导程序的数据区域然后请求系统重启。引导程序启动 B 槽时会维护一个尝试计数每启动失败一次计数减一减到零就自动切回 A 槽只有 B 槽真正起来、关键服务跑通之后才把计数清零确定性地锁定 B 槽。这个尝试计数机制是整套回滚能否生效的关键。很多移植版在这个环节想当然认为“重启后能进桌面就算成功”于是提前把计数清掉。结果系统在进桌面前其实已经悄悄崩溃过两次计数被人为清零后下次一崩就连回退的资格都没有了。4. 把 Chrome OS 思路移植到安卓车机/手机四个必须处理的落差4.1 分区模型不同一个 kernel/rootfs一个 boot/system/vendorChrome OS 的双槽位是“内核 根文件系统”两个大分区。安卓车机/手机要复杂得多至少包含 boot、system、vendor有些还有 init_boot、odm、product而且这些分区在较新的安卓里还可能不是物理分区而是被塞进一个叫 super 的大分区里的逻辑分区。移植 A/B 时你不能简单地把“写一个 rootfs 镜像”翻译成“写一个 system 镜像”而是要把构成一套完整系统的所有目标分区都纳入写入计划。好消息是不用自己从零实现。安卓本身已经有 A/B 分区的标准布局boot_a/boot_b、system_a/system_b、vendor_a/vendor_b引导程序通过 misc 分区里的 bootloader message 记录当前槽位和尝试次数。你要做的主要是核对设备里的分区表确保这些“双份”分区真实存在而不是名字有了、实际空间共用。分区角色Chrome OS安卓 A/B 对应内核KERNEL-A/KERNEL-Bboot_a/boot_b根文件系统ROOT-A/ROOT-Bsystem_a/system_b、vendor_a/vendor_b用户数据单份 statefuldata 分区单份共享槽位标记引导程序数据区misc 分区的 bootloader message4.2 验证链不同从签名镜像到完整 verified bootChrome OS 的更新安全性靠的是完整信任链引导程序验证内核签名内核验证根文件系统的 dm-verity 哈希树每一级都验过了才放行。安卓也有对应的验证框架但在车机上很多方案只停留在“下载的镜像校验一下哈希”这个层面引导链完全没接。我的建议是既然做了双槽位就顺手把验证链也补齐。系统签名、dm-verity 表这些在车机上越来越重要。否则你辛苦保证的更新安全可能被一个没上锁的引导程序全部绕过去双槽位再稳也架不住入口是敞开的。4.3 供电与网络策略不能照抄要按使用场景重设Chrome OS 是笔记本和迷你主机场景它的电源约束是“插电 有电池”手机是“电池 偶尔连着充电器”车机是“整车电源 可能随时被拔钥匙”。三者的更新策略必须分别设置不能一套逻辑通吃。我列过一张对照表车机的几个关键门槛和手机完全不同检查更新频率车机不需要像手机那样一天查好几次设置成 6 小时一次即可电量门槛手机看电池百分比车机看电源挡位和电瓶电压网络策略车机如果装了蜂窝模块必须区分计费网络和免费 Wi-Fi避免一个镜像包把用户的流量套餐刷穿。4.4 存储空间预算双槽位意味着镜像空间翻倍A/B 最直白的代价是存储开销系统所有关键分区要做两份额外还要留出下载暂存空间。以一套中等规模的车机安卓系统为例boot 分区就算 64MBsystem 分区 4GBvendor 分区 1.5GB双份就是约 11GB再加一个全量包暂存 5GB一共 16GB 起步。很多低成本车机的存储只有 16GB 或 32GB预算一算就穿帮。应对方案有几条路优先用增量包体积能压到全量的三分之一甚至更小开启流式下载边下边写省掉完整暂存这一份存储实在不够的再考虑虚拟 A/B用快照方式减少完整副本。但虚拟 A/B 对系统分区布局和启动校验的要求更高属于进阶方案团队没有足够测试资源时不要轻易上。5. 实测中最容易翻车的三个场景与完整排查思路5.1 断电断网后更新“丢失”一看暂存路径二看进度存储位置现象下载到 80%断了一次电重启后更新引擎状态回到空闲进度清零。先别急着怀疑更新引擎有问题多半是暂存目录或进度记录放在了不该放的地方。排查链路如下第一步进系统后查看下载缓存目录挂在哪个文件系统上执行 df 确认它是持久分区还是内存文件系统。如果挂在内存文件系统重启必丢先把路径改到持久分区。第二步查看进度文件的存储目录确认它在持久分区并且断电后没有被清理脚本删掉。第三步看更新引擎日志里有没有明显的续传记录字样。有说明续传机制在工作没有说明前两步有一处没做到位。5.2 新槽位起不来但回滚失效问题多半在尝试计数处理现象B 槽写入成功重启后黑屏再重启还是黑屏本该回滚到 A 槽却完全没有动静。这类问题十有八九出在尝试计数的处理顺序上。排查链路先抓引导程序日志确认每次启动实际选中的是哪个槽位、计数还剩几次。如果引导程序每次都在试 B说明“优先启动 B”的标记没被正确清掉但计数又没在减少多见于计数被放在了易失存储里。再查系统侧负责“标记成功启动”的代码确认它是在完全启动完成之后才执行的。很多时候移植团队为图省事把这个标记提前到内核起来就执行结果系统明明没起来标记已经糊上去了。5.3 写入中报空间不足预算时少算了一个临时文件现象写入到 60% 时更新中止日志报空间不足。明明之前算过“A 槽用多少、B 槽用多少、预留多少”怎么会不够原因往往出在没算这几项下载暂存文件本身可能还在占用空间如果没开流式下载日志文件在调试模式下会飞速膨胀还有系统分区的元数据、哈希树、以及文件系统预留块。我的建议是预算公式别太乐观系统镜像双份 下载暂存 10% 到 15% 的余量余量按实际日志增长和分区元数据估。上线前写一个自动化脚本把每个分区的剩余空间打印出来和人肉预估对一遍。6. 落地车机项目时我自己用的参数配置与验收清单6.1 更新策略参数按“车机不是手机”来设以下是我在实际车机项目里验证过的参考参数可以直接抄作业但务必按你们自己的硬件调整更新检查周期6 小时一次另外在整车下电前如果检测到未完成的下载允许延长工作 5 分钟做一次断点存档。电量/电源门槛整车电源处于 ON 挡且电瓶电压高于 12V 才允许写入下载不受此限。网络策略免费 Wi-Fi 自动下载蜂窝网络只在弹窗确认后下载且单次流量上限 500MB超出自动暂停。包策略优先增量包版本差距超过两代转全量包。重试策略下载失败重试 3 次间隔 5/15/30 分钟指数退避。灰度策略1% → 5% → 20% → 50% → 100%每档停留至少一天。6.2 上线前的验收清单把断电测试做成自动化我在项目里把下面这些场景写成了自动化脚本每次打包后必须全绿才能发版下载过程中任意时刻断电重启后能从断点续传。写入过程中断电重启后旧槽位能正常启动下次更新能清掉半成品重新来。B 槽写入成功后人为破坏其内核重启应触发尝试计数递减并回退 A 槽。新槽位启动成功后尝试计数被正确清零不会在若干次重启后误回退。电量低于阈值时系统拒绝进入写入阶段但允许完成下载。空间不足时给出明确提示而不是悄悄失败。双槽位来回升级降级循环十次用户数据不丢。这些用例看着琐碎但它们才是车机 OTA 真正值钱的部分。功能开发可以靠代码评审可靠性只能靠反复打断电测出来。6.3 灰度发布与失败决策别一口气推给所有用户最后一个建议永远不要一次性把更新推给全部设备。双槽位机制再稳也架不住硬件批次差异、外设兼容性、用户使用习惯这些变量。灰度发布的时候要盯的不是“下载了多少”而是“成功启动新槽位的比例”和“回滚率”这两个指标。回滚率超过阈值立刻停发利用旧槽位还保存着的上一版本引导所有设备回退。我在项目里设过一个简单规则整体更新成功率低于 95%或者回滚率高于 3%立即冻结灰度并拉通排查。这个数字不是拍脑袋定的是先用几十台内部测试车跑了两周统计出正常波动范围之后再设的。最后再分享一点个人体会。做完这个车机 OTA 项目我最大的感悟是更新流程的好坏不在平时跑得多顺而在断电、断网、存储写坏这些最恶劣的条件下能不能兜底。双槽位、原子切换、尝试计数这套东西听起来很简单但每一环都要靠反复的断电和坏块测试才能真正让人放心。如果你们项目也在做安卓车机或手机的更新方案我建议第一步不是选服务器也不是写代码而是先把这套流程在测试台架上完整跑通再来谈灰度。另外一个小技巧把更新引擎的日志级别调到最详细并且保留最近三份日志。很多线上问题靠日志就能直接定位别等到出事那天才发现日志已经被冲掉了。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表