部分唤醒锁定操作卡住

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

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

检测问题

您有时可能并不知道应用的部分唤醒锁定卡住了。如果您已发布了应用,Android Vitals 可以帮助您认识到这个问题。

Android Vitals

当您的应用出现部分唤醒锁定卡住时,Android Vitals 可通过 Play 管理中心提醒您,从而帮助您改进应用性能。Android Vitals 报告部分唤醒锁定卡住的条件是在电池工作时段内后台至少发生了一次时长达 1 小时的部分唤醒锁定。

电池工作时段的定义取决于平台版本。

  • 在 Android 10 中,电池工作时段是在给定的 24 小时内接收到的所有电池报告的汇总。电池报告是指两次电池充电(从低于 20% 充到 80% 以上或者从任意电量值充满到 100%)间隔的时间。
  • 在 Android 11 中,电池工作时段是固定的 24 小时时间段。

显示的电池工作时段是针对应用的所有测量用户的汇总值。如需了解 Google Play 如何收集 Android Vitals 数据,请参阅 Play 管理中心文档。

当您发现应用存在过多的部分唤醒锁定卡住时,下一步就是解决问题。

解决问题

唤醒锁定是在 Android 平台的早期版本中引入的,但随着时间的推移,许多以前需要唤醒锁定的用例现在都改用像 WorkManager 这样的新 API,而且效果更好。

本部分包含有关解决唤醒锁定的提示,但从长远来看,请考虑迁移应用,以遵循最佳实践部分中的建议。

找出并修复代码中获取唤醒锁定的位置,例如对 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 bug 报告的输出解析为电源相关事件的可视化表示。

最佳实践

一般来说,您的应用应避免部分唤醒锁定,因为它太容易耗尽用户的电量。Android 为以前需要部分唤醒锁定的几乎所有用例提供备用 API。部分唤醒锁定的一个保留用例是确保音乐应用在屏幕处于关闭状态时继续播放音乐。如果您要使用唤醒锁定来运行任务,请考虑采用后台处理指南中介绍的替代方法。

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

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