唤醒锁: 检测 Android* 应用中的 No-Sleep(无法进入睡眠)问题

摘要

如果 Android* 应用使用唤醒锁不当,将会显著增加电池耗电量。 在本文中,我们将介绍一些提示和技巧,帮助您了解如何确认与误用唤醒锁有关的 No Sleep 漏洞。

1. 介绍
2. 唤醒锁
2.1. 唤醒锁简介
2.2. Android 用户唤醒锁
2.3. Android 内核唤醒锁
2.4. No-Sleep 漏洞
3. 找出 No Sleep 漏洞
3.1. 使用 adb
3.2. 使用 BetterBatteryStats 应用
4. 测试案例
5. 结论
6. 参考文献

1. 介绍

限制电池耗电量对智能手机非常有必要。 为了获得最大的自主性,Android 的操作系统设计可在检测到系统上无用户活动时进入睡眠模式。 一些应用需要设备保持开启状态 — 即使长时间无用户操作。 比如,看视频、听音乐、使用 GPS 以及玩游戏。 Android 可为操作系统或应用提供了这样的机制,以确保设备保持唤醒状态。 该机制称为唤醒锁。 如欲了解其他信息,请阅读 Christopher Bird 的文章: “适用于 Android 的唤醒锁”。

这种机制的出现让管理组件活动的责任落到应用开发人员的身上。 如果使用错误,应用可能会大量消耗电池电量 — 即使应用并未在前台运行。

2. 唤醒锁

2.1.唤醒锁简介

唤醒锁是一种控制主机设备电源状态的软件机制。 操作系统可导出明确的电源管理句柄和 API,以指定某个组件何时需要保持开启或唤醒状态,直至其从任务中被明确释放。

唤醒锁机制可在两个层面上实施: 用户和内核。 下图展示了 Android 唤醒锁实施的内部设计。 用户唤醒锁可被高层面的操作系统服务或应用采用,并通过电源管理服务提供。 它支持应用控制设备的电源状态。 内核唤醒锁由操作系统内核或驱动程序控制。 用户唤醒锁被映射至内核唤醒锁。 任何活动的内核层面唤醒锁都可阻止系统在 ACPI S3 状态挂起(在 RAM 挂起)— 它是移动设备最节能的状态。

2.2. Android 用户唤醒锁

Android 架构通过 PowerManager 导出唤醒锁机制。唤醒锁可划分为并识别四种用户唤醒锁:

标记值CPU屏幕键盘

PARTIAL_WAKE_LOCK

开启

关闭

关闭

SCREEN_DIM_WAKE_LOCK

开启

变暗

关闭

SCREEN_BRIGHT_WAKE_LOCK

开启

变亮

关闭

FULL_WAKE_LOCK

开启

变亮

变亮

请注意,自 API 等级 17 开始,FULL_WAKE_LOCK 将被弃用。 应用应使用 FLAG_KEEP_SCREEN_ON。

可以使用唤醒锁强迫一些组件(CPU、屏幕和键盘)保持唤醒状态。

请了解有关 PARTIAL_WAKE_LOCK 的特别提醒:无论任何显示器超时或屏幕处于任何状态,CPU 都将继续运行 — 即使用户按下电源按钮。 这可能会导致出现静默耗电,即手机看上去处于待机模式(屏幕关闭),但是实际上处于完全唤醒状态。

在其他唤醒锁中,用户仍可使用电源按钮让设备进入睡眠状态。 按下电源按钮后,除局部唤醒锁外所有唤醒锁均将完全释放。

上述即为应用控制唤醒锁的方法。 基本而言,它是一个获取/释放机制。 当应用需要让一些组件保持开启状态时,它便会获取唤醒锁。 当不再需要这些组件处于开启状态时,则需要将唤醒锁释放。

PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "My Tag");
wl.acquire();
..screen will stay on during this section..
wl.release();

2.3. Android 内核唤醒锁

内核唤醒锁是由内核控制的低级唤醒锁。 它们可从内核内部获取/释放。 就此而言,应用开发人员对它们的直接控制更少,但是应用的运行状况可以间接触发这些唤醒锁并在无意中增加电池的耗电量。

下面是内核唤醒锁的示例。

Wlan_rx: 当通过 Wi-Fi* 发送或接收数据时由内核控制。

PowerManagerService: 是适用于所有局部唤醒锁的容器。

Sync: 在同步流程运行时启用。

Alarm_rtc: 控制告警(当应用或流程执行定期检查时使用)。

Main: 保持内核处于唤醒状态。 系统进入挂起模式时,这是最后一个被释放的唤醒锁。

2.4. No-Sleep 漏洞

应用必须在某一时刻释放它需要的每个唤醒锁,以便允许系统返回深睡眠模式。 如果该唤醒锁一直未被释放,则该获取/释放机制可能会导致出现漏洞。 即使启用了唤醒锁的应用停止在前台运行,唤醒锁仍在使用中。 当使用释放调用明确释放唤醒锁或应用被终止(强制关闭)时,唤醒锁才可被释放。 让一些唤醒锁处于启用状态将阻止系统进入深睡眠模式 — 即使没有活动大量增加耗电量和静默减少电池的自主性。 这就是 No-Sleep 漏洞。 由于 Android 的事件驱动特性,开发人员可能无法想到其应用获取并需要关闭的唤醒锁的所有代码路径。 这种漏洞类型称为 No-Sleep 漏洞代码路径。

发生这种类型的漏洞的另一种情况是,唤醒锁未有效获取而被释放。 在不同的线程中获取和释放唤醒锁的多线程代码中可能会出现这种情况。 这就是 No-Sleep 漏洞竞态条件。

最后一个问题是 No-sleep 扩张,其中获取唤醒锁的时间比实际所需的时间长。

为何重点指出这些问题? 根据 P. Vekris 的研究: “328 个使用唤醒锁的应用中,55% 的应用未遵循我们针对 no-sleep 漏洞提供的策略”[2012]。 一些主要的应用在出现 No-Sleep 漏洞时被释放。 因此,开发人员需要意识到这一点,以便以最优的方式运行其应用。

3. 找出 No-Sleep 漏洞

您可以通过两种方式解决 no sleep 漏洞: 静态的代码路径扫描分析和动态的运行时分析。 在本文中,我们主要介绍运行时分析。

这种方法无法保证您找到应用中所有的唤醒锁漏洞。 但是,它可以帮助您找到在运行时期间出现的唤醒锁问题。 如要找出唤醒锁问题,您需要按照未释放唤醒锁的代码路径执行。 测试一个应用是否解决了 no sleep 漏洞包括在应用的不同位置操作应用,尝试以不同的方式从应用中退出,以确认唤醒锁是否仍然存在。

在某些情况下,有必要阻止系统进入深睡眠状态 — 即使应用停止在前台运行。 应用可能需要在后台执行一项任务。 此时即为这种情况,例如,如果应用需要长时间下载: 视频或游戏数据集。 在进行下载时,用户可能让应用在后台运行,但是在下载完成前,手机应保持唤醒状态。 在这种情况下,在下载完成前,应用应一直启用唤醒锁。 当然,您需要确保在某个点释放唤醒锁。 例如,当手机在下载期间断开网络,如果无法再执行操作,手机则无需保持唤醒状态。

总之,找出 No Sleep 漏洞与所在的环境关系密切。 没有预定义的标准可让您轻松找出该问题。 只能依靠常识找到上述的漏洞。

3.1. 使用 adb

shell 命令是查看唤醒锁最简单的工具。

如要完整了解内核唤醒锁,请输入:

adb shell cat /proc/wakelocks

namecountexpire_countwake_countactive_sincetotal_time
"PowerManagerService"1502000337817677431
"main"15000984265842688
"alarm"151207920217778251643
"radio-interface"1600016676538930
"alarm_rtc"8044001204136324759
"gps-lock"100010753659786
namesleep_timemax_timelast_change
"PowerManagerService"957294091221409216636679723417252748
"main"02124247323559498127170228
"alarm"2176173620473579769419723371461242
"radio-interface"016593284969486387144974
"alarm_rtc"1200253446201660829365019483176054624
"gps-lock"01075365978637632803440

对于使用内核 3.4 或更高版本的映像,请使用“adb shell cat /sys/kernel/debug/wakeup_sources”。 虽然该格式可提供全部信息,但是不太适合用户使用。 我们在下面介绍的工具更方便。

使用“adb shell dumpsys power”可以轻松查看特定的应用。 以下是该命令典型的输出方式。 您可以看到,在命令发布后,用户唤醒锁将以红色呈现。 该命令可以查看系统中呈现的用户唤醒锁。

Power Manager State:
mIsPowered=true mPowerState=3 mScreenOffTime=1618305 ms
mPartialCount=3
mWakeLockState=SCREEN_ON_BIT
mUserState=SCREEN_BRIGHT_BIT SCREEN_ON_BIT
mPowerState=SCREEN_BRIGHT_BIT SCREEN_ON_BIT
mLocks.gather=SCREEN_ON_BIT
mNextTimeout=2382037 now=2378097 3s from now
mDimScreen=true mStayOnConditions=3 mPreparingForScreenOn=false mSkippedScreenOn=false
mScreenOffReason=0 mUserState=3
mBroadcastQueue={-1,-1,-1}
mBroadcastWhy={0,0,0}
mPokey=0 mPokeAwakeonSet=false
mKeyboardVisible=false mUserActivityAllowed=true
mKeylightDelay=6000 mDimDelay=587000 mScreenOffDelay=7000
mPreventScreenOn=false mScreenBrightnessOverride=-1 mButtonBrightnessOverride=-1
mScreenOffTimeoutSetting=600000 mMaximumScreenOffTimeout=2147483647
mLastScreenOnTime=27380
mBroadcastWakeLock=UnsynchronizedWakeLock(mFlags=0x1 mCount=0 mHeld=false)
mStayOnWhilePluggedInScreenDimLock=UnsynchronizedWakeLock(mFlags=0x6 mCount=0 mHeld=true)
mStayOnWhilePluggedInPartialLock=UnsynchronizedWakeLock(mFlags=0x1 mCount=0 mHeld=true)
mPreventScreenOnPartialLock=UnsynchronizedWakeLock(mFlags=0x1 mCount=0 mHeld=false)
mProximityPartialLock=UnsynchronizedWakeLock(mFlags=0x1 mCount=0 mHeld=false)
mProximityWakeLockCount=0
mProximitySensorEnabled=false
mProximitySensorActive=false
mProximityPendingValue=-1
mLastProximityEventTime=0
mLightSensorEnabled=true mLightSensorAdjustSetting=0.0
mLightSensorValue=11.0 mLightSensorPendingValue=10.0
mHighestLightSensorValue=47 mWaitingForFirstLightSensor=false
mLightSensorPendingDecrease=false mLightSensorPendingIncrease=false
mLightSensorScreenBrightness=42 mLightSensorButtonBrightness=0 mLightSensorKeyboardBrightness=0
mUseSoftwareAutoBrightness=true
mAutoBrightessEnabled=true
creenBrightnessAnimator:
animating: start:42, end:42, duration:480, current:42
startSensorValue:47 endSensorValue:11
startTimeMillis:2361638 now:2378092
currentMask:SCREEN_BRIGHT_BIT
mLocks.size=4:
SCREEN_DIM_WAKE_LOCK          'StayOnWhilePluggedIn_Screen_Dim' activated (minState=1, uid=1000, pid=388)
PARTIAL_WAKE_LOCK             'StayOnWhilePluggedIn_Partial' activated (minState=0, uid=1000, pid=388)
PARTIAL_WAKE_LOCK             'HDA_PARTIAL_WAKE_LOCK' activated (minState=0, uid=10046, pid=4690)
PARTIAL_WAKE_LOCK             'AudioOut_2' activated (minState=0, uid=1013, pid=157)
mPokeLocks.size=0:

如要确认应用进入后台后是否还有唤醒锁启用,您可以按照下列流程操作:

1. 将设备连接至 USB。
2. 启用应用并在该应用上操作。
3. 按电源按钮进入睡眠模式或以某种方式退出应用。
4. 等待 20 秒钟。
5. 以命令行的方式输入下列指令:
> adb shell dumpsys power
6. 查看是否有 PARTIAL_WAKE_LOCKs 与此相同,例如:
PARTIAL_WAKE_LOCK       ‘AudioOut_2’ activated(minState=0, uid=1013, pid=157)
7.每隔 15 秒钟重复一次第 5 步,共重复 3 至 5 次。 如果结果相同,可能出现了问题。

3.2. 使用 BetterBatteryStats 应用

BetterBatteryStats* (https://play.google.com/store/apps/details?id=com.asksven.betterbatterystats&hl=en) 是 Sven Knispel 开发的一款 Android 应用,可在 Google Play 上购买。 它可以收集信息,帮助您发现耗电问题,尤其是唤醒锁问题。

首先,通过选择 “Other”条目,您可以查看深睡眠和唤醒模式与总时间。 理想状态下,大多数情况,如果手机未使用则应处于深睡眠状态。

此外,您还可对比唤醒时间与屏幕开启时间,以了解何时为实际活动状态。 正常情况下,屏幕开启时间和唤醒时间应处于关闭状态。

您可以查看各时间的电池充电评估和唤醒、屏幕开启和 Wi-Fi* 状态。

然后,您可以查看内核唤醒锁。 您可以查看每种内核唤醒锁花费的时间及数量。 时间长或数量多可能代表出现了问题。 在该报告中,您无法找出是哪一应用或流程导致热点出现,但是可以发现特定应用触发的运行状况。

在内核唤醒锁报告中,“PowerManagerService”可汇总用户唤醒锁中花费的时间。 如果该命令行显示了热点,您可以通过查看局部唤醒锁报告找出。

大多数情况下,局部唤醒锁可指出控制它的应用。 找到导致问题出现的元凶会有很大的帮助。 但是,有时,一些活动可能通过其他应用启动。 例如,游戏可能会通过分配给 Android 媒体库的 AudioOut 声道播放声音。 如果未正确编码,您可能会认为未关闭声道是由于游戏出现问题, 而不会认为是 Android Gallery 出现问题。

AlarmManager 可能会提示告警导致唤醒出现,或某个应用做了大量的告警修改。 您可能希望查看“告警”部分,但是只有在拥有根权限的映像上才能进行查看。

此外,网络接入也仅支持有根权限的映像。 如果您察觉到网络上出现较高的流量可能会有帮助。 multipdp/svnet-dormancy 内核唤醒锁可能指示您还有一些较高的网络使用率。

4. 测试案例

让我们看一个使用游戏的真实案例。 启动游戏,玩 5 分钟左右,然后以非常规的方式从游戏中退出。 在我们的案例中,我们通过按主页键强制退出。 音乐停止,主页界面出现。 用户看来,一切都正常。 停止活动几分钟后,屏幕正常变黑。 让手机保持该状态约半个小时的时间,然后使用 BestBatteryStats 来检查它。 在“Other”界面上,您可以看到,虽然屏幕未开启,但是手机仍然处于唤醒状态。 此外,您还可以看到电池耗电率为 8.5%/小时。 因此在这种状态下,充满电的手机持续使用时间也不会超过 12 个小时。

现在,我们来看一下内核唤醒锁。 我们可以看到,虽然没有活动,但是两个内核唤醒锁仍然保持唤醒状态。 其中一个是 PowerManagerService,这意味着有用户局部唤醒锁开启;另一个是 AudioOutLock 等待锁。 我们来看一下局部唤醒锁界面。

在局部唤醒锁界面上,我们可以看到媒体库应用中的音频通道仍然打开。 这很奇怪,因为用户未明确使用媒体库应用。 实际上,游戏启动了媒体库,以便播放游戏的音乐。 开发人员忘记了在主页按钮中断应用时关闭音频通道。 开发人员应将这种情况考虑在内,并相应地对应用进行修改。

5. 结论

唤醒锁是非常有用且强大的工具。 但是如果使用不当,它们可能会对设备的电池寿命产生非常不好的影响,从而极大地影响用户体验。 对于开发人员来讲,应确保在 QA 过程中其代码未导致任何 No Sleep 漏洞出现。 他们应考虑使用可用的工具对实际使用中其应用对电池的影响进行分析,并尽量降低其对用户设备的影响。

6. 参考文献

有关编译器优化的更完整信息,请参阅优化通知