接到个挺有意思的需求,把Android关机流程从头到尾捋一遍。之所以想做这件事,是因为前阵子在某款定制设备上排查关机卡顿问题,翻了一整天的源码和日志,最后发现是三方应用在关机广播里做了同步网络请求,导致ShutdownThread卡在广播分发阶段足足等了十几秒。这让我觉得有必要把这条链路完整记录下来——从用户按下电源键到硬件下电,中间到底发生了什么,每一步在等谁,谁慢了会影响什么。这篇文章适合正在做系统定制、Framework开发或者对Android电源管理感兴趣的开发者,看完之后你不仅知道关机有多复杂,也能定位自己的设备卡在哪一环。
1. 关机流程全景:从电源键到内核
1.1 用户按下电源键之后的第一站
很多人以为关机就是SystemUI弹个对话框,用户点一下“关机”,系统就断电了。实际完全不是这样。Android的关机是一套跨了多个进程、多个守护进程、甚至涉及内核的分布式协作流程,任何一环出错都可能导致关机失败、重启或者卡死。
先看触发源头。用户在SystemUI的GlobalActions(就是长按电源键弹出的那个全局菜单)里点击“关机”,SystemUI会调用PowerManager的shutdown方法。这里有个容易踩坑的点:PowerManager.shutdown是系统API,需要Manifest.permission.SHUTDOWN权限,普通应用调它会被PermissionDenied直接拒绝。所以如果你想在应用层封装一个“定时关机”功能,要么走系统签名,要么通过ADB shell去执行,没有第三条路。
不过用户点击之后,真正的重头戏才刚开始。PowerManagerService收到shutdown请求后,会分两步走:先调用ShutdownThread.shutdown,进入Java层的关机编排;同时通过窗口管理器把“正在关机”的UI展示出来。后续所有组件的关闭动作,都在ShutdownThread里串行或者并行地被执行。
1.2 ShutdownThread是谁
ShutdownThread是frameworks/base/services/core/java/com/android/server/power/ShutdownThread.java这个类,可以说是整个关机流程的调度核心。它就像工厂里的车间主任,手里拿着一个长长的任务清单,逐项通知各个部门干活,并且要确认每项任务都完成了才进入下一步。
这个类里最重要的就是run方法。整个关机动作在run方法里用一个巨大的状态机推进,基本可以概括成以下几个阶段:
- 发ACTION_SHUTDOWN广播,通知所有监听关机的应用和系统组件;
- 等待广播接收者处理完毕,但设置了超时上限;
- 关闭各类系统服务,特别是ActivityManagerService、MountService这类重量级服务;
- 让vold(卷管理守护进程)卸载存储设备;
- 调用底层接口,触发内核完成最后的电源管理动作。
需要注意的是,ShutdownThread并不是在独立进程里跑的,它是运行在system_server进程里的一个线程。这意味着如果某个广播接收者阻塞了主线程,或者某个服务关闭时死锁,整个system_server都可能卡住,表现就是界面一直停在“正在关机”,然后用户感觉到设备发烫、掉电异常,这种情况我排查过不止一次。
1.3 不同触发方式下的分流逻辑
Android关机其实有好几条入口:用户主动关机、重启、紧急重启,以及长按电源键强制硬件断电。其中用户主动关机走的是上面说的完整ShutdownThread流程;重启走的是几乎一样的流程,只是最后一步变成reboot而不是power off;紧急重启(同时按电源键和音量键)会有额外的panic触发逻辑,但本质上还是走ShutdownThread。
值得单独说的是强制关机。很久以前Android设备可以通过长按电源键8到10秒来强制断电,这个动作其实不经过Android系统——它是由硬件PMIC(电源管理芯片)处理的。系统再怎么卡死,PMIC的长按超时还是能在硬件层面把电源切断,这也是为什么有些设备在关机卡死后按长按电源键能救回来。理解这一点,对排查“系统完全无响应”的场景非常有帮助:如果长按电源键都强制关不了机,基本可以判定是PMIC或者电池硬件的问题,而不是软件层面的故障。
2. 框架层核心:ShutdownThread的运行逻辑
2.1 广播通知为什么要按顺序来
ShutdownThread.run()里第一步动作是发送ACTION_SHUTDOWN广播。这个广播的语义是“系统要关机了,大家赶紧保存数据、释放资源”。之所以先说这个,是因为它最耗时也最容易出幺蛾子。
这个广播有个特性经常被忽略:它是动态注册接收者才能收到的protected broadcast,静态注册到manifest里是收不到的。很多人图省事在AndroidManifest里声明了一个Action为android.intent.action.ACTION_SHUTDOWN的receiver,结果发现关机时根本没回调,就是这个原因。正确的做法是在应用启动时通过Context.registerReceiver动态注册,并且获取一个唤醒锁以防设备在广播处理过程中进入休眠。
ShutdownThread在派发广播时不是发完就完事的,它会阻塞等待所有接收者处理完毕。具体机制是:每个接收者在onReceive里可以通过goAsync将耗时操作移至异步线程,并持有PendingResult;ShutdownThread通过一个计数锁跟踪每个PendingResult是否完成,直到全部完成或超时。这个超时时间在AOSP里大约是10秒,如果某个接收者一直不返回,系统也不会无限等下去,会强制继续后面的流程。但这个“强制继续”也意味着那个应用的数据保存可能不完整,下次开机可能多出一些半残的缓存文件。
2.2 系统服务的关闭顺序藏着大学问
广播发完之后,ShutdownThread开始按顺序关闭系统服务。这个顺序不是随便排的,每一步都有讲究。
先看关闭顺序的大致逻辑:在Android 8及以后的版本里,ShutdownThread不是逐个关闭所有服务,而是先调用ActivityManagerService的关键接口,让它执行shutdown。AMS会先通知所有前台Activity退出,再关掉各进程里注册的组件,最后才是停止自身的一些后台线程。之所以先动AMS,是因为它是调度中枢,如果最后关它,其他组件还在被它调度,就会产生无限等待。
接下来是MountService的shutdown。它要配合vold完成文件系统相关的收尾动作,比如卸载SD卡、刷回存储设备缓存、标记文件系统干净。这一步如果做得不好,轻则开机时频繁扫描修复,重则直接丢数据。我见过一个案例,某设备关机的MountService阶段卡了快20秒,查到最后是某个外置USB存储设备的IO卡死,vold在等待设备响应时没有及时超时,导致整个关机流程被拖住。
再之后才是各类杂项的关闭:蓝牙、Wi-Fi、传感器、输入法这些服务都会被逐个通知进入低功耗或停止状态。ShutdownThread对这些服务的处理往往是“尽力而为”,不会像广播和AMS那样严格等待,因为某些服务关不掉也不至于让手机无法关机,但广播和AMS如果处理失败,会直接影响数据完整性和下次开机体验。
2.3 关键延时与超时控制
看ShutdownThread源码时,你会发现一个非常有价值的设计思路:整个关机流程实际上是在“尽量争取时间”和“不让用户等太久”之间权衡。代码里布满了各种超时控制,比如等待广播完成最多10秒、等待vold卸载最多10秒、等待AMS关闭后台服务最多5秒。
这些超时值背后的逻辑是一致的:关机流程中的每一步都是希望在有限时间内完成,但如果某个组件就是不给面子,系统不能无限等待。等得久了用户会感觉到设备卡死,再严重一点可能直接触发看门狗重启。所以在定制系统时,如果哪一步经常超时,优先要解决的是那一步真正的问题,而不是把超时值调大。把超时调大只是把问题掩盖起来,表面上流程能走完,但用户会明显感觉到关机变慢。
从我个人的工程经验来说,真正良性的关机流程应该控制在3到5秒内:广播阶段1秒左右,AMS和vold各1到2秒,下电动作瞬间完成。如果你定制的设备关机超过10秒,不用怀疑,一定有组件在“拖后腿”。
3. 底层配合:vold、init与内核怎么联动
3.1 为什么必须先卸载存储
现在我们降低一个视角,看Java层之外的配合过程。ShutdownThread在Java层调用了MountService.shutdown,但真正干活的其实是MountService底层的vold守护进程。
vold是Volume Daemon的缩写,负责管理Android所有卷设备的挂载、卸载、格式化等操作。在关机流程里,vold要做的核心事情有两件:一是确保所有已挂载的卷都正常卸载(clean unmount),二是把文件系统标记为干净状态。为什么要这么较真?因为如果文件系统没有正常卸载,下次开机时会触发fsck检查甚至自动修复,严重的话某些分区会进入只读模式。
有一个非常容易被误解的地方:很多人以为关机时Android会把/data分区卸载,其实不会。从Android 4.2开始,/data分区在正常关机时并不会真的卸载,因为系统需要它来记录关机日志(比如dropbox、batterystats这些),同时现代化的文件系统(如F2FS、ext4)在挂载状态下也能通过sync和日志刷回家保证一致性。真正会被vold卸载的是外置存储、虚拟卷这类可插拔设备。这点和PC上的Windows关机哲学是一致的:内核会把脏页刷回磁盘,但不会卸载C盘。
vold卸载卷的过程也不是瞬间完成的。每个存储设备在卸载前都要向上层广播ACTION_MEDIA_UNMOUNTED之类的状态变化,还会等待文件系统把最后的元数据写回介质。这个过程受限于存储介质本身的IO速度,所以如果你接了特别慢的U盘或者SD卡,这一步成为瓶颈是意料之中的事。
3.2 init进程怎么接手关机动作
等Java层所有服务都关闭完毕后,ShutdownThread会做最后的收尾:它设置sys.powerctl这个系统属性为“shutdown”,然后整个流程就跳出了Java世界,进入init进程的管辖范围。
可能有人疑惑,为什么改一个属性就能触发关机?这其实是Android系统设计的一个精妙之处。init进程始终在轮询property_service,一旦发现sys.powerctl的值变化,就会执行对应的动作。在init.rc里,这些动作被描述为trigger,比如on property:sys.powerctl=shutdown就会触发执行各种关机脚本,包括卸载分区、flush日志等。
init进程的关机动作里有个细节:它会调用一个叫“powerctl”的内核接口,往/sys/power/reboot这个sysfs节点写入“shutdown”字符串。内核拿到这个指令后,会遍历所有在线CPU,调用device_shutdown关闭各类设备驱动,最后执行内核级的power off。这一步如果失败,最典型的表现就是设备不会断电,而是“关机后重启”,也就是你看到的关机动画消失后屏幕又亮了,这在排查问题时要格外重视。
3.3 内核下电前还要做什么
内核的power off流程可以概括为:执行reboot系统调用,进入kernel_power_off函数,先关掉次要设备驱动,然后调用每个注册的machine_power_off回调,最终把CPU送入halt或类似状态。这个过程中,各个设备驱动要正确处理自己的关闭顺序,尤其是电源管理芯片、基带、充电控制器的驱动。
基带模块的关闭尤其重要。如果基带没有正常下电,关机后可能出现“电池掉电极快”或者“设备发热”的情况。这是因为基带在高负载状态下功耗不低,很多低端设备在关机后电池仍然一直在消耗,甚至有人反馈“手机关机了还会变烫”,多半就是基带没被正确关闭。
还有一类特殊场景是“关机充电”(Charge Mode)。设备连着充电器关机时,很多设备的PMIC并不会完全切断CPU供电,而是进入一个只保留充电管理和关键显示驱动的低功耗状态。这块走的是独立的charger模式,和正常的关机流程是并行的。在做定制时千万别把这两条路径混在一起,否则可能出现“关机后指示灯乱闪”、“充电图标不显示”的诡异问题。
4. 工程实战:定制关机流程的几个硬骨头
4.1 改关机确认对话框的前置准备
做系统定制的人,最常接到的需求就是“把关机确认弹窗改一下”。这个弹窗在AOSP里是SystemUI模块下的GlobalActions部分。要注意版本差异,Android 12以后,GlobalActions被重构得相当彻底,很多之前可以直接改的XML布局被拆分成了多个子布局,再加上状态栏、锁屏界面等多种展示场景,改动前一定要先在设备上确认当前版本的实际弹窗效果。
在动手之前,我的建议是先梳理需求边界:是只想改“关机”和“重启”两个按钮的文案,还是要加点特别的功能?比如有些定制设备需要把“重启到Recovery”作为一个独立选项。基础的文案修改可以直接通过overlay资源实现,但是要增加新的动作项,就必须动Java/Kotlin代码了。
如果是纯文案,用RRO(Runtime Resource Overlay)方式就够了,在overlay的config.xml里覆盖对应的string资源,不需要改framework代码。这样好处太多了:避免修改SystemUI后编译耗时、不用处理SELinux权限变化、升级系统时还能保留定制。如果是加逻辑按钮,那就得真正修改GlobalActionsDialog的代码,找到buildActions方法,在actions列表里新增一个ActionItem,实现对应的press逻辑,然后在AlertDialog上重新设置适配器。
4.2 自定义关机广播监听器的正确写法
接着讲一个被问过很多次的点:怎么在应用里监听关机事件,好让应用在关机前保存数据。前面我提过,ACTION_SHUTDOWN是protected broadcast,静态注册收不到,所以正确姿势是动态注册。
public class ShutdownReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (Intent.ACTION_SHUTDOWN.equals(intent.getAction())) { // 这里的耗时操作必须走goAsync,否则会被系统严格执行超时 PendingResult result = goAsync(); PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); PowerManager.WakeLock wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "shutdown:save"); wakeLock.acquire(2000); new Thread(() -> { try { saveData(); } finally { result.finish(); wakeLock.release(); } }).start(); } } }这段代码有几个要点值得拆开讲。第一,onReceive里不能直接做耗时操作,因为它在主线程执行,阻塞了SystemUI和system_server的交互。第二,goAsync拿到PendingResult后,一定要在异步任务结束时调用finish,否则ShutdownThread会一直等待这个接收者,直到超时。第三,WakeLock的acquire时间要留余量,但也不能太长,因为如果你在广播处理期间CPU真的休眠了,代码根本执行不完。我一般建议保守设置在1到3秒,只要业务不是特别重,这个时间足够完成一次轻量级的数据落地。
另外补充一个很实用的小技巧:如果想知道当前设备关机和开机的具体时间点,可以在接收者里记录SystemClock.elapsedRealtime和System.currentTimeMillis,把两个值都写进SharedPreferences或者文件里。下次开机时对比一下,就能估算出设备从“用户确认关机”到“真正关机”用了多久,这个信息对做功耗优化很有价值。
4.3 解决关机慢的常规排查路径
排查关机慢这个问题,很多初学者会去改ShutdownThread里的超时时间,这是下下策。正确思路是定位到底哪一步慢了。
第一步,抓取完整的关机logcat。在关机前执行:
adb logcat -b all -v threadtime > /sdcard/shutdown_logcat.txt然后在Shell里执行adb shell input keyevent KEYCODE_POWER,让它弹窗,再点关机。如果你的设备支持串口或者有内核日志抓取条件,把dmesg也一并抓到。在这些日志里,重点搜索关键词:ShutdownThread、vold、MountService、ActivityManager、PowerManagerService。
第二步,根据关键日志判断卡点。下面我整理了一张问题速查表,基本能覆盖大部分场景。
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
| 一直停在“正在关机”界面超过15秒 | 有广播接收者未在onReceive/异步线程里及时finish | 搜索结果里找到对应应用,优化掉耗时操作 |
| 关机动画放完,屏幕灭了又亮起来 | init的powerctl动作失败或者内核reboot流程异常 | 抓kernel日志,重点看device_shutdown阶段是否出错 |
| 关机后无线网卡/基带仍然耗电 | 这些驱动在shutdown时没有正确注册reboot_notifier | 检查底层驱动和内核配置 |
| vold阶段耗时严重 | 外置存储设备IO卡死、挂载状态异常 | 检查存储设备健康状态,或者调整vold的超时策略 |
| 系统属性设置失败 | SELinux限制了write权限 | 在对应的property_contexts和SELinux policy里放开权限 |
第三步,也是最容易被忽视的:关机慢很可能是过往运行态的累积问题,而不是关机那一刻才冒出来的。比如某个应用长期持有Partial WakeLock不释放,导致系统一直不能进入深度休眠,那它在关机过程中的资源回收也会异常艰难。所以排查时要同时看关机前一段时间的日志,把前因后果串起来。
4.4 关机动画与UI层的适配细节
说完逻辑层,再补一个视觉层容易踩的坑:关机动画。AOSP默认是没有关机动画的,很多定制系统为了让用户体验更好,会在关机时播放一段类似开机动画的shutdownanim.zip。这里需要注意,关机动画播放的时机是ShutdownThread已经广播完成之后,分布在vold和AMS的处理阶段。如果关机动画本身有Bug,可能会拖延整体关机时间。
还有一个经常被忽略的点:关机动画跑完后,屏幕会短暂黑屏,然后进入“正在关机”的静态界面。这个静态界面其实是由WindowManager绘制的一个ProgressDialog,它会在ShutdownThread执行到systemShutdown时显示。如果你改过SystemUI的全局弹窗布局,这里要注意层级关系,不要让你自定义的视图盖住了系统的关机进度提示,否则用户就会觉得“按下关机键后没有任何反馈”。
5. 常见故障与排查技巧实录
5.1 典型问题的真实复盘
接着讲几个我在实际排查中遇到过的真实案例,这些经验很难在文档里找全,但非常值得参考。
案例一:某设备关机后约5秒自动重启。一开始我怀疑是内核电源管理驱动问题,反复审查device_shutdown流程都没发现异常。后来通过抓取pstore日志,发现其实是init进程在关机脚本阶段执行了一个非幂等的清理动作,导致关机成功后触发了panic重启机制。这种问题的隐蔽性在于,它既不是Java层问题,也不是典型的内核驱动问题,而是介于init和生产脚本之间的灰色地带。
案例二:关机一直卡在“正在关闭”长达25秒,用户反馈极其糟糕。从日志看是某个支付类App的Service在onDestroy时做了一次同步网络请求,而那个请求因为无线网络没有及时断开,一直在重试。问题的根源是关机广播处理时,所有应用都在抢着利用最后的网络机会,触发了一波“末班车效应”。这种问题单纯调大超时没用,正确解法是在关机广播阶段对网络访问做全局限流,或者在关键广播处理时直接禁用网络。
案例三:某些第三方应用在关机阶段收不到广播。排查后发现,这跟它们自身注册的PendingIntent有关——如果应用用了FLAG_IMMUTABLE之类的新版本flag,在某些Android版本组合下会导致动态注册的receiver无法被系统识别。这其实是代码兼容性问题,但表现时间点恰好卡在关机流程,很容易让人误判成系统问题。
5.2 如何二次开发一个“省电关机”模式
最后一个加分项:给关机流程做一个定制扩展——例如支持“快速关机”模式。这种模式的核心思路是在某些场景下跳过部分不必要的服务关闭动作,比如不等待耗时应用的数据保存,直接把进程杀掉,然后立刻下电。
实现思路并不复杂:在ShutdownThread.shutdown方法入口处增加一个参数判断,如果配置了快速关机,则把某些等待锁直接置为“已触发”,同时把广播处理阶段的总等待时间缩短到1.5秒左右。需要注意的是,快速关机模式会对应用数据完整性造成较大风险,建议只在应急场景(比如电量极低即将断电)下启用。
另外有人问过我怎么增加“定时关机”功能。正确路线是先做好AlarmManager定时触发,到点后再调PowerManager.shutdown。这个过程中要注意AlarmManager的闹钟类型选择,建议用RTC_WAKEUP,保证设备在休眠状态下也能被唤醒起来执行关机任务。实测下来这种方案很稳,不会出现定时到了但系统还在睡的情况。
6. 写在最后的经验之谈
Android关机流程分析这件事,其实特别适合作为系统入门的“解剖课”。它的链路长、组件多,但每一步又足够清晰。我在最初接触的时候也走了一些弯路,比如纠结于为什么发完广播后还要等待、为什么vold卸载和AMS关闭要区分先后。等真正看完AOSP源码和实际日志之后才明白,这些设计的核心逻辑只有一个:在保证数据安全和下电成功率的前提下,尽量让关机过程短一点。
实际操作中我也总结出一条经验:遇到关机问题,不要前期就上调试器和打断点,先老老实实把logcat和kernel日志抓到,把流程时间线还原出来,大多数问题的答案都会浮出水面。建议手头常备一份AOSP源码索引,把ShutdownThread、MountService、vold这几个关键类的位置熟记在心,排查问题的速度会快很多。关机流程虽然不多见于日常应用开发,但一旦涉及系统定制,它就是绕不开的硬骨头。希望这篇文章能帮你少踩几个坑。