卡住的部分唤醒锁

部分唤醒锁是 PowerManager API 中的一种机制,它允许开发者在设备屏幕关闭后(无论是由于系统超时还是用户按下电源按钮)保持 CPU 运行。您的应用通过调用 acquire() 并使用 PARTIAL_WAKE_LOCK 标记来获取部分唤醒锁。如果在您的应用在后台运行(应用的任何部分对用户不可见)时长时间持有部分唤醒锁,则该唤醒锁会卡住。这种情况会消耗设备的电池电量,因为它会阻止设备进入低功耗状态。部分唤醒锁应仅在必要时使用,并在不再需要时立即释放。

如果您的应用存在卡住的部分唤醒锁,您可以使用此页面中的指南来诊断和解决问题。

检测问题

您可能并不总是知道您的应用的部分唤醒锁是否卡住了。如果您已发布您的应用,Android 核心指标可以帮助您了解问题。

Android 核心指标

当您的应用出现卡住的部分唤醒锁时,Android 核心指标可以通过 Play 控制台 向您发出警报,从而帮助您提高应用的性能。当在电池使用会话中发生至少一个持续一小时且在后台运行的部分唤醒锁时,Android 核心指标会将部分唤醒锁报告为卡住状态。

“电池会话”的定义取决于平台版本。

  • 在 Android 10 中,电池会话是特定 24 小时内接收到的所有电池报告的聚合。电池报告指的是两次电池充电之间的时间间隔,无论是从低于 20% 充电到高于 80%,还是从任何充电水平充电到 100%。
  • 在 Android 11 中,电池会话是固定的 24 小时周期。

显示的电池会话数量是应用所有已测用户数据的聚合。有关 Google Play 如何收集 Android 核心指标数据的信息,请参阅Play Console 文档。

一旦您意识到您的应用存在过多的卡住部分唤醒锁,下一步就是解决此问题。

修复问题

唤醒锁是在早期版本的 Android 平台中引入的,但随着时间的推移,许多以前需要唤醒锁的用例现在可以通过更新的 API(如WorkManager)更好地满足。

本节包含修复唤醒锁的技巧,但从长远来看,请考虑迁移您的应用以遵循最佳实践部分中的建议。

识别并修复代码中获取唤醒锁的位置,例如对newWakeLock(int, String)WakefulBroadcastReceiver 子类的调用。以下是一些提示

  • 我们建议在唤醒锁标签名称中包含您的包、类或方法名称,以便您可以轻松识别源代码中创建唤醒锁的位置。以下是一些其他提示
    • 在名称中省略任何个人身份信息 (PII),例如电子邮件地址。否则,设备日志将显示_UNKNOWN 而不是唤醒锁名称。
    • 不要以编程方式获取类或方法名称,例如通过调用getName(),因为它可能会被 Proguard 混淆。而是使用硬编码字符串。
    • 不要向唤醒锁标签添加计数器或唯一标识符。系统将无法聚合由相同方法创建的唤醒锁,因为它们都具有唯一的标识符。
  • 确保您的代码释放所有获取的唤醒锁。这比确保每个对acquire() 的调用都有一个相应的对release() 的调用更复杂。以下是一个由于未捕获的异常而未释放的唤醒锁示例

    Kotlin

    @Throws(MyException::class)
    fun doSomethingAndRelease() {
        wakeLock.apply {
            acquire()
            doSomethingThatThrows()
            release()  // does not run if an exception is thrown
        }
    }

    Java

        void doSomethingAndRelease() throws MyException {
            wakeLock.acquire();
            doSomethingThatThrows();
            wakeLock.release();  // does not run if an exception is thrown
        }

    以下是代码的正确版本

    Kotlin

    @Throws(MyException::class)
    fun doSomethingAndRelease() {
        wakeLock.apply {
            try {
                acquire()
                doSomethingThatThrows()
            } finally {
                release()
            }
        }
    }

    Java

        void doSomethingAndRelease() throws MyException {
            try {
                wakeLock.acquire();
                doSomethingThatThrows();
            } finally {
                wakeLock.release();
            }
        }
  • 确保在不再需要时立即释放唤醒锁。例如,如果您使用唤醒锁来允许后台任务完成,请确保在该任务完成后释放唤醒锁。如果唤醒锁保持的时间超过预期而未释放,则可能意味着您的后台任务花费的时间超过预期。

在修复代码中的问题后,请使用以下 Android 工具验证您的应用是否正确释放了唤醒锁

  • dumpsys - 一个提供有关设备上系统服务状态信息的工具。要查看电源服务的状态(包括唤醒锁列表),请运行adb shell dumpsys power

  • Battery Historian - 一个将 Android 错误报告 的输出解析为与电源相关的事件的可视化表示的工具。

最佳实践

通常,您的应用应避免使用部分唤醒锁,因为这很容易耗尽用户的电池电量。Android 为几乎所有以前需要部分唤醒锁的用例提供了替代 API。部分唤醒锁的一个剩余用例是确保音乐应用在屏幕关闭时继续播放。如果您使用唤醒锁来运行任务,请考虑后台处理指南中描述的替代方案。

如果必须使用部分唤醒锁,请遵循以下建议

  • 确保您的应用的某些部分保持在前台。例如,如果您需要运行服务,请启动前台服务。这会直观地向用户指示您的应用仍在运行。
  • 确保获取和释放唤醒锁的逻辑尽可能简单。当您的唤醒锁逻辑与复杂的有限状态机、超时、执行程序池和/或回调事件相关联时,该逻辑中的任何细微错误都可能导致唤醒锁保持的时间超过预期。这些错误很难诊断和调试。