简介:面向Android系统源码学习者的关机和重启(reboot/shutdown)实现剖析包,聚焦SystemServer、PowerManagerService以及ActivityManagerService中shutdown()和reboot()等核心入口,帮助开发者理解从用户空间到内核层触发电源操作的完整链路。压缩包共49个文件,以java、class、xml、jar等类型为主,包含Android工程配置、界面运行截图、APK与DEX产物,整体约1.42MB,便于对照源码与编译产物进行调试,已有332人学习。通过这部分源码可掌握系统级关机和重启的权限控制机制,了解如何通过Intent请求ACTION_REQUEST_SHUTDOWN并处理用户确认;还能进一步修改源码实现定时关机、自定义重启策略等扩展,同时提升应用电源管理和系统底层排错能力。
1. 从长按电源键到内核断电,这中间发生了什么
当你在 Android 设备上长按电源键选择关机,屏幕熄灭的瞬间系统其实做了大量工作:先通知各服务保存状态、同步文件系统、停止运行中的应用,最后才通过内核的 reboot() 系统调用把电源切断。这个流程远不是“直接断电”四个字能概括的,Android 源码里同时藏着关机与重启两条路径,权限、参数、回调时机完全不同。这份关机和重启(reboot and shutdown)源码.zip 打包了完整的 Android 工程结构,包含 AndroidManifest.xml、res、assets、gen 和 libs,适合做系统定制、AOSP 裁剪,或是想弄清楚 PowerManagerService 如何工作的开发者。读之前不需要看完整框架,只要顺着 SystemServer -> PowerManagerService -> ShutdownThread 这条线走一遍,就能明白大部分设计意图。
2. SystemServer 到内核 reboot() 的调用链与 Android 权限模型
2.1 用户空间的 shutdown 请求如何触达内核:reboot() 系统调用的参数语义
Linux 内核为用户空间提供了 reboot() 系统调用,Android 在用户空间并没有绕过它去直接操作电源管理芯片的寄存器,而是通过 libc 暴露的 reboot() 封装把指令传给内核。关机与重启的区别,本质上是传给内核的参数不同:关机时内核执行的是电源管理流程,最终让电源管理芯片切断电源;重启时内核只做硬件复位,然后把引导流程重新走一遍。
#include <sys/reboot.h> /* 关机:请求内核进入电源关闭流程 */ reboot(RB_POWER_OFF); /* 重启:请求内核复位,跳过断电步骤 */ reboot(RB_AUTOBOOT);上面这段代码看起来简单,实际使用中你不会在应用层直接调用它,因为 Android 的 SELinux 策略会拦截普通进程对 reboot() 的调用。系统框架层一般会走 PowerManagerService 的封装,再通过 JNI 进入 libc 的 reboot()。注意两个宏的区别:RB_POWER_OFF 是关机语义,RB_AUTOBOOT 是重启语义,如果混淆这两个参数,设备会做出你意料之外的动作。我在调试低功耗设备时,经常看到有人把“重启后进入 bootloader”的需求误写成普通重启,最后发现是少了 bootloader 这一层参数传递。
补一段 JNI 层的说明。AOSP 中 PowerManagerService 通过 nativeShutdown 和 nativeReboot 这样的本地方法进入 JNI,JNI 里再去调用 libc 的 reboot()。所以从 app 到内核,路径至少经历了 Binder 调用、PowerManagerService 状态检查、JNI 转接三层,每一层都可能在权限不满足时直接返回失败。
2.2 REBOOT 与 SHUTDOWN 权限:普通应用与系统应用的分界线
Android 把关机和重启设计成 signature 级别的权限,普通第三方应用默认拿不到。这里的关键差异在于,REBOOT 权限控制 PowerManager.reboot(),SHUTDOWN 权限控制 PowerManager.shutdown(),两者是独立声明的。
| 权限 | 保护级别 | 对应接口 | 典型使用者 |
|---|---|---|---|
| android.permission.REBOOT | signature | PowerManager.reboot() | 系统设置、AOSP 自带应用 |
| android.permission.SHUTDOWN | signature | PowerManager.shutdown() | SystemUI 长按电源键 |
| ACTION_REQUEST_SHUTDOWN | 不需要权限 | 系统弹窗确认后执行 | 第三方省电类 App |
第三方应用能做的,大多是通过 Intent 发起 ACTION_REQUEST_SHUTDOWN,由系统 UI 弹出确认框,用户在界面上确认后才会真正触发关机。源码里的 Manifest 可以印证这一点:系统级应用会把 android:sharedUserId="android.uid.system" 写上,只有和系统共享 UID,才有机会获得 signature 权限。如果你拿到这份源码后直接装到普通设备上,会发现点击按钮没有反应,原因就在这里——并不是逻辑写错,而是权限等级不够。
2.3 权限不足时的表现与判断方法
排错时先看 logcat。常见现象有两种:一种是直接抛 SecurityException,提示缺少 REBOOT 或 SHUTDOWN 权限;另一种是什么日志都没有,因为异常被框架层吃掉了。前者说明你的应用是独立 APK 安装的,没有系统签名;后者说明你已经拿到系统权限,但调用时机不对,比如系统尚未完成初始化。判断应用是否拥有系统权限,可以用下面的命令在开发阶段快速确认。
adb shell dumpsys package com.example.shutdown | grep "sharedUser"如果输出里能看到 android.uid.system,说明应用已经作为系统应用运行;如果这一行为空,说明权限模型还是普通应用那一套。这里顺带提一下,dumpsys package 输出的信息非常全,比单纯看 Android Studio 的日志更能定位权限问题,建议先把它用熟。
3. PowerManagerService 与 ShutdownThread 源码走查:参数、清理流程与定制点
3.1 PowerManagerService 的 shutdown() 与 reboot() 入口逻辑
PowerManagerService 的源码位置在 frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java,它接收来自 PowerManager 的 Binder 调用,再做状态校验。需要注意,它不是直接执行关机的类,真正干活的是 ShutdownThread,PowerManagerService 只负责确认当前系统状态允许执行。调用关系可以用下面这行代码来理解。
// 系统服务中触发软关机的常见写法 PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); pm.shutdown(false /* confirm */, "user" /* reason */, false /* wait */);三个参数值得展开。confirm 为 true 时,ShutdownThread 会先弹确认对话框,为 false 则直接执行;reason 会作为字符串传给底层,用于标识关机原因,比如 "user" 表示用户主动操作;wait 表示调用是否阻塞等待关机完成。定制系统时开发者经常把 confirm 写死为 false 来做定时关机,但要注意,PowerManagerService 在 Android 9 之后加了调用者的 UID 检查,不是系统 UID 直接拒绝,所以单纯改参数解决不了权限问题。reason 字符串会被记录到系统属性里,后续排查“设备为什么关机”时非常有用。
3.2 ShutdownThread 的清理流程:保存状态、停止服务、同步文件系统
ShutdownThread 是整个关机流程的编排者,它把操作拆成多个步骤,用 Handler 消息串联。顺序大致是:先向系统广播 ACTION_SHUTDOWN,让各服务有机会保存状态;然后同步文件系统,确保数据不会丢失;最后才调用底层接口真正关闭电源。
// ShutdownThread 中触发最终关机步骤的示意代码 Intent shutdownBroadcast = new Intent(Intent.ACTION_SHUTDOWN); context.sendBroadcast(shutdownBroadcast); // 同步文件系统,避免写入中断导致数据损坏 syncFileSystem(); // 走 PowerManagerService 的低层接口关闭电源 PowerManagerService.lowLevelShutdown(reason);这段逻辑解释了为什么关机过程可能很慢:广播是异步的,如果某个服务在 onReceive 里做了耗时操作,会一直延迟到超时。调试时如果发现关机要等十几秒,多半是某个广播接收器在阻塞。文件系统同步这一步在重启流程中同样存在,所以不要把“重启不保存数据”理解成“重启不会写数据”,它只是跳过了部分用户空间的清理,文件系统照样要同步。另外,ACTION_SHUTDOWN 广播是粘性广播的一种特例,系统会等待接收方处理完毕,这也是关机慢的常见来源之一。
3.3 通过源码修改实现定时关机:关键参数与常见坑
基于这份源码做定时关机,最常见的做法是在系统应用里注册一个 AlarmManager 定时任务,到点调用 PowerManager.reboot(),通过 reason 参数区分是进入 recovery 还是正常重启。
AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); am.setExact(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent); // 定时任务触发时执行重启,reason 会写入 sys.boot.reason PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); pm.reboot("scheduled");这里的 reason 参数会被写进系统属性,recovery 阶段可以通过 getprop 读取到,比如用 "reboot,recovery" 或 "reboot,bootloader" 做差异化处理。坑点在于 AlarmManager 的唤醒方式,设备深度休眠时 setExact 也不会准时触发,必须配合应用内持有的 WakeLock,或者直接走 AlarmManager 的 setAlarmClock 方法。另一个常见问题是在低电量时强行触发重启,电池电量低于阈值时,内核会拒绝执行关机动作,日志里表现为 reason 已传入但设备没有反应。所以做定时关机功能时,一定要在触发前检查电量状态,避免用户设备直接断电。
4. 用 Android Studio 调试关机和重启源码:编译、部署与验证
4.1 从 Eclipse 工程结构迁移到 Android Studio 构建
这份源码保留着 Eclipse 时代的工程形态,根目录有 .project、.classpath、proguard-project.txt,还有 gen 和 assets。直接双击用 Android Studio 打开会提示无法识别,常见做法是新建一个空项目,再把 src、res、assets、libs 拷贝到对应目录,并补上 build.gradle 配置。
android { compileSdk 30 defaultConfig { applicationId "com.example.powerctl" minSdk 21 targetSdk 30 } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) }这里有个重要细节:如果源码里使用了 android.uid.system,打包时必须使用平台签名,否则安装到设备后仍然无法调用 PowerManager.reboot()。Android Studio 的 debug 签名不可用,需要通过 signingConfigs 指向编译系统生成的 platform key。gen 目录是 Eclipse 自动生成的,迁移时可以删掉,让 Android Studio 重新生成 R 类;assets 目录如果没有特殊资源也可以留空,不会影响编译。build.gradle 中的 compileSdk 建议与设备系统版本保持一致,否则运行时会出现 Binder 接口不匹配的问题。
4.2 编译、签名与部署到模拟器
验证源码逻辑最稳妥的环境是 Android Studio 自带的模拟器配合 AOSP system image,因为这类镜像里的应用就拥有系统权限,不需要自己处理 platform key。如果一定要在真机上验证,可以把 APK 推到 /system/priv-app/ 目录下,重启后由系统加载。
adb root adb remount adb push powerctl.apk /system/priv-app/powerctl/powerctl.apk adb reboot安装后要注意 /system/priv-app 下的应用属于 privileged 应用,但仍然不能自动获得 signature 权限,只有与系统共享 UID 才能真正调用框架层的敏感接口。所以更可靠的调试路径是:把应用声明为 android:sharedUserId="android.uid.system",再用 platform key 签名,最后刷入系统镜像,而不是单纯放到 priv-app 目录。模拟器路径下,由于镜像本身是 userdebug 版本,SELinux 策略相对宽松,很多权限问题可以暂时绕过,适合先跑通业务逻辑,再切到真机考虑权限收敛。
4.3 用 adb 快速验证关机和重启接口
开发阶段不需要每次都走到 UI,adb 提供了一批直达接口的验证方式。adb shell reboot 就是最直接的验证命令,它走的就是与源码相同的重启路径。
# 普通重启,走框架层重启接口 adb shell reboot # 重启到 recovery adb shell reboot recovery # 通过 init 的 sys.powerctl 属性触达内核 adb shell setprop sys.powerctl reboot,recovery上面三条命令的效果不完全一样。adb shell reboot 由 adbd 直接调用框架层的重启接口,适合验证系统应用权限是否生效;setprop sys.powerctl reboot,recovery 是 init 进程在 watch 到系统属性变化后执行的,绕过了 PowerManagerService 的权限检查,只适合确认内核和 bootloader 链路是否正常。验证关机时要注意,执行后设备会断开连接,建议先用 dumpsys power 记录下当前状态,再执行命令,方便事后对照日志。如果设备没有反应,优先看 adbd 的日志,确认是权限拦截还是命令没有下发到 init。
5. 进阶技巧:重启到 Bootloader、Recovery 与延时重启的验证手法
5.1 用 reason 区分五种重启场景
源码里 PowerManager.reboot() 的 reason 参数最终会拼成 sys.powerctl 的值,不同的前缀组合对应不同的引导目标。下面这张表可以作为定制时的速查。
| reason 值 | 实际行为 | 适用场景 |
|---|---|---|
| reboot | 正常重启 | 系统更新后的软重启 |
| reboot,fastboot | 进入 fastboot 模式 | 刷机、解锁 |
| reboot,recovery | 进入 recovery | OTA 升级包安装 |
| reboot,bootloader | 进入 bootloader | 底层固件更新 |
| shutdown | 关机 | 无 |
注意 reason 的拼接格式是固定前缀 reboot 或 shutdown,加上逗号分隔的子目标,如果你写成了 recovery 而不是 reboot,recovery,init 会把它当作无效命令忽略。实际测试时,fastboot 和 bootloader 两个模式容易混淆,前者是 fastboot 协议层的刷机模式,后者是芯片厂商的引导菜单,不同芯片平台的命名也有差异,定制前要先确认目标设备的 bootloader 实现。
5.2 延时重启的可靠验证方法
实际调试定时重启时,建议先把延时设到 10 分钟以上,避免误触发。用 AlarmManager 的 setExactAndAllowWhileIdle 在 Doze 模式下仍然可能被系统合并,更稳妥的是同时持有 PowerManager.PARTIAL_WAKE_LOCK,并在触发瞬间打印日志确认时间点。
# 查看最近一次重启的原因与时间 adb shell dumpsys dropbox --print | grep system_server # 用 getprop 确认重启时写入的 reason adb shell getprop sys.boot.reasonsys.boot.reason 这个属性在每次启动后都会保留上一次的重启原因,比如 reboot,recovery 或 shutdown,user,这是排查“到底是谁把设备叫醒又重启”的最快入口。dumpsys dropbox 里则记录了 system_server 的崩溃与重启堆栈,如果定时任务没触发,这里通常能看到完整的线索。另外,验证关机流程时可以把串口日志打开,内核侧的 final_call 打印会明确显示 reboot() 收到的参数,这是 adb 断开后唯一还能确认底层行为的通道。
本文还有配套的精品资源,点击获取