13. 唤醒锁的调试与优化:dumpsys power命令、唤醒锁泄漏检测、battery historian分析

说到唤醒锁的调试,我脑子里第一个蹦出来的场景就是:产品经理拿着手机,一脸严肃地跟我说「这手机待机一晚上掉电 20%,你看看吧」。嗯,这种时候,dumpsys power 就是我的第一把手术刀。

13.1 dumpsys power:你的第一现场

先别急着上什么高大上的工具。我个人的习惯是,发现问题后第一时间连上 adb,敲一行命令:

adb shell dumpsys power

输出内容很多,但核心就几块。我带你快速过一遍。

13.1.1 关键字段解读

你会在输出里看到类似这样的东西:

Power Manager State:
  mWakefulness=Asleep
  mWakeLockSummary=0x0
  mScreenOn=false
  mInteractive=false
  mDirty=0x00000000

Wake Locks: size=3
  PARTIAL_WAKE_LOCK           'MyApp:location_lock'     (ref count=1)
  PARTIAL_WAKE_LOCK           'System:network_sync'     (ref count=0)
  SCREEN_DIM_WAKE_LOCK        'Launcher:keep_awake'     (ref count=1)

这里我一般先看 mWakefulness。如果它显示 Asleep,但屏幕还亮着,那就有问题了。再看 Wake Locks 列表,重点关注那些 ref count 不为 0 的锁。

核心思路:一个正常的待机状态,除了系统必要的锁(比如网络同步),不应该有任何第三方应用的唤醒锁长期持有。

13.1.2 我常用的排查套路

拿到 dumpsys power 的输出后,我一般按这个顺序查:

  1. 看唤醒锁总数:如果超过 10 个,基本可以断定有泄漏嫌疑。
  2. 看每个锁的持有时间:有些锁虽然数量少,但一持就是几小时,这比数量多更可怕。
  3. 看锁的持有者:是系统服务还是第三方 App?如果是第三方,直接定位到包名。

我曾经遇到过一个案例,某个视频 App 在退出播放后,还持有一个 PARTIAL_WAKE_LOCK 长达 8 小时。你想想看,这 App 后台什么都没干,但 CPU 就是没法睡。最后定位到是播放器 SDK 的一个 bug,退出时没释放锁。

13.2 唤醒锁泄漏检测:从现象到根因

泄漏检测说白了就是「找不归的锁」。我把它分成两个层面:

13.2.1 静态代码检测

在代码层面,我习惯用 lint 加上自定义规则。比如检查 acquire()release() 是否成对出现。一个典型的泄漏代码长这样:

PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyTag");
wl.acquire();
// 执行一些异步操作
// 但忘记在回调里调用 wl.release()

这种问题在异步回调里特别常见。我建议你在 acquire() 之后,立刻用 try-finally 包起来:

wl.acquire();
try {
    // 你的业务逻辑
} finally {
    wl.release();
}

嗯,这看起来是基本功,但我在 code review 里至少见过 20 次遗漏。

13.2.2 动态运行时检测

光靠静态检查不够。有些锁是在特定条件下才 acquire 的,比如网络请求超时后。这时候我会用 dumpsys power 配合 dumpsys batterystats 做动态监控。

我写过一个简单的脚本,每隔 5 秒抓一次 dumpsys power,然后对比唤醒锁列表的变化。如果某个锁在 30 秒内一直存在且没有变化,基本就是泄漏了。

小技巧:在 acquire() 时传入一个唯一的 tag,比如带上文件名和行号。这样在 dumpsys 输出里一眼就能看出是哪个模块的问题。

13.3 Battery Historian:可视化你的耗电轨迹

说实话,dumpsys power 只能看到「当前」的状态。但电池耗电是个时间过程,你需要看「历史」。这时候 Battery Historian 就派上用场了。

13.3.1 数据采集

先重置统计,然后跑你的测试场景:

adb shell dumpsys batterystats --reset
// 跑你的测试,比如待机 2 小时
adb bugreport bugreport.zip

然后把 bugreport.zip 上传到 Battery Historian 的网页工具里。我个人习惯用 Docker 部署本地版本,这样数据不用出内网。

13.3.2 怎么看图

打开 Historian 后,你会看到一条时间轴。我重点关注这几个区域:

  • Wake Lock 行:看有没有长条形的色块。一个正常的唤醒锁应该是短脉冲,如果出现持续几分钟的色块,那就是泄漏。
  • Top 唤醒锁:右侧会列出持锁时间最长的几个锁。我一般先看这里,直接定位到「罪魁祸首」。
  • CPU 运行状态:如果 CPU 在屏幕关闭后还频繁醒来,那大概率是唤醒锁或 Alarm 导致的。

我记得有一次,一个社交 App 在后台每 30 秒就持锁一次,每次持续 2 秒。从 Historian 上看,CPU 几乎没停过。最后发现是它的心跳包机制写死了,没做合并和退避。

注意:Battery Historian 的数据是「事后分析」。它告诉你「发生了什么」,但不会告诉你「为什么发生」。根因定位还是得靠 dumpsys power 和代码审查。

13.4 实战:一个完整的排查流程

我拿一个真实案例给你串一遍。用户反馈手机待机一晚掉电 15%。我的排查步骤:

  1. 第一步:连 adb,跑 dumpsys power。发现 Wake Locks 列表里有一个 com.example.weather:update_lock,ref count 为 1,且 mWakefulnessAsleep。嗯,有嫌疑。
  2. 第二步:跑 dumpsys batterystats,看这个锁的累计持有时长。结果显示它持有了 6 小时 23 分钟。实锤了。
  3. 第三步:用 Battery Historian 看时间轴。发现这个锁在屏幕关闭后 5 分钟开始持有,一直持续到早上。CPU 在这段时间内一直处于浅睡眠状态。
  4. 第四步:反编译这个天气 App 的代码(或者看日志),发现它在后台更新天气数据时,acquire() 了锁,但网络请求超时后没有走 release() 分支。

修复方案很简单:在 finally 块里加 release()。但排查过程花了整整一个下午。你想想看,如果没有这些工具,光靠猜,得试到什么时候?

13.5 知识体系总览

下面这张图是我自己总结的唤醒锁调试知识体系,你可以把它当成一个检查清单:

唤醒锁调试与优化知识体系 dumpsys power • 查看当前唤醒锁列表 • 检查 mWakefulness 状态 • 分析 ref count 变化 • 定位持锁进程 • 对比前后快照 泄漏检测 • 静态代码审查 • try-finally 模式 • 动态监控脚本 • 超时自动释放 • 唯一 tag 命名 Battery Historian • 时间轴可视化 • 唤醒锁色块分析 • Top 持锁排行 • CPU 状态关联 • 历史趋势对比 三者配合使用:dumpsys 看现状 → 泄漏检测找根因 → Historian 看趋势

说白了,调试唤醒锁没有银弹。工具只是帮你缩小范围,最终定位还是得靠对代码逻辑的理解。我个人的经验是:先怀疑,再验证。不要轻易相信「这个锁肯定没问题」——我吃过太多这种亏了。


公众号:蓝海资料掘金营,微信deep3321