ARTICLE DETAIL

资讯详情

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

Android 关机重启源码解析:从 PowerManager 到 init 的完整调用链

Android 关机重启源码解析:从 PowerManager 到 init 的完整调用链 简介面向Android系统应用开发与源码研究者的关机和重启reboot/shutdown源码分析资源聚焦SystemServer接收UI层请求后如何执行保存系统状态、关闭服务、停止应用等清理操作再调用内核reboot()函数完成关机或重启的完整链路。其中对比了REBOOT_MODE_SHUTDOWN和REBOOT_MODE_RESTART参数差异说明了普通应用无法直接触发、需要REBOOT或SHUTDOWN权限以及第三方应用通过Intent请求ACTION_REQUEST_SHUTDOWN并由用户确认的机制同时点出PowerManagerService与ActivityManagerService中的shutdown()/reboot()方法是关键入口适合电源管理学习、系统级调试或开发定时关机、定制重启功能。压缩包共49个文件、1.42MB以java源码、class文件、xml配置为主体另含jar、apk、dex、项目配置和png截图方便对照工程结构与运行效果。目前已有332人学习。从源码中可梳理用户空间到内核层的触发链路理解权限校验与清理顺序为自定义电源策略和系统优化提供可落地的修改起点。1. 这套 Android 关机和重启源码到底在解什么题拿到一份名为Android 关机和重启reboot and shutdown源码.zip的压缩包第一反应别急着解压先想清楚这里面该有什么。Android 的“关机”和“重启”在系统里并不是两个简单调用而是从应用层按钮到init进程、再到内核syscall的一条完整调用链。PowerManager的reboot()只是冰山一角真正干活的是ShutdownThread这个系统线程、PowerManagerService的权限校验以及reboot这个二进制执行后和init通信的整套机制。这份源码值得研究的点恰恰是大多数应用层开发者日常碰不到的地方Android 在收到关机请求后会依次弹出关机对话框、发送ACTION_SHUTDOWN广播、逐个停止关键服务、最后写入sys.powerctl属性触发内核级电源操作。这套流程里还埋着几个让人头疼的坑——比如REBOOT权限需要signature|privileged级别、ShutdownThread主线程阻塞会导致 ANR、三方应用想重启系统只能靠su和Runtime.exec()。本文会从源码目录结构开始把reboot和shutdown两条路径拆开讲覆盖PowerManagerService、ShutdownThread、init的powerctl处理再落到签名权限、SELinux和实际编译集成上适合正在做系统定制、TV 盒子固件或者 ROM 开发的工程师。新手机厂商要用这份代码改出一键重启进 Recovery 的功能TV 方案商要拿它做关机定时任务都可以直接参考下面的流程。2. 定位核心实现从上层 API 到 init 的完整调用链2.1 解压后先看这几个关键文件拿到 zip 后先unzip到工作目录然后按下面的路径去核对源码是否完整。AOSP 中关机/重启相关代码散落在frameworks/base和system/core两个仓库zip 里至少应该包含这些unzip Android_关机重启源码.zip -d ~/reboot_source cd ~/reboot_source find . -path */ShutdownThread.java -o -path */PowerManagerService.java | head -5正常会输出类似frameworks/base/services/core/java/com/android/server/power/ShutdownThread.java的路径。如果没有输出说明压缩包改过目录结构用find . -name *.java | xargs grep -l rebootOrShutdown再找一次。核心文件清单如下文件/路径作用关键类或方法frameworks/base/core/java/android/os/PowerManager.java暴露给应用层的 APIreboot(String reason)、shutdown(boolean confirm)frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java系统服务端实现校验权限和延迟shutdownOrRebootInternal()frameworks/base/services/core/java/com/android/server/power/ShutdownThread.java真正执行关机/重启流程的线程shutdown()、run()、actionDone()system/core/reboot/reboot.creboot命令行工具源码main()解析参数并调用__reboot()system/core/init/powerctl.cppinit进程处理sys.powerctl属性HandlePowerctlMessage()frameworks/base/services/core/java/com/android/server/power/ShutdownThread.java内部广播接收关机广播ACTION_SHUTDOWN相关逻辑实际看代码时建议用grep -n按方法名跳转先看PowerManagerService.shutdownOrRebootInternal()的入口因为上层PowerManager.reboot()最终 binder 调用都会进到这个方法。这个方法的签名大致是// PowerManagerService.java private void shutdownOrRebootInternal(final boolean shutdown, final boolean confirm, final String reason, boolean wait) { if (mHandler null || !mSystemReady) { // 系统未就绪时直接抛异常或忽略 throw new IllegalStateException(Too early to call shutdown()); } // reason 非空时走重启关机时 reason 为 null // 关键权限校验在调用者这里已经由 PowerManager 的 Binder 接口做了 }注意wait参数true表示调用方会阻塞等待关机流程完成false则直接返回。这个参数直接影响上层PowerManager.reboot()会不会 ANR后面第 3 章会说具体场景。2.2 应用层调用到系统服务的两条入口应用层发起重启的写法有两种。系统应用或签名应用比如系统设置里的“重启”按钮可以直接持有PowerManager实例并调用带REBOOT权限保护的接口PowerManager pm (PowerManager) getSystemService(Context.POWER_SERVICE); // 携带 reason 字段进入 Recovery 就是 recovery进 BootLoader 是 bootloader pm.reboot(recovery);这条调用链是PowerManager.reboot()-PowerManagerService.reboot()-shutdownOrRebootInternal(false, false, reason, true)。注意confirm参数为false说明不会弹确认框。而普通第三方应用没有REBOOT权限android.permission.REBOOT的protectionLevel是signature|privileged只能通过反射或者Runtime.exec()走reboot命令// 第三方应用通常配合 ROOT 使用 Runtime.getRuntime().exec(new String[]{su, -c, reboot recovery});这里要区分shutdown和reboot两个命令在reboot.c里的行为差异。reboot命令实际上是一个二进制程序的符号链接shutdown指向同一份代码但编译器传入了不同的宏定义。看system/core/reboot/reboot.c的main()开头int main(int argc, char *argv[]) { int action 0; // 编译时通过 -DACTION_REBOOT... 区分命令名本身也会影响默认行为 if (strcmp(basename(argv[0]), shutdown) 0) { action ACTION_SHUTDOWN; } // ... // 关键代码写入 sys.powerctl 属性 property_set(sys.powerctl, shutdown, reason); }这里最容易被忽略的是reboot.c并不是直接调用reboot()系统调用而是通过property_set(sys.powerctl, ...)通知init进程由init统一执行电源操作。好处是init可以做 SELinux 上下文切换、同步文件系统、处理ro分区的 remount 等收尾工作。这个设计也是 Android 与标准 Linux 关机流程差异最大的地方——普通 Linux 用systemctl或shutdown命令直接操作 systemdAndroid 则强制经过属性系统。3. 深挖 shutdown 源码ShutdownThread 如何把系统安全停掉3.1 Shutdown 的 4 个阶段和状态机ShutdownThread.shutdown()是系统关机的总入口调用方传入Context、PowerManager实例和confirm标志。整个流程被拆成几个连续的阶段用一个ActionDone接口做同步阻塞。// ShutdownThread.java public static void shutdown(final Context context, PowerManager pm, boolean confirm) { // 第 1 步注册关机广播接收器 // 第 2 步如果是第一次请求非重试先显示关机动画或确认对话框 // 第 3 步调用 beginShutdownSequence() 真正执行 ShutdownThread shutdownThread new ShutdownThread(); shutdownThread.mContext context; shutdownThread.mPowerManager pm; beginShutdownSequence(context); }beginShutdownSequence()里会做几件重要的事sInstance.mReboot false标记这是关机而不是重启sInstance.mReason null然后启动一个PROCESS_SHUTDOWN_TIMEOUT的延时广播默认超时时间是 10 秒超时后强制继续执行后续步骤。真正干活的run()方法逻辑用伪代码拆解如下public void run() { // 阶段 A广播 ACTION_SHUTDOWN等待所有接收器处理完成 // 用 CountDownLatch 或 ActionDone 接口做同步 sendShutdownBroadcast(); // 阶段 B设置系统属性让其他进程知道系统正在关闭 SystemProperties.set(sys.shutdown.requested, 1); // 阶段 C逐个停止关键服务mount、vold、netd 等 // 阶段 D调用 PowerManagerService 的 shutdownOrRebootInternal 真正执行 mPowerManager.shutdownOrRebootInternal(mReboot, false, mReason, true); }注意sendShutdownBroadcast()内部用的是sendOrderedBroadcastAsUser并且传入了一个resultReceiver。也就是说它并不是发完广播就结束而是等所有有序广播接收器都执行完onReceive()才继续。如果某个接收器在这期间卡死就会触发最开始设的 10 秒超时。这些动作能看出 Android 关机不同于桌面系统它必须先通知所有应用保存数据再确保文件系统刷盘。3.2 广播接收和超时机制的实际代码看sendShutdownBroadcast()的代码重点注意它构造了一个局部广播接收器用来在全部应用处理完后调用actionDone()// ShutdownThread.java private void sendShutdownBroadcast() { Intent intent new Intent(Intent.ACTION_SHUTDOWN); intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); // 采用有序广播保证接收器串行执行 mContext.sendOrderedBroadcastAsUser(intent, UserHandle.ALL, null, mShutdownBroadcastReceiver, null, 0, null, null); }大多数应用开发者在AndroidManifest.xml里注册ACTION_SHUTDOWN接收器时以为它会在onReceive()里拿到足够时间做清理。实际上文档没有说明的是如果接收器是BroadcastReceiver而不是goAsync() 后台线程配合它的执行时间会被限制在onReceive()返回前非常短暂。正确做法是public class ShutdownReceiver extends BroadcastReceiver { Override public void onReceive(Context context, Intent intent) { if (Intent.ACTION_SHUTDOWN.equals(intent.getAction())) { // 不能在这里做耗时操作要用 goAsync 线程池 final PendingResult result goAsync(); new Thread(() - { // 保存数据写磁盘 saveData(); // 必须调用 finish否则系统会认为该接收器卡死 result.finish(); }).start(); } } }这里goAsync()的作用是告诉系统“我已经接管了这个广播会在后台线程完成工作稍后通过PendingResult.finish()通知你”。如果不这么做onReceive()主线程一返回接收器的生命周期就结束了后面的耗时任务可能被系统直接回收。而actionDone最终会调用ShutdownThread.run()里的mActionDoneSync让流程继续往下走。整个ShutdownThread的源码里布满了这种“等一个动作完成再继续”的同步机制这也是阅读时最容易搞混的地方——它虽然叫Thread实际运行时阻塞点非常多不像普通线程一路跑到底。3.3 权限和 SELinux 在关机路径中的坑ShutdownThread的run()走到最后会调用PowerManagerService的shutdownOrRebootInternal()此时系统服务层会再次检查权限。PowerManagerService.reboot()的 Binder 接口被PowerManager调用权限检查位于PowerManagerService的binder内部类Override public void reboot(boolean confirm, String reason, boolean wait) { // permission 检查 mContext.enforceCallingOrSelfPermission(android.Manifest.permission.REBOOT, null); // 进一步判断是否允许设备管理 if (reason ! null reason.startsWith(dm)) { // 设备管理器触发的重启需要额外校验 DevicePolicyManager } shutdownOrRebootInternal(false, confirm, reason, wait); }这个enforceCallingOrSelfPermission一旦抛SecurityException上层PowerManager.reboot()就会直接抛异常。所以第三方应用走pm.reboot()会 crash必须用su。另一个隐蔽问题出在 SELinuxinit进程处理sys.powerctl属性写入时会校验写入进程的安全上下文。su进程的 context 通常是u:r:su:s0而init的powerctl属性规则要求写入者是init或特定 domain。在system/sepolicy/private/property_contexts里能看到类似定义sys.powerctl u:object_r:powerctl_prop:s0这意味着如果 ROM 里 SELinux 处于 enforcing 模式即使你已经拿到了 ROOT 权限直接执行setprop sys.powerctl reboot,recovery也可能被拒绝。验证方式是用adb shell su -c id确认 context然后用dmesg | grep avc查看是否有 denied 记录adb shell dmesg | grep avc: denied | grep powerctl如果输出里有powerctl_prop相关 denied说明是 SELinux 策略问题而不是代码问题需要在sepolicy里给sudomain 加一条allow su powerctl_prop:property_service set;。这一步常见于自己编译的 AOSP 工程官方 ROM 通常已经把su和shell的权限配好了。4. reboot 源码的三种形态普通重启、Recovery、BootLoader4.1reboot.c的参数解析和sys.powerctl写入逻辑reboot命令有三种常用形态其实都是同一个二进制加不同参数。看源码// system/core/reboot/reboot.c int main(int argc, char *argv[]) { int action ACTION_REBOOT; // 解析参数-p 代表关机power off // 不含 -p 时默认重启带 recovery 或 bootloader 参数时设置 reason if (argc 1) { if (!strcmp(argv[1], recovery)) { action ACTION_REBOOT_RECOVERY; } else if (!strcmp(argv[1], bootloader)) { action ACTION_REBOOT_BOOTLOADER; } } // 写入属性注意这里拼的是字符串 char *cmd action ACTION_REBOOT ? reboot : shutdown; property_set(sys.powerctl, cmd); }property_set是 Bionic libc 提供的接口内部通过 socket 连接property_service这个 socket 通信由init进程维护。sys.powerctl属性被写入后init会收到一个 property 变更触发执行HandlePowerctlMessage()。核心逻辑在system/core/init/powerctl.cpp中判断命令是reboot还是shutdown然后调用do_reboot()或do_shutdown()。这里有一个值得注意的细节reboot命令传参不是通过属性值里的某个字段而是直接拼接字符串比如property_set(sys.powerctl, reboot,recovery)。逗号后面跟的是reason参数。init解析时会用strchr找逗号然后按字符串匹配// init/powerctl.cpp int HandlePowerctlMessage(const std::string command) { // 用逗号切分前半是 action后半是 reason std::string action command.substr(0, command.find(,)); if (action reboot) { // reason 可能是 recovery/bootloader 或 null // 最终调用 reboot(RB_AUTOBOOT) 或向 bootloader 传递 cmdline } else if (action shutdown) { // 真正执行 poweroff() } }实际使用中如果遇到重启后不进 Recovery 的问题多半是 reason 拼写错误比如多打了个空格或用了大写Recovery。源码里对 reason 的长度也有限制Android 12 及以后版本会先校验长度不超过 32 字节超出会直接返回错误。可以用下面的命令测试属性写入是否成功adb shell setprop sys.powerctl reboot,bootloader注意此时设备会立即重启进入 bootloader如果没反应用adb shell getprop sys.powerctl查看当前值确认是否写入失败。getprop返回值如果为空说明 SELinux 或权限拒绝。理解sys.powerctl是理解整个 Android 重启源码的钥匙它不是一个普通属性而是init特判的一个“控制通道”。4.2shutdown -s -t 7200这类命令在 Android 上的映射热词里出现了shutdown -s -t 7200、30分钟后重启电脑shutdown这些是 Windows 的关机命令但在 Android 源码阅读者眼中它们对应的其实是“定时关机/定时重启”的实现需求。AOSP 原生没有提供像 Windows 那样的命令行定时关机工具但是可以借助AlarmManager或Handler.postDelayed实现类似效果。常见做法是写一个系统应用或者用adb shell配合nohup定时执行# 30 分钟后重启 adb shell nohup sh -c sleep 1800 reboot 这种方式的问题是adb shell断开会话后进程可能被杀所以正规做法是把延时任务交给init或AlarmManager。看ShutdownThread的源码时你会发现confirm参数和shutdown调用的Thread.sleep并不是为定时设计的系统级定时关机通常是改PowerManagerService加一个内部接口或者直接用以下命令配合at类似机制。对没有at服务的 Android更可靠的方案是写一个小的系统服务注册AlarmManager闹钟AlarmManager am (AlarmManager) getSystemService(Context.ALARM_SERVICE); long triggerAt System.currentTimeMillis() 30 * 60 * 1000L; // 触发后执行重启 am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAt, pendingIntent);这段代码的好处是设备即使在 Doze 模式下也能按时触发因为setExactAndAllowWhileIdle专门为此设计。若只是做 ROM 内置的“定时重启”功能把这段代码放到系统设置里加一个开关就行。命令行方式虽然直观但会话断开即失效工厂测试时可以用用户场景不推荐。4.3 修改源码给 reboot 增加一个“重启到 eMMC” 自定义模式拿到源码后最常见的定制需求是增加新的重启模式。比如盒子方案商想增加一个“重启到烧录模式”可以在reboot.c参数解析处加分支同时要改init那边的解析。这个改动涉及三层第一层reboot.c加参数映射} else if (!strcmp(argv[1], burn)) { action ACTION_REBOOT_BURN; }第二层在写入sys.powerctl时把自定义 reason 传下去if (action ACTION_REBOOT_BURN) { property_set(sys.powerctl, reboot,burn); }第三层powerctl.cpp里把burn映射到特定分区或 bootloader 命令if (reason burn) { // 写 misc 分区标记或者直接传递 cmdline 给 bootloader std::string cmdline androidboot.burn_mode1; // 调用 kernel reboot 时带上 cmdline reboot_with_cmdline(RB_AUTOBOOT, cmdline); }做这三步时最常犯的错是只改第一层忘记init那个if分支这样执行reboot burn会退化成普通重启。正确流程是把这三层都改完然后编译reboot工具并 push 到/system/bin/同时编译 init 并替换boot.img。只 pushreboot不更新 boot 分区的话sys.powerctl的值会变成reboot,burn而旧版 init 不认识burn会直接按普通重启处理没有报错也没有预期效果排查时很容易造成困惑。5. 将源码集成到自己的 AOSP 工程编译、签名与实测验证5.1 替换系统内置 reboot 工具的正确步骤假设你从 zip 解出来的源码是完整的 AOSP 对应版本现在要把它集成到自己的 ROM 工程。先确认源码里的Android.bp或Android.mk文件reboot.c对应的构建目标名称是什么# 在源码根目录执行 grep -rn reboot system/core/reboot/Android.bp | head -10输出里能看到类似name: reboot和name: shutdown这样的目标。编译时可以直接source build/envsetup.sh lunch your_device-userdebug # 重新编译 reboot 和 shutdown 工具 make reboot shutdown编译产物在out/target/product/your_device/system/bin/reboot和shutdown使用adb root后 push 覆盖adb root adb remount adb push out/target/product/your_device/system/bin/reboot /system/bin/reboot adb push out/target/product/your_device/system/bin/shutdown /system/bin/shutdown adb shell chmod 755 /system/bin/reboot /system/bin/shutdown adb reboot需要注意adb remount在 Android 10 及以上默认关闭如果报错remount of the / superblock failed需要先执行adb disable-verity然后重启一次。这里涉及动态分区和dm-verity如果不处理push 的文件在重启后会被还原看起来像是源码没生效。验证是否替换成功重启后执行adb shell reboot --help如果输出里包含你新增的自定义参数说明修改已生效。5.2 用reboot命令验证三种重启模式的实测输出在真机上验证源码改动是否完整最直观的方法是执行命令后观察启动模式。下面是完整的测试矩阵建议按顺序执行测试项执行命令预期结果失败排查方向普通重启adb shell reboot设备重启进入系统看logcat -b system -d | grep -i reboot进入 Recoveryadb shell reboot recovery重启后进入恢复模式确认 recovery 分区可启动getprop ro.crypto.state进入 BootLoaderadb shell reboot bootloader重启后进入 bootloader 界面验证sys.powerctl是否写入成功关机adb shell shutdown设备完全断电检查logcat的ShutdownThread输出实测时最需要注意adb shell reboot和adb reboot的区别前者在设备端由shell用户执行会走完整的reboot.c逻辑包括sys.powerctl写入后者是 adb 服务端直接调用PowerManager.reboot()绕过了命令行工具。所以测试第三层powerctl.cpp的定制逻辑必须用前者否则改动不会触发。测完开机后立即执行adb logcat -b all -d | grep -E reboot|shutdown|powerctl会看到类似下面的日志序列PowerManagerService: [api] reboot: reasonrecovery ShutdownThread: shutdown started Sysprops: set sys.powerctl reboot,recovery init: powerctl: rebooting with reason: recovery这个输出顺序可以直观地验证调用链是否完整。如果缺中间的ShutdownThread: shutdown started说明你的reboot命令替换过程中把系统自带的流程跳过了如果init: powerctl没出现说明属性写入失败重点查 SELinux。5.3 验证关机动画和 ANR 问题的一个技巧代码集成后最关心的通常是关机动画是否正常显示、是否卡顿或 ANR。源码里ShutdownThread的beginShutdownSequence()有一段用来控制关机动画的逻辑它通过SystemProperties.get(sys.boot.animation)判断动画配置然后设置ServiceManager里的SurfaceControl来显示。实测时最好的验证方式是用下面的命令模拟半路进入的关机流程# 触发一次带确认对话框的关机32 秒后系统强制继续 adb shell am broadcast -a android.intent.action.BOOT_COMPLETED adb shell settings put global shutdown_timeout 5000 adb shell reboot -p第二条命令把全局关机超时时间改成了 5 秒这是调试用的隐藏参数。修改后执行shutdown -p如果在 5 秒内广播接收器没完成ShutdownThread会跳过等待直接进入后续流程。这样拿到的 log 能区分是超时问题还是执行阶段的问题。如果是在自定义 ROM 上调试 ANR可以在ShutdownThread.java的run()里临时加一条Log.d(ShutdownThread, current step: step)然后重新编译services.jar或者直接替换整个framework.jar看日志卡在哪一步。这一步是瓶颈分析时最快的手段最有效的索引就是mActionDone对应的那个同步锁对象有没有被正常通知。最终这一步不需要更多改动掌握sys.powerctl属性和ShutdownThread状态机的配合方式以后无论拿到哪个版本的 Android 关机重启源码都能沿着这条链快速定位改动点。上面的 push、编译、测试流程也完全可以复用。本文还有配套的精品资源点击获取
返回列表