3、Recovery模式启动流程:从bootloader到Recovery、init进程与Recovery、recovery.cpp入口分析
这一章我们聊聊Recovery模式到底是怎么启动起来的。很多做ROM开发的兄弟,可能天天刷Recovery,但真要说清楚它从按下电源键到界面显示之间发生了什么,能讲明白的人不多。我当年刚接触这块时也是一头雾水,后来啃了几个月源码才理清楚。今天我把这些经验整理出来,希望能帮你少走弯路。
3.1 从bootloader到Recovery:那条看不见的接力棒
手机开机,第一个跑起来的不是Linux内核,而是bootloader。嗯,这里要注意,bootloader其实是个很小的程序,它负责初始化硬件、加载内核。但Recovery模式怎么被选中的?
说白了,靠的是「启动参数」这根接力棒。
bootloader会读取misc分区,这个分区里存着一个小结构体——bootloader_message。里面有个字段叫command,如果它的值是"boot-recovery",bootloader就知道:哦,用户要进Recovery模式。
我在项目中遇到过一个问题:某款手机刷了第三方Recovery后,每次重启都会自动进Recovery。查了半天,发现是misc分区里的command字段没被清空。所以bootloader每次启动都以为用户要进Recovery。这个坑,后来我专门写了个脚本在OTA完成后清理这个字段。
bootloader拿到指令后,会加载Recovery内核(通常是boot分区里的kernel),然后传递一个关键参数给内核:androidboot.mode=recovery。内核启动后,init进程会读取这个参数,决定接下来跑什么。
关键点:Recovery模式和正常系统共用同一个内核,区别在于init进程启动后的服务列表不同。Recovery模式下的init.rc会启动recovery服务,而不是zygote。
3.2 init进程与Recovery:谁在指挥谁?
内核启动完成后,第一个用户态进程就是init(PID=1)。init进程会解析init.rc文件,但Recovery模式下,它读的是/init.rc还是/init.recovery.rc?
答案是:两者都会读。但init会先检查内核启动参数中的androidboot.mode。如果值是recovery,它就会额外加载/init.recovery.rc。这个文件里定义了Recovery模式特有的服务。
我们来看看典型的init.recovery.rc里有什么:
on early-init
# 设置Recovery模式特有的权限
chmod 0777 /cache/recovery
chmod 0777 /data/misc
on init
# 启动Recovery服务
service recovery /system/bin/recovery
class main
seclabel u:r:recovery:s0
oneshot
注意那个oneshot标志。这意味着Recovery服务只运行一次,退出后不会重启。为什么?因为Recovery做完该做的事(比如刷机、清数据)后,就应该重启手机,而不是重新进入Recovery。
我个人习惯在调试Recovery时,把这个oneshot改成disabled,然后手动启动服务。这样如果Recovery崩溃了,不会直接重启,而是可以看log。当然,这只是调试技巧,正式发布版本一定要改回来。
调试小技巧:如果你在开发Recovery,可以在init.recovery.rc里加一句setprop ro.debuggable 1,这样就能用adb连接Recovery模式下的设备了。不过要注意,某些厂商的bootloader会锁adb。
3.3 recovery.cpp入口分析:一切从这里开始
init进程启动recovery服务后,最终会执行/system/bin/recovery这个可执行文件。它的入口在bootable/recovery/recovery.cpp里的main()函数。
这个main函数,说实话,写得不算优雅。但胜在逻辑清晰。我们一步步拆解:
int main(int argc, char **argv) {
// 1. 解析启动参数
// 2. 初始化UI系统
// 3. 挂载必要的分区
// 4. 读取命令文件
// 5. 执行具体操作
// 6. 清理并重启
}
第一步,解析参数。Recovery支持很多启动参数,比如--wipe_data、--update_package等。这些参数从哪里来?还记得前面说的misc分区吗?bootloader会把command字段的内容传给内核,内核再传给init,init最后通过/cache/recovery/command文件传递给recovery进程。
我曾经踩过一个坑:某次OTA升级后,Recovery总是报"command file not found"。查了半天,发现是/cache分区没挂载。Recovery在读取command文件前,必须先挂载/cache分区。这个顺序问题,在早期Android版本里是个常见bug。
第二步,初始化UI。Recovery的UI系统是自己实现的,不依赖Android的SurfaceFlinger。它直接操作framebuffer,用minui库绘制界面。所以Recovery模式下,你看到的那些菜单、进度条,都是纯C++代码画出来的。
第三步,挂载分区。Recovery需要挂载/system、/data、/cache等分区。但注意,挂载方式有讲究:
| 分区 | 挂载方式 | 说明 |
|---|---|---|
| /system | 只读 | 刷机时可能需要重新挂载为读写 |
| /data | 视情况而定 | wipe data时需要格式化,不能挂载 |
| /cache | 读写 | 用于存储升级包和日志 |
第四步,读取命令。Recovery会从/cache/recovery/command文件里读取要执行的操作。这个文件的内容格式很简单,每行一个参数。比如:
--update_package=/sdcard/update.zip
--wipe_cache
第五步,执行操作。这是核心逻辑。Recovery会根据命令调用对应的函数:
--update_package:调用install_package(),执行OTA升级--wipe_data:调用wipe_data(),格式化/data分区--wipe_cache:调用wipe_cache(),清空/cache分区- 如果没有命令:显示Recovery菜单,等待用户操作
第六步,清理并重启。操作完成后,Recovery会清空/cache/recovery/command文件,然后调用reboot()重启系统。如果不清理command文件,下次启动又会进Recovery——这就是前面说的那个坑。
注意事项:Recovery模式下不要随意挂载/data分区。如果你在wipe data之前挂载了/data,会导致无法格式化,因为分区正在使用中。我见过有开发者在这个问题上栽跟头,刷机脚本写错了顺序,结果用户手机变砖。
3.4 启动流程总结:一张图看懂
说了这么多,我们来梳理一下完整的启动链路:
- 按下电源键 → bootloader启动
- bootloader读取misc分区,发现command="boot-recovery"
- bootloader加载内核,传递参数androidboot.mode=recovery
- 内核启动,init进程解析init.rc和init.recovery.rc
- init启动recovery服务,执行/system/bin/recovery
- recovery.cpp的main()函数执行:挂载分区 → 读取命令 → 执行操作 → 重启
这个流程,从Android 4.0到Android 13基本没变过。核心逻辑非常稳定。你想想看,一个系统组件能保持十几年的兼容性,说明它的设计确实经得起考验。
我个人建议,如果你想深入理解Recovery,最好的办法是自己在源码里加log。在recovery.cpp的main函数里,每个关键步骤前加一句LOGI("step X: xxx"),然后编译烧录,看logcat输出。这样比看任何文档都直观。
下一章,我们会深入Recovery的UI系统,看看那些菜单和进度条是怎么画出来的。到时候我会分享一个我踩过的坑——关于屏幕旋转导致UI错乱的问题,保证让你有所收获。