3、WakeLock深度剖析:WakeLock的创建与释放、锁的类型与级别、引用计数与非引用计数模式
好,咱们今天来啃一块硬骨头——WakeLock。说实话,我在做Android系统开发的头两年,跟WakeLock打交道时没少踩坑。有一次线上反馈说手机待机一晚上掉电30%,查来查去,最后发现是一个三方SDK持有了WakeLock没释放。嗯,从那以后,我对WakeLock的敬畏心就上来了。
WakeLock说白了就是电源管理模块给上层应用提供的一把"锁"。你拿着这把锁,系统就不敢轻易让CPU休眠或者屏幕熄灭。但问题是——这把锁怎么创建?怎么释放?有哪些类型?引用计数又是怎么回事?咱们一个一个说清楚。
3.1 WakeLock的创建流程
先看创建。在应用层,你调用PowerManager.newWakeLock(),系统会返回一个WakeLock对象。但真正的创建逻辑在系统服务端。
// frameworks/base/core/java/android/os/PowerManager.java
public WakeLock newWakeLock(int levelAndFlags, String tag) {
validateWakeLockParameters(levelAndFlags, tag);
return new WakeLock(levelAndFlags, tag, mContext.getOpPackageName());
}
这里有个细节——levelAndFlags参数。它其实是一个int值,高8位表示锁的类型,低24位表示额外的标志位。我个人习惯在代码里用位运算来拆解它,方便调试。
真正创建锁的核心在PowerManagerService里。当WakeLock对象第一次acquire()时,会通过Binder调用到系统服务:
// frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java
private void acquireWakeLockInternal(IBinder lock, int flags, String tag,
String packageName, WorkSource ws, String historyTag, int uid, int pid) {
// 1. 检查权限
// 2. 创建或更新WakeLock记录
// 3. 更新电源状态
}
注意看,这里的IBinder lock参数。每个WakeLock在系统服务端都对应一个Binder对象。这个Binder对象的作用很关键——当应用进程挂了,Binder死亡通知会触发系统自动释放锁。我曾经在项目里遇到过应用闪退导致锁没释放的情况,幸好有这个机制兜底。
3.2 WakeLock的释放流程
释放流程跟创建是对称的。应用调用release(),最终走到PowerManagerService.releaseWakeLockInternal()。
核心要点:释放锁时,系统会检查引用计数。如果是非引用计数模式,直接释放;如果是引用计数模式,计数减1,减到0才真正释放。
private void releaseWakeLockInternal(IBinder lock, int flags) {
WakeLock wakeLock = mWakeLocks.remove(lock);
if (wakeLock != null) {
// 更新电源状态
applyWakeLockFlags(wakeLock, flags);
// 通知系统更新休眠策略
updatePowerStateLocked();
}
}
这里有个坑——release()和release(int flags)的区别。带flags的版本可以传递RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY等特殊标志。我建议你在释放靠近传感器的锁时,用带flags的版本,避免传感器状态切换导致误操作。
3.3 锁的类型与级别
WakeLock的类型,说白了就是告诉系统:你到底想锁住什么?
| 类型 | 标志位 | CPU | 屏幕 | 键盘灯 |
|---|---|---|---|---|
| PARTIAL_WAKE_LOCK | 0x00000001 | 保持唤醒 | 可关闭 | 可关闭 |
| SCREEN_DIM_WAKE_LOCK | 0x00000006 | 保持唤醒 | 变暗 | 可关闭 |
| SCREEN_BRIGHT_WAKE_LOCK | 0x0000000a | 保持唤醒 | 亮屏 | 可关闭 |
| FULL_WAKE_LOCK | 0x0000001a | 保持唤醒 | 亮屏 | 亮灯 |
| PROXIMITY_SCREEN_OFF_WAKE_LOCK | 0x00000020 | 保持唤醒 | 根据距离传感器 | 可关闭 |
你想想看,为什么要有这么多种类型?其实是为了平衡功耗和用户体验。比如你在做音乐播放器,只需要CPU保持运行,屏幕可以关,那就用PARTIAL_WAKE_LOCK。如果你在做视频播放,屏幕必须亮着,那就用SCREEN_BRIGHT_WAKE_LOCK。
我的建议:能用PARTIAL_WAKE_LOCK就别用FULL_WAKE_LOCK。屏幕和键盘灯是耗电大户。我在优化一个视频App时,把FULL_WAKE_LOCK改成SCREEN_BRIGHT_WAKE_LOCK,续航提升了15%。
3.4 引用计数模式与非引用计数模式
这个知识点,说实话,很多人搞混过。引用计数模式,就是每次acquire()计数加1,每次release()计数减1,减到0才真正释放。非引用计数模式,就是一次release()直接释放,不管之前acquire()了多少次。
默认情况下,newWakeLock()创建的是引用计数模式。如果你想改成非引用计数,需要调用setReferenceCounted(false)。
WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyTag");
// 默认是引用计数模式
wakeLock.acquire(); // 计数=1
wakeLock.acquire(); // 计数=2
wakeLock.release(); // 计数=1,还没释放
wakeLock.release(); // 计数=0,真正释放
// 改成非引用计数模式
wakeLock.setReferenceCounted(false);
wakeLock.acquire();
wakeLock.acquire();
wakeLock.release(); // 直接释放,不管acquire了几次
注意:非引用计数模式容易出问题。我曾经在一个项目中,因为多线程环境下多次acquire但只release了一次,导致锁一直没释放。后来排查发现,是另一个同事把模式改成了非引用计数,但代码逻辑还是按引用计数写的。所以,我建议你尽量用默认的引用计数模式,除非你非常清楚自己在做什么。
3.5 实战中的避坑指南
最后,分享几个我在实战中总结的经验:
- 成对出现:每个
acquire()必须对应一个release()。我习惯用try-finally块来保证释放。 - 超时机制:
acquire(long timeout)可以设置超时自动释放。这个在后台任务中特别有用,防止异常情况导致锁一直持有。 - WorkSource:如果你的应用有多个进程或线程持有WakeLock,建议设置WorkSource,方便排查问题。
- 不要滥用:WakeLock是系统资源,用完后立即释放。我见过有些应用在后台轮询时一直持有PARTIAL_WAKE_LOCK,这种设计对续航是灾难。
好了,关于WakeLock的创建、释放、类型和引用计数,咱们就聊到这儿。下一节,我会带你看看WakeLock在系统层面的实现细节,包括Binder机制和电源状态机的交互。到时候你会发现,原来系统为了管理这把锁,背后做了这么多工作。