3、A/B系统分区详解:A/B槽位概念、bootloader与boot分区、system与vendor分区、persist与userdata分区、分区表与GPT
好,咱们今天来聊聊A/B系统分区。说实话,这个机制是Android OTA里最核心的设计之一。我当年第一次接触A/B系统时,心里就一个想法——这玩意儿怎么这么绕?但搞明白之后,你会发现它其实很巧妙。
3.1 A/B槽位概念
A/B系统,说白了就是给系统准备了两套完整的分区。一套叫slot A,一套叫slot B。你想想看,手机里同时装着两套系统,一套在跑,另一套闲着。更新的时候,直接往闲的那套里写新版本,写完了重启切换过去。这不就解决了更新失败变砖的问题吗?
我习惯把A/B槽位比作「双保险」。你在用A槽时,B槽就是你的备份。反过来也一样。系统里有个叫misc的分区,专门记录当前用的是哪个槽位。这个分区很小,但作用巨大——它决定了bootloader该从哪边启动。
核心要点:A/B系统让OTA更新变成了「后台下载 + 后台安装 + 一次重启」。用户几乎感觉不到更新过程,这才是真正的无缝体验。
我记得有一次,客户反馈说OTA更新后设备反复重启。查了半天,发现是misc分区里的槽位标记写错了。bootloader读到的槽位是A,但A槽根本没更新完。嗯,这种问题排查起来特别头疼,因为日志都来不及抓。
3.2 bootloader与boot分区
bootloader是系统启动的第一棒。它负责初始化硬件、加载boot分区里的内核。在A/B系统中,bootloader需要做一件额外的事——读取当前槽位,然后加载对应槽位的boot镜像。
boot分区里装的是Linux内核和ramdisk。每个槽位都有自己的boot分区,比如boot_a和boot_b。更新时,新内核会被写入非活跃槽位的boot分区里。
个人经验:我曾经在调试一个启动失败的问题时,发现bootloader加载的内核版本和system分区不匹配。原因是更新脚本只更新了system,忘了更新boot。从那以后,我每次做OTA都会检查boot和system的版本号是否一致。
bootloader本身不参与A/B切换,它只负责执行。真正决定切到哪个槽位的,是update_engine或者recovery。bootloader只是忠实地读取misc分区里的标记。
3.3 system与vendor分区
system分区放的是Android框架和系统应用。vendor分区放的是硬件相关的驱动和库。在A/B系统中,这两个分区都是成对出现的——system_a、system_b、vendor_a、vendor_b。
为什么要分开?说白了就是为了解耦。厂商更新驱动时,不需要动system分区。Google推送安全补丁时,也不需要动vendor分区。各管各的,互不干扰。
| 分区 | 内容 | 更新频率 |
|---|---|---|
| system | Android框架、系统应用、库 | 大版本更新、安全补丁 |
| vendor | 硬件驱动、HAL、厂商库 | 驱动升级、厂商定制 |
你想想看,如果system和vendor混在一起,每次更新都得打包成一个巨大的镜像。不仅下载慢,而且风险高——万一vendor部分出问题,整个系统都起不来。分开之后,更新可以只针对某个分区,灵活多了。
注意:system和vendor的版本必须兼容。我曾经见过一个案例,system更新到了Android 13,但vendor还是Android 12的驱动,结果WiFi直接罢工。所以OTA脚本里一定要做版本兼容性检查。
3.4 persist与userdata分区
persist分区是个特殊的存在。它不参与A/B切换,也就是说,不管你在A槽还是B槽,persist分区里的数据都是一样的。这个分区用来存一些需要持久化的校准数据,比如触摸屏校准参数、摄像头参数等。
userdata分区就更不用说了,它存的是用户数据——应用、照片、设置等。这个分区也不参与A/B切换。你想想,如果每次更新都清空用户数据,用户不骂娘才怪。
我习惯把persist和userdata比作「不动如山」的分区。无论你怎么更新系统,它们都保持原样。但这里有个坑——如果新系统对persist里的数据格式有变化,那就麻烦了。比如新驱动需要新的校准参数,但persist里存的还是旧的。这种情况只能通过OTA脚本做数据迁移。
避坑指南:我曾经遇到一个项目,persist分区里存了WiFi MAC地址。新系统改了MAC地址的存储格式,结果升级后所有设备的WiFi MAC都变成了00:00:00:00:00:00。从那以后,我坚持在OTA脚本里加一段persist数据迁移的逻辑。
3.5 分区表与GPT
分区表就是一张地图,告诉系统每个分区在存储设备上的起始位置和大小。Android现在基本都用GPT(GUID Partition Table)格式,取代了老旧的MBR。
GPT的好处很明显:支持超过2TB的磁盘、分区数量不受限制、有备份分区表防止损坏。在A/B系统中,分区表里会列出所有槽位的分区,比如:
分区表示例(GPT):
Partition 1: xbl_a (bootloader)
Partition 2: xbl_b (bootloader备份)
Partition 3: boot_a (内核)
Partition 4: boot_b (内核备份)
Partition 5: system_a (系统)
Partition 6: system_b (系统备份)
Partition 7: vendor_a (厂商)
Partition 8: vendor_b (厂商备份)
Partition 9: persist (持久数据)
Partition 10: userdata (用户数据)
...
你发现没有,每个分区都有对应的备份。这就是A/B系统的精髓——冗余。但冗余也意味着空间占用翻倍。所以做分区规划时,一定要算好存储空间。我见过一些低端设备,因为存储太小,不得不放弃A/B系统,改用传统的recovery模式更新。
个人建议:分区表设计时,给每个分区留10%-20%的余量。因为后续系统升级可能会需要更大的分区。我当年就吃过这个亏——system分区只留了刚好够用的空间,结果Android大版本更新时,镜像大小超了,不得不重新调整分区表。
GPT还有一个重要特性——分区GUID。每个分区都有一个唯一的GUID,系统通过GUID来识别分区,而不是靠分区名。这样即使分区名变了,只要GUID不变,系统就能找到正确的分区。
这张图展示了一个典型的A/B系统分区布局。左边是Slot A,右边是Slot B,下面是公共分区。更新时,新系统写入非活跃槽,然后通过misc分区切换槽位。整个过程用户无感知。
嗯,分区这块内容比较多,但核心就一句话——A/B系统通过冗余分区实现了无缝更新。搞懂了分区结构,你就掌握了OTA的一半。剩下的就是更新流程和脚本了,那些咱们后面再聊。
总结:A/B系统分区的设计思路就是「空间换时间,冗余换安全」。虽然占用了双倍存储空间,但换来了几乎零风险的OTA更新体验。对于车载系统来说,这种可靠性是必须的。