卡住的部分唤醒锁

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

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

检测问题

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

Android 核心指标

当您的应用出现卡住的部分唤醒锁时,Android 核心指标可以通过 Play Console 向您发出警报,从而帮助您提高应用的性能。当在后台运行至少 1 个小时的部分唤醒锁在一个电池使用周期中出现时,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。部分唤醒锁的一个剩余用例是确保音乐应用在屏幕关闭时继续播放。如果您使用唤醒锁来运行任务,请考虑后台处理指南中描述的替代方案。

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

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