14、唤醒锁实战案例:音乐播放器保持CPU唤醒、下载任务保持网络唤醒、传感器数据采集唤醒
好,咱们今天聊点实在的。前面讲了那么多唤醒锁的理论和API,说白了都是为了解决一个核心问题:怎么让手机在特定场景下该醒的时候醒着,该睡的时候睡着。
我这些年做项目,踩过最多的坑就是唤醒锁滥用。要么是音乐播着播着突然断了,要么是下载任务一锁屏就失败,要么是传感器数据莫名其妙丢了一堆。嗯,今天咱们就拿三个最典型的场景开刀,把代码和思路都捋清楚。
核心原则:唤醒锁不是越多越好,而是越精准越好。能用PARTIAL_WAKE_LOCK就别用FULL_WAKE_LOCK,能用超时就别用无限期。
14.1 音乐播放器:保持CPU唤醒
音乐播放这个场景,我估计大家都遇到过。用户锁屏听歌,结果播着播着就卡了。为什么?因为CPU休眠了,音频缓冲区没数据了。
其实音乐播放只需要CPU保持运行,屏幕关不关无所谓。所以这里用PARTIAL_WAKE_LOCK就够了。
// 音乐播放器中的唤醒锁管理
public class MusicPlayerService extends Service {
private PowerManager.WakeLock wakeLock;
private static final String TAG = "MusicPlayer";
@Override
public void onCreate() {
super.onCreate();
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
wakeLock = pm.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
TAG + ":audio_playback"
);
// 注意:这里不要立即acquire,等真正开始播放再获取
}
public void startPlayback() {
if (!wakeLock.isHeld()) {
wakeLock.acquire(10 * 60 * 1000L); // 最多持锁10分钟
Log.d(TAG, "获取CPU唤醒锁,防止播放中断");
}
// 开始播放逻辑...
}
public void pausePlayback() {
if (wakeLock.isHeld()) {
wakeLock.release();
Log.d(TAG, "暂停播放,释放唤醒锁");
}
// 暂停播放逻辑...
}
@Override
public void onDestroy() {
super.onDestroy();
if (wakeLock.isHeld()) {
wakeLock.release();
}
}
}
我的经验:我在做某个音乐App时,发现用户切歌频繁会导致唤醒锁反复acquire/release。后来我加了个延迟释放机制——暂停后等3秒再释放,避免频繁开关锁带来的功耗抖动。
这里有个细节要注意:acquire()可以传超时参数。我个人习惯永远加上超时,哪怕你觉得逻辑上一定会release。为什么?因为万一出现异常,系统还能帮你兜底,不至于让CPU一直醒着。
14.2 下载任务:保持网络唤醒
下载任务和音乐播放不一样。下载需要网络连接,而网络模块在屏幕关闭后可能会被系统关闭。这时候光用PARTIAL_WAKE_LOCK是不够的,还得配合WifiManager.WifiLock。
你想想看,CPU醒着但Wi-Fi关了,下载照样失败。所以下载场景需要双重锁:CPU锁 + Wi-Fi锁。
// 下载任务中的双重锁管理
public class DownloadTask {
private PowerManager.WakeLock cpuLock;
private WifiManager.WifiLock wifiLock;
public DownloadTask(Context context) {
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
cpuLock = pm.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
"DownloadTask:cpu_lock"
);
WifiManager wm = (WifiManager) context.getApplicationContext()
.getSystemService(Context.WIFI_SERVICE);
wifiLock = wm.createWifiLock(
WifiManager.WIFI_MODE_FULL_HIGH_PERF,
"DownloadTask:wifi_lock"
);
}
public void startDownload() {
// 获取双重锁
cpuLock.acquire(30 * 60 * 1000L); // 30分钟超时
wifiLock.acquire();
Log.d("DownloadTask", "获取CPU+Wi-Fi锁,开始下载");
// 执行下载...
// 建议在子线程中执行,避免阻塞主线程
new Thread(() -> {
// 下载逻辑
// 下载完成后释放锁
releaseLocks();
}).start();
}
private void releaseLocks() {
if (cpuLock.isHeld()) {
cpuLock.release();
}
if (wifiLock.isHeld()) {
wifiLock.release();
}
Log.d("DownloadTask", "下载完成,释放所有锁");
}
}
避坑指南:我曾经在项目中犯过一个错误——下载完成后忘记释放Wi-Fi锁。结果用户反馈手机待机功耗异常高,一查发现Wi-Fi锁一直没释放,导致Wi-Fi模块持续全速运行。从那以后,我强制要求所有锁的获取和释放必须成对出现,并且用try-finally包裹。
另外,大文件下载建议分段持锁。比如每下载10MB释放一次锁再重新获取,这样系统有机会在间隙中做一次省电调度。别一口气持锁几个小时,那太粗暴了。
14.3 传感器数据采集:按需唤醒
传感器采集这个场景比较特殊。它不像音乐播放那样需要持续唤醒,也不像下载那样需要长时间持锁。传感器采集往往是周期性的——采集几秒钟,然后休息几分钟。
我见过不少开发者直接持锁不释放,结果传感器一直在采,功耗直接起飞。正确的做法是:只在采集窗口期内持锁,采集完立即释放。
// 传感器数据采集:按需持锁
public class SensorCollector {
private PowerManager.WakeLock wakeLock;
private SensorManager sensorManager;
private Sensor accelerometer;
private Handler handler = new Handler(Looper.getMainLooper());
// 采集周期:每5分钟采集10秒
private static final long COLLECT_DURATION = 10_000; // 10秒
private static final long INTERVAL = 5 * 60_000; // 5分钟
public SensorCollector(Context context) {
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
wakeLock = pm.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
"SensorCollector:data_collection"
);
sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
}
public void startPeriodicCollection() {
// 使用Handler实现周期性采集
handler.postDelayed(new Runnable() {
@Override
public void run() {
collectData();
// 再次调度
handler.postDelayed(this, INTERVAL);
}
}, 0);
}
private void collectData() {
// 采集前获取锁
wakeLock.acquire(COLLECT_DURATION + 2000); // 多给2秒余量
sensorManager.registerListener(new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
// 处理传感器数据
float x = event.values[0];
float y = event.values[1];
float z = event.values[2];
Log.d("SensorCollector",
String.format("采集数据: x=%.2f, y=%.2f, z=%.2f", x, y, z));
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// 精度变化处理
}
}, accelerometer, SensorManager.SENSOR_DELAY_NORMAL);
// 10秒后停止采集并释放锁
handler.postDelayed(() -> {
sensorManager.unregisterListener(this);
if (wakeLock.isHeld()) {
wakeLock.release();
}
Log.d("SensorCollector", "采集结束,释放唤醒锁");
}, COLLECT_DURATION);
}
}
我的建议:传感器采集的持锁时间一定要精确计算。我见过有人把COLLECT_DURATION设成30秒,结果实际采集只需要5秒,白白浪费了25秒的功耗。建议用System.nanoTime()精确计时,采集完立即释放,不要等固定时长。
还有一个容易被忽略的点:传感器注册和注销一定要配对。如果只注册不注销,传感器会一直工作,即使你释放了唤醒锁也没用——传感器本身就在耗电。
14.4 三种场景对比总结
好了,三个场景都讲完了。我画了一张对比图,方便你快速回顾:
从这张图能看出来,三种场景虽然都用唤醒锁,但用法完全不同。音乐播放是持续低功耗,下载任务是持续高功耗,传感器采集是间歇性功耗。你想想看,如果拿音乐播放的锁策略去搞下载,Wi-Fi模块早关了;拿下载的策略去搞传感器,电池半天就没了。
最后说一句:唤醒锁是Android电源管理的核心工具,但也是双刃剑。用好了,用户体验丝滑;用不好,用户骂你耗电。我这些年最大的体会就是——永远假设你的锁会泄漏,永远给锁加上超时。
总结三个实战要点:
- 音乐播放:PARTIAL_WAKE_LOCK + 延迟释放 + 超时保护
- 下载任务:CPU锁 + Wi-Fi锁 + 分段持锁 + 成对释放
- 传感器采集:按需持锁 + 精确计时 + 传感器注销
好了,这一章的内容就到这儿。代码和思路都给你了,剩下的就是动手实践。记住,纸上得来终觉浅,绝知此事要躬行。