ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android RescueParty 救援机制:触发到 recovery 清数据

Android RescueParty 救援机制:触发到 recovery 清数据 开机动画转了两三分钟屏幕一黑机器自己重启了再转再黑再重启。反复四五轮之后屏幕直接跳进一个蓝底或黑底的 recovery 菜单然后提示正在清除数据。这个场景做过定制板子、安卓盒子、车机或者量产手机的兄弟应该都不陌生——Android 的 rescueParty救援模式出手了。后台 logcat 里刷出来的是一串 RescueParty 标签的日志用户看到的是设备莫名其妙恢复出厂设置。网上搜 rescueParty 的人问题基本就两个它到底在什么条件下触发以及为什么无法启动的终局处理方式是钻进 recovery 把用户数据清掉。我前后跟过几条产品线的系统定制从 AOSP 主线代码到各家芯片方案商的魔改版本都翻过也见过测试同学把救援当成硬件故障来报的乌龙。下面把 rescueParty 从触发条件、分级动作、到写 BCB 进 recovery 的整条链路拆开讲一遍顺带把复现、调试和避坑的手法整理出来。1. 先还原一次无限重启到 recovery的现场1.1 用户视角看到的三段式过程救援触发的过程从用户角度拆开看其实是三段。第一段是正常的开机尝试开机动画在转看起来跟平时没区别只是转得久一点因为每次 system_server 启动到某个阶段都会挂掉。第二段是自动重启循环设备自己黑屏、震动、再亮屏重新走一遍引导这个循环会重复若干次次数取决于触发的是包扫描失败还是启动本身失败。第三段就是 rescue 的最高级别动作——写入恢复命令、重启进 recovery屏幕上出现清数据的界面如果该机型 recovery 是纯文本菜单用户甚至要自己按一下确认键。很多人以为卡开机动画和进 recovery是两件不相干的事其实它们是同一条链路的前后两段。卡动画是现象救援是系统在后台做的判断进 recovery 是判断之后的最终执行动作。中间那个自动重启的循环才是救援机制真正在计数的地方。1.2 为什么系统宁可清数据也不停在动画界面站在系统设计者的角度这个取舍其实很清晰。当 framework 层起不来的时候任何在 framework 里做修复的动作都跑不起来——你想重置某个设置项需要 SettingsProvider 活着你想卸载一个坏掉的预装应用需要 PackageManagerService 活着。而设备已经卡在启动流程里这些东西全在半死状态。那剩下的可选项只有两个让用户继续盯着一个永远转不完的动画或者用一个不依赖 framework 的最小环境去做一次彻底的状态复位。前者的结果是设备变砖用户只能返厂后者的结果是数据没了但机器能开机、能打电话、能重新装应用。对绝大多数消费者来说第二种结果明显更容易接受厂商的售后成本也低得多。这就是 rescueParty 存在的根本理由——它不是修复而是止损。需要提前说清楚一点救援模式本身不修任何东西它做的所有动作本质都是删和重置。指望它把一台系统文件被改坏的机器救回来是不现实的。1.3 触发链路上一共有几个报警人RescueParty 不是一个主动扫描的服务它自己不会定时去检查系统健康度。真正干活的方式是被别人举报系统里几个关键组件在出问题的时候会主动调RescueParty的静态方法上报RescueParty 累计这些上报达到阈值之后才开始动作。主线分支上比较常见的上报点有这么几类上报来源调用位置示意典型故障场景包扫描失败PackageManagerService扫描 system 分区时核心预装包 manifest 损坏、签名不匹配、dex 优化反复失败启动过程失败ActivityManagerService引导阶段system_server 在启动窗口期内崩溃或被 watchdog 干掉应用连续崩溃崩溃处理路径较新分支同一个应用在短时间内反复崩频率超标这些上报点分散在不同进程里RescueParty提供的是静态入口方法把它们汇总到同一个计数器上。理解了这一点后面看日志就不会迷惑logcat 里出现 RescueParty 的日志不一定是应用崩了也可能是包扫描没通过。2. RescueParty 源码里的核心机制拆解2.1 一个文件干完所有事主线代码里整个救援逻辑集中在一个文件frameworks/base/services/core/java/com/android/server/RescueParty.java这个类大概两千行出头没有对应的 Service没有 Binder 接口全是静态方法和静态字段。它被SystemServer在早期startOtherServices阶段调用一次初始化之后就处于待命状态谁出问题谁调它。这种无 Service 化的设计很合理——救援逻辑必须在系统最不健康的时候也能跑如果它本身依赖某个 Service 活着那关键时刻它自己就先挂了。类的对外入口方法在不同 Android 版本上有变化。比较早期的形态是两个方法一个给包扫描失败用一个给启动失败用到 Android 10 之后逐步合并成带布尔标志的方法用参数区分这次是启动失败还是单个包失败再往后又加入了应用连续崩溃的入口。想确认你手上分支的具体形态直接grep -n public static void note frameworks/base/services/core/java/com/android/server/RescueParty.java一条命令把所有上报入口列出来比翻文档快得多。2.2 分级救援动作一级比一级狠救援不是一发入魂直接清数据它是分级递进的。老版本Android 8、9是五级Android 11 之后主线变成六级名字和顺序大致如下等级常量名示意动作用户能感知到的变化0LEVEL_NONE什么都不做无1LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS重置非可信默认值极少数设置项回默认2LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES重置非可信修改项第三方改过的设置被抹掉3LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS重置可信默认值厂商预置的默认值也回滚4LEVEL_RESET_SETTINGS_ALL重置全部设置设置项基本清空数据还在5LEVEL_FACTORY_RESET进 recovery 清数据恢复出厂这里的可信和非可信是最容易被忽略的细节。系统在判断一个设置项该不该被清掉时会看它的来源如果是系统自己默认值或者厂商预置的默认值属于可信一侧如果是第三方应用通过接口改进去的属于非可信一侧。前几级只动非可信那部分尽量保住厂商的定制默认值等级越高清理范围越往可信一侧蔓延。所以你会看到一个很有意思的现象有些定制机上救援走了两三级设备的壁纸、铃声、默认输入法还是原来的但某些被第三方 App 改过的开关悄悄回默认了。这不是玄学是分级在起作用。2.3 阈值和时间窗崩几次才会动手救援不会因为你崩了一次就出手它有自己的计数逻辑。主线上的默认阈值常量通常是 5 次配合一个时间窗概念同一个故障在窗口期内累计到阈值才抬升一级。// 以下是按主线简化出来的骨架常量名和数值请以你手上的分支为准 private static final int DEFAULT_RESET_THRESHOLD 5; private static int calculateRescueLevel(int currentLevel, int failureCount, int threshold, int minimumLevel) { if (currentLevel LEVEL_NONE) { if (failureCount threshold) { return minimumLevel; } } else if (failureCount threshold) { return currentLevel 1; } return currentLevel; }这段骨架说明了两个关键点。第一从 0 级起步的时候一次到位跳到某个最低起始等级而不是老老实实从 1 级开始不同上报来源的起始等级不一样启动失败的起始等级通常比单个包失败更激进。第二已经进入某个等级之后再次满足阈值就简单地加一一路推到最高级。想验证你这台设备上的实际阈值最快的办法是看日志里每次升级之间隔了多少次崩溃或者直接读源码里的常量grep -n THRESHOLD\|WINDOW frameworks/base/services/core/java/com/android/server/RescueParty.java别指望用一条setprop就把救援关掉。阈值和等级都是编译期常量量产机上想改只有两条路改代码重新出包或者在上报点做屏蔽。有些分支暴露了仅供测试用的开关方法只在当前进程有效重启就没了。3. 从降级重置到写恢复命令的完整过程3.1 前几级到底重置了什么重置动作最终是交给 SettingsProvider 去做的。大致的调用链是RescueParty 拿到当前等级判断这次该动Settings.Global、Settings.Secure、Settings.System里的哪几张表然后通过 ContentResolver 向 SettingsProvider 发起一次重置调用由 SettingsProvider 按自己维护的一份白名单和黑名单去决定每个键的生死。这份名单是救援机制里最厂商相关的部分。AOSP 只提供了一个默认范围各家定制会往里加自己的东西。我见过一个很典型的坑某厂商把自己的开机向导完成标记存在 Settings.System 里结果救援走到第三级这个标记被当成可重置项清掉了机器开机后重新弹了一遍开机向导测试同学以为是 Launcher 出问题了查了两天才找到救援日志。排查这类问题的思路很简单只要设备出现某些设置莫名其妙恢复默认先去 logcat 里搜 RescueParty看有没有出现过等级抬升。有就是救援干的没有再往别处查。3.2 最后一级为什么必须是 recovery到了LEVEL_FACTORY_RESET这一级系统要干的事情就一件把/data清掉。这件事为什么不能在 framework 里做原因有四个。第一/data必须在不挂载状态才能安全格式化而 framework 起来的过程中/data早就挂上了还有一堆进程在上面读写强行格式化等于自毁。第二加密设备上/data的解密密钥材料涉及 metadata 分区这套处理逻辑在 vold 和 recovery 里不在 system_server 里。第三此时 framework 本身处于半死状态你没法保证执行清理的那段代码能跑完不断电。第四重启是唯一可靠的状态复位手段重启之后所有进程、所有缓存、所有内存态都没了系统回到一个干净的起点。所以救援的最后一步不是清理而是写一条命令然后重启。真正动手清理的是重启之后起来的那个恢复环境。这也是无法启动时会进入 recovery这句话的完整含义不是系统想进 recovery而是清/data这件事只有 recovery 能干系统只能把命令挂在那儿然后重启。3.3 RescueSystem 写 BCB 的细节写入动作由android.os.RecoverySystem完成核心是两步。第一步组装命令串形如--wipe_data --localezh_CN不同分支可能还会附加--reason之类的溯源字段方便事后从恢复日志里倒查这次清理是谁触发的。第二步把这个命令串塞进 BCB然后调电源管理接口重启。BCB 是 bootloader control block 的缩写位置在 misc 分区开头结构长这样/* 简化后的结构字段长度和顺序以你手上的 bootloader 头文件为准 */ struct bootloader_message { char command[32]; /* 引导指令写 boot-recovery */ char status[32]; /* 执行状态回写 */ char recovery[768]; /* 恢复命令例如 --wipe_data */ char stage[32]; char reserved[1184]; };流程走起来是这样RescueParty 调到RecoverySystemRecoverySystem打开 misc 分区的块设备A/B 设备上通常通过 boot_control HAL 走把command字段写成boot-recovery、recovery字段写成命令串然后调用PowerManager的重启接口并带上进入恢复模式的标志。重启以后 bootloader 先读 BCB看到boot-recovery就跳过正常引导路径把控制权交给恢复镜像。早期版本里还有一个额外动作把命令同时写一份到/cache/recovery/command。后来 cache 分区在很多新设备上被合并掉了这条路径逐渐退化成可选。翻老代码的时候看到双份写入不要奇怪那是历史遗留。在 A/B 和虚拟 A/B 设备上物理上没有独立的 recovery 分区恢复环境是 boot 分区里那份 ramdisk 的一个变体通过引导参数切进去。用户看到的现象一样底层路径不一样排查的时候别拿老设备的思路硬套。4. 进了 recovery 之后清数据这一步是怎么执行的4.1 recovery 解析命令串恢复环境起来之后第一件事是解析自己收到的参数。命令串里每个双横线开头的字段对应一个动作--wipe_data表示要清用户数据--wipe_cache表示清缓存--locale只是设置界面语言。解析完之后进主流程先做设备相关的预处理调用设备抽象层的 pre-wipe 钩子再进真正的清理函数。这里有个容易被忽略的点恢复环境里跑的清理代码和你在设置里点恢复出厂设置跑的其实是同一套逻辑。两者最终都会收敛到同一个清理函数上区别只在于一个由用户点击触发一个由 BCB 里的命令触发。所以如果你在设置里点恢复出厂能正常工作救援路径上的清理大概率也能正常工作反过来如果恢复出厂本身有 bug救援路径一样会出问题。4.2 格式化 /data 时内部存储的去留清理函数处理/data的时候不是简单地把它格式化完事内部还有一套保留逻辑。设计者考虑的是用户照片、视频、下载文件这些东西存在/data/media里跟应用的私有数据物理上在同一个分区上。如果一刀切格式化用户几年的照片就没了。AOSP 的处理方式是把/data/media和/data的其余部分区别对待格式化之前先把媒体目录挪到临时位置格式化完成后再挪回来。较新的分支把这件事抽象成一个 preserve_media 标志交给底层的格式化模块处理但设计意图是一样的。不过这里有个现实问题是否连带清掉内部存储取决于命令串里有没有对应的媒体清理标志以及厂商自己那份恢复环境怎么实现的。所以同样是救援进了 recovery 清了一次不同机器上照片的去留差别可能非常大。这件事在自家机型上一定要实测确认别拿别人的结论套。加密设备上还有一层坑如果 metadata 分区和/data的状态对不上格式化完可能出现无法解密、无法挂载/data的情况设备会在恢复环境里再次卡住。这种情况通常得做一次更彻底的分区级清理用户侧是无解的只能走售后。4.3 恢复环境里的两种结局清完之后恢复环境有两种走法。一种是直接重启进正常系统这时候设备应该能正常开机用户看到的是全新的开机向导界面。另一种是恢复环境自己也卡住了比如/data格式化失败、metadata 损坏、分区表异常这时候设备会停在恢复菜单上需要手动选择重启或者其他操作。判断是哪一种看恢复日志就够了。主线恢复环境的日志会往几个固定位置落最省事的办法是清完之后立刻接上调试线抓一份搜关键字wipe_data、format、failed to mount一眼就能看出清理是成功还是中途失败了。5. 实战怎么复现、怎么抓证据、怎么定位5.1 先把日志抓到手设备一旦进了救援流程最先要看的是救援自己的日志。它用的 TAG 就是 RescueParty抓的时候可以这样adb logcat -b all -s RescueParty:* PowerManagerService:* RecoverySystem:* rescue.log adb shell getprop ro.boot.bootreason adb shell getprop | grep -i rescue第一条把救援相关的日志全捞出来包括等级抬升、执行了哪个等级、上报来源是什么。第二条看的是上次重启的原因返回值能直接告诉你这次重启是普通重启、看门狗触发还是进了恢复模式比如出现 recovery 字样就说明确实走了恢复路径。第三条看有没有和救援相关的属性残留用来确认计数状态。如果设备已经进不去系统了日志就得靠别的通道。/sys/fs/pstore/目录下通常会有上次崩溃的 console 记录/proc/last_kmsg在部分内核配置上也有效。这些是老设备上的救命稻草比反复重启试运气靠谱。5.2 手动复现一次降级救援想验证救援逻辑是否正常在开发板上做是最安全的。最可控的复现方式是破坏一个系统分区里的核心包让包扫描阶段就报错adb root adb remount # 备份原包 adb pull /system/priv-app/SomeCoreApp/SomeCoreApp.apk ./backup.apk # 用一个故意损坏的包替换它例如只写入随机数据的假 apk adb push ./broken.apk /system/priv-app/SomeCoreApp/SomeCoreApp.apk adb reboot重启之后盯紧 logcat 里的 RescueParty 输出正常情况下能看到包扫描失败的上报然后在达到阈值之后看到等级抬升的日志。验证完记得把备份的包推回去别把测试机彻底搞成砖。需要注意的是破坏普通应用和破坏核心包的效果差别很大。早期版本的救援只对系统分区里的包扫描失败敏感第三方应用反复崩溃未必触发。所以复现实验一定要挑系统分区的包而且要在测试机上做量产机上一旦触发就是真清数据。5.3 厂商集成时最容易踩的几个坑第一个坑是自研 Launcher 或系统界面不稳定。这两个组件如果反复崩溃直接被判定为启动失败救援等级抬得特别快。我见过一个案子自研桌面在某个机型上因为一次资源加载异常每十秒崩一次测试机跑了一下午第二天早上开机就是恢复出厂设置的状态测试同学报了个设备随机清数据的严重问题。第二个坑是把带问题的预装包放进系统分区。系统分区的包在扫描阶段出错会直接走上报路径。这类问题在上报点做一下白名单、在出厂前做一次完整的包扫描校验就能提前拦掉一大半。第三个坑是自定义默认值和救援的重置范围冲突。前面说过重置逻辑依赖一份名单厂商往里加定制项的时候如果没标好类别救援走到中间等级就会把你精心调好的默认值清掉用户投诉升级之后设置全乱了。第四个坑是研发阶段反复插拔电源导致的误触发。启动过程被打断也会被计入启动失败次数够了照样抬等级。调试机上遇到莫名其妙的救援动作先确认一下是不是自己频繁断电造成的。6. 常见问题速查与避坑经验6.1 问题速查表现象大概率原因排查手段卡动画几轮后自动进 recovery启动失败累计到最高级触发了恢复查 logcat 里的 RescueParty 等级日志、查 bootreason设置项莫名回默认数据还在救援走到中间等级只重置了设置搜 RescueParty 日志确认等级和上报来源设备莫名恢复出厂设置某个系统组件反复崩溃累计到最高级查崩溃日志里的重复堆栈找反复崩的那个组件救援日志里看不到上报来源分支定制改过上报点或日志级别被调高直接读该分支的 RescueParty.java 和调用点恢复环境里清完数据起不来metadata 与 data 状态不一致、分区损坏抓恢复日志搜挂载失败关键字必要时做分区级清理改阈值没生效阈值是编译期常量改的是错误位置确认是否重新编译并刷入确认改动落在实际分支上6.2 我踩过的几个坑第一个坑是以为救援有关闭开关。最初接到能不能让设备别自动清数据的需求我第一反应是找个系统开关关掉折腾了半天才发现没有。后来走了另一条路在上报点做判断对某些确定是临时性失败的场景不上报。这个思路比关掉整个救援机制好因为救援本身是有价值的保险关掉它等于把售后风险留给了自己。第二个坑是只盯着应用层崩溃找原因。有台设备反复进恢复模式我们把应用崩溃日志翻了个遍什么都没找到最后发现根因在包扫描阶段——一个预装的系统包 dex 优化一直失败重试到阈值就上报了。教训是排查救援问题一定要把上报点到执行点整条链路都覆盖别想当然只查应用崩溃。第三个坑是忽略了恢复环境的数据保留差异。我们内部文档里写了一句救援会清除用户数据结果有台机型实测发现照片还在团队里为这个事来回确认了好几天。后来统一了口径文档里只写救援会重置系统状态是否清除内部存储必须实测确认。技术文档里凡是涉及数据去留的表述都要留出实测余地。7. 几个容易被混淆的救援场景澄清社区里搜 rescueParty 的人经常会搜到一堆看着相关其实完全无关的东西顺手澄清几个我被人问过很多次的。第一个是电脑开机时那句 default boot device missing or boot failedinsert recovery media and hit any key。这个和 Android 一点关系都没有它是主板固件找不到可引导硬盘时打的提示属于 PC 侧的引导失败。有人拿这句来问我的手机是不是也进了 rescue纯粹是关键词撞车。第二个是手动进恢复模式和救援自动进恢复模式的区别。前者是用户按音量键加电源键主动进入用于刷机、双清之类的操作主动权在用户手里。后者是系统在启动失败后自己写 BCB 重启进去的用户没有选择权。两种路径最后停的界面可能长得一模一样但触发原因完全不同——看到恢复界面先别急着判断肯定是我自己按错了。第三个是各种同名工具造成的误解比如一些数据恢复工具、云恢复工具名字里带 recovery 但和 Android 的恢复分区毫无关系。搜资料的时候认准 AOSP 源码路径和 BCB 这两个关键词能过滤掉九成噪音。最后一个提醒是给做测试和售后的同学判定一台设备是不是被救援清过数据最硬的证据是 logcat 里的 RescueParty 等级日志配合 BCB 的历史记录其次是恢复环境的日志。只看设置被重置、数据不见了这两条现象就下结论很容易冤枉别的模块。我自己的习惯是先捞日志再表态捞不到日志就注明未取到直接证据这样后面复盘时不会被自己的结论带着跑。
返回列表