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 启动流程总结:一张图看懂

说了这么多,我们来梳理一下完整的启动链路:

  1. 按下电源键 → bootloader启动
  2. bootloader读取misc分区,发现command="boot-recovery"
  3. bootloader加载内核,传递参数androidboot.mode=recovery
  4. 内核启动,init进程解析init.rc和init.recovery.rc
  5. init启动recovery服务,执行/system/bin/recovery
  6. recovery.cpp的main()函数执行:挂载分区 → 读取命令 → 执行操作 → 重启

这个流程,从Android 4.0到Android 13基本没变过。核心逻辑非常稳定。你想想看,一个系统组件能保持十几年的兼容性,说明它的设计确实经得起考验。

我个人建议,如果你想深入理解Recovery,最好的办法是自己在源码里加log。在recovery.cpp的main函数里,每个关键步骤前加一句LOGI("step X: xxx"),然后编译烧录,看logcat输出。这样比看任何文档都直观。

下一章,我们会深入Recovery的UI系统,看看那些菜单和进度条是怎么画出来的。到时候我会分享一个我踩过的坑——关于屏幕旋转导致UI错乱的问题,保证让你有所收获。