3. Android Wakelock机制:原理、类型与释放策略
Wakelock 这东西,说白了就是一把「锁」。
它锁住的是系统休眠的权限。你拿着这把锁,系统就不敢睡。你放手了,系统才能安心进入休眠。我在早期做功耗优化时,发现很多应用层开发者对 Wakelock 的理解就是「用就完了」,结果导致手机一晚上掉电 30%。嗯,这其实是个大坑。
3.1 Wakelock 的工作原理
Wakelock 的核心机制其实很简单:谁持有锁,谁阻止休眠。
系统内核里有一个「suspend 流程」,每次尝试休眠前,都会检查当前是否有活跃的 Wakelock。只要有一个锁没释放,系统就乖乖等着。我习惯把这种机制叫做「投票制」——每个 Wakelock 都是一张反对票,只要有一张反对票,休眠提案就被否决。
关键流程:
- 应用层通过 PowerManager 申请 Wakelock
- Binder 调用到系统服务 PowerManagerService
- PMS 通过 native 层调用内核的 wakeup_source 接口
- 内核维护一个 wakeup_sources 链表,每次 suspend 前遍历检查
我记得有一次调试一个第三方 App 导致的不休眠问题,抓了 kernel log 发现有个 wakeup_source 一直处于 active 状态。顺着 pid 查过去,发现是某个直播 App 在后台持有一个 PARTIAL_WAKE_LOCK 没释放。你想想看,用户都锁屏了,App 还在后台跑,这不是耍流氓吗?
3.2 Wakelock 类型详解
Android 定义了四种 Wakelock 类型,每种类型的「锁力」不一样。我整理了一张表,方便你对照:
| 类型 | CPU | 屏幕 | 键盘灯 | 典型场景 |
|---|---|---|---|---|
| PARTIAL_WAKE_LOCK | 保持唤醒 | 可关闭 | 可关闭 | 后台音乐播放、下载任务 |
| SCREEN_DIM_WAKE_LOCK | 保持唤醒 | 保持亮屏(变暗) | 可关闭 | 视频播放、导航 |
| SCREEN_BRIGHT_WAKE_LOCK | 保持唤醒 | 保持亮屏(全亮) | 可关闭 | 游戏、交互操作 |
| FULL_WAKE_LOCK | 保持唤醒 | 保持亮屏(全亮) | 保持亮灯 | 全屏游戏、VR 应用 |
这里有个细节要注意:从 Android 5.0 开始,SCREEN_DIM、SCREEN_BRIGHT、FULL 这三种类型已经被废弃了。为什么?因为 Google 觉得应用不应该直接控制屏幕亮度,这太粗暴了。现在推荐的做法是用 FLAG_KEEP_SCREEN_ON 这个 Window 属性来代替。
避坑指南:
我曾经在适配 Android 9 时,发现一个老项目里用了 FULL_WAKE_LOCK。编译没问题,但运行时 logcat 会打印一条 deprecated 警告。更坑的是,在某些厂商的 ROM 上,这个锁根本不起作用。所以,新项目千万别再用这三种类型了。
3.3 PARTIAL_WAKE_LOCK 的正确使用姿势
PARTIAL_WAKE_LOCK 是目前唯一推荐使用的 Wakelock 类型。它只锁 CPU,不锁屏幕。说白了就是:CPU 可以继续跑,但屏幕该关就关。
我习惯这样使用:
// 申请锁
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
PowerManager.WakeLock wl = pm.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
"MyApp:BackgroundTask"
);
wl.acquire();
// 执行后台任务
doBackgroundWork();
// 释放锁
wl.release();
嗯,这段代码看起来没问题对吧?但实际项目中,我见过太多人在这里翻车了。最常见的问题就是:忘记释放锁。
我的习惯:
我每次申请 Wakelock 都会配套写一个 try-finally 块,确保 release 一定会被执行。另外,我会在 tag 里加上包名和具体用途,这样出问题时方便定位。比如 "com.example:MusicPlayer" 就比 "MyLock" 好一万倍。
3.4 Wakelock 超时机制
你可能会问:如果应用申请了锁,但崩溃了怎么办?系统岂不是永远睡不了?
答案是:系统有兜底机制。
acquire() 方法有一个带超时参数的重载:
// 申请锁,最多持有 10 秒
wl.acquire(10 * 1000L);
这个超时时间到了之后,系统会自动释放锁。我建议所有后台任务都加上超时,哪怕你觉得任务肯定能在 5 秒内完成。为什么?因为网络抖动、IO 阻塞这些情况你根本预料不到。
我记得有一次,一个下载服务在弱网环境下卡住了,持有一个 PARTIAL_WAKE_LOCK 长达 40 分钟。用户投诉手机发烫,我们查了半天才发现是 acquire 没设超时。从那以后,我定了个规矩:所有 Wakelock 必须设超时,除非你能证明不需要。
3.5 释放锁的注意事项
释放锁看似简单,但有几个坑:
- 不要重复释放:release() 调用多次会抛异常。可以用一个标志位判断是否已释放。
- 不要在异步回调里释放:如果回调在子线程,而锁在主线程申请,release 可能不生效。我习惯用 Handler 切回主线程释放。
- 注意引用计数:acquire() 和 release() 必须成对出现。多次 acquire 需要同样次数的 release。
最佳实践:
private PowerManager.WakeLock mWakeLock;
public void startTask() {
if (mWakeLock == null) {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
"MyApp:Task"
);
mWakeLock.setReferenceCounted(false); // 关闭引用计数
mWakeLock.acquire(30 * 1000L); // 最多 30 秒
}
}
public void stopTask() {
if (mWakeLock != null && mWakeLock.isHeld()) {
mWakeLock.release();
mWakeLock = null;
}
}
这里我用了 setReferenceCounted(false),这样不管 acquire 调用多少次,一次 release 就能释放。我个人比较喜欢这种方式,因为引用计数容易搞乱,尤其是在复杂的业务逻辑里。
3.6 内核层面的 wakeup_source
说完了应用层,咱们看看底层。Android 的 Wakelock 最终会映射到内核的 wakeup_source 机制。
内核里维护了一个全局链表,所有活跃的 wakeup_source 都挂在这个链表上。每次系统准备 suspend 时,会遍历这个链表:
// 内核伪代码
for_each_wakeup_source(ws) {
if (ws->active) {
// 还有活跃的锁,不能休眠
return -EBUSY;
}
}
// 可以休眠了
suspend_enter();
你可以通过 /sys/kernel/debug/wakeup_sources 查看当前所有 wakeup_source 的状态。我调试功耗问题时,第一步就是看这个文件:
adb shell cat /sys/kernel/debug/wakeup_sources | head -20
输出里会显示每个 wakeup_source 的 active_count、active_since 等信息。如果某个锁的 active_since 时间戳很久没变,那基本就是它导致的不休眠。
调试技巧:
我习惯写一个脚本,每隔 5 秒抓一次 wakeup_sources 的状态,然后对比变化。哪个锁一直 active,哪个就是罪魁祸首。这个方法帮我定位过至少 10 个功耗问题。
3.7 总结与避坑清单
最后,我列一个实战清单,你写代码时对照着检查:
- 能用前台服务就别用 Wakelock:前台服务自带 CPU 锁,而且用户可见,更规范。
- 必须设超时:acquire(timeout) 是底线,没有例外。
- 成对释放:acquire 和 release 要一一对应,最好用 try-finally。
- tag 要有意义:方便定位问题,格式建议 "包名:功能描述"。
- 废弃类型别用:SCREEN_DIM、SCREEN_BRIGHT、FULL 这些,忘掉它们。
- 测试要覆盖异常场景:进程被杀、网络断开、回调延迟,这些都要测到。
我曾经在一个项目里,就因为少设了一个超时参数,导致用户手机在夜间待机时无法进入深度休眠。那次的教训让我明白:Wakelock 是一把双刃剑,用好了是省电利器,用不好就是耗电元凶。嗯,希望你能用好它。