Android 进阶——性能优化之电量优化全攻略及实战小结
Posted CrazyMo_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 进阶——性能优化之电量优化全攻略及实战小结相关的知识,希望对你有一定的参考价值。
文章大纲
引言
电池续航时间是移动用户体验中最重要的一个方面。没电的设备完全无法使用。因此,对于应用来说,尽可能地考虑电池续航时间是至关重要的。为使应用保持节能,前一篇Android 进阶——性能优化之电量优化全攻略及实战小结(一)对于一些概念进行了总结。
一、在低电耗模式和应用待机模式下进行测试
为确保用户获得良好的体验,您应在低电耗模式和应用待机模式下全面测试您的应用。
1、在低电耗模式下测试您的应用
您可以按以下步骤在低电耗模式下测试您的应用:
-
使用 android 6.0(API 级别 23)或更高版本的系统映像配置硬件设备或虚拟设备。
-
将设备连接到开发计算机并安装您的应用。
-
运行您的应用并使其保持活动状态。
-
运行以下命令,强制系统进入闲置模式:
$ adb shell dumpsys deviceidle force-idle
-
准备就绪后,运行以下命令,使系统退出闲置模式:
$ adb shell dumpsys deviceidle unforce
-
执行以下命令,重新激活设备:
$ adb shell dumpsys battery reset
-
在重新激活设备后观察应用的行为。确保应用在设备退出低电耗模式时正常恢复。
2、在应用待机模式下测试您的应用
如需在应用待机模式下测试您的应用,请执行以下操作:
-
使用 Android 6.0(API 级别 23)或更高版本的系统映像配置硬件设备或虚拟设备。
-
将设备连接到开发计算机并安装您的应用。
-
运行您的应用并使其保持活动状态。
-
运行以下命令,强制应用进入应用待机模式:
$ adb shell dumpsys battery unplug $ adb shell am set-inactive <packageName> true
-
使用以下命令模拟唤醒您的应用:
$ adb shell am set-inactive <packageName> false $ adb shell am get-inactive <packageName>
-
在唤醒应用后观察它的行为。确保应用从待机模式正常恢复。您应特别检查应用的通知和后台作业是否继续按预期运行。
3、列入白名单的可接受用例
下表重点介绍了请求将应用列入电池优化豁免白名单或应用目前在该白名单中的可接受用例。一般来说,除非低电耗模式或应用待机模式破坏了应用的核心功能,或者由于技术方面的原因而导致您的应用无法使用高优先级 FCM 消息,否则您的应用不应在白名单中。
4、确定当前充电状态
首先,确定当前充电状态。BatteryManager
会在一个包含充电状态的粘性 Intent
中广播所有电池和充电详情。
由于它是一个粘性 Intent,因此您并不需要如下一代码段中所示的那样通过简单地调用 registerReceiver
传入 null
作为接收器来注册 BroadcastReceiver
,便可返回当前电池状态 Intent。您可以在此处传入实际 BroadcastReceiver
对象,但由于稍后我们将会处理更新,因此并不需要这样做。
IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent batteryStatus = context.registerReceiver(null, ifilter);
您可以提取当前充电状态,并且如果设备正在充电,则还可以提取设备是通过 USB 还是交流充电器进行充电。
// Are we charging / charged?
int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
status == BatteryManager.BATTERY_STATUS_FULL;
// How are we charging?
int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
boolean usbCharge = chargePlug == BatteryManager.BATTERY_PLUGGED_USB;
boolean acCharge = chargePlug == BatteryManager.BATTERY_PLUGGED_AC;
通常,如果设备连接到交流充电器,您应最大限度地提高后台更新的频率;如果设备是通过 USB 充电,则应降低更新频率;如果电池正在放电,则应进一步降低更新频率。
5、监控充电状态变化
就像设备可以轻松地插入电源,充电状态也很容易发生变化,因此必须监控充电状态的变化并相应地改变刷新频率。
每当设备连接或断开电源时,BatteryManager
都会广播一项操作。请务必接收这些事件,即便您的应用并未运行 - 尤其要考虑到这些事件可能会影响您启动应用以便发起后台更新的频率 - 因此您应在清单中注册一个 BroadcastReceiver
,通过在一个 Intent 过滤器内定义 ACTION_POWER_CONNECTED
和 ACTION_POWER_DISCONNECTED
来同时监听这两种事件。
<receiver android:name=".PowerConnectionReceiver">
<intent-filter>
<action android:name="android.intent.action.ACTION_POWER_CONNECTED"/>
<action android:name="android.intent.action.ACTION_POWER_DISCONNECTED"/>
</intent-filter>
</receiver>
6、确定当前电池电量
在某些情况下,确定当前电池电量也很有用处。您可以选择在电池电量低于某一水平时降低后台更新的频率。
您可以通过从电池状态 intent 提取当前电池电量和刻度来了解当前电池电量,如下所示:
int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
float batteryPct = level * 100 / (float)scale;
7、监控显著的电池电量变化
您无法轻松地持续监控电池状态,您也不必如此。
一般而言,持续监控电池电量对电池的影响大于应用正常行为造成的影响,因此最好只监控显著的电池电量变化 - 特别是在设备进入或退出电量不足状态时。
以下清单代码段摘自某个广播接收器内的 Intent 过滤器元素。通过监听 ACTION_BATTERY_LOW
和 ACTION_BATTERY_OKAY
,每当设备电池电量不足或退出不足状态时,便会触发该接收器。
<receiver android:name=".BatteryLevelReceiver">
<intent-filter>
<action android:name="android.intent.action.BATTERY_LOW"/>
<action android:name="android.intent.action.BATTERY_OKAY"/>
</intent-filter>
</receiver>
一般而言,建议您在电量极低时停用所有后台更新。如果手机自行关机,您就无法利用相关数据,数据的新鲜度也就无关紧要。在许多情况下,为设备充电与将设备插入基座是同一操作。下一课将为您介绍如何确定当前基座状态以及如何监控设备插接状态的变化。
二、Wakelock 机制
Android为了确保应用程序中关键代码的正确执行,提供了Wake Lock(一种锁的机制, 只要有人拿着这个锁,系统就无法进入休眠)可以被用户态程序和内核获得。使得应用程序有权限通过代码阻止AP进入休眠状态。WakeLock是Android中为应用层及框架层提供的用来保证CPU处于唤醒状态的一种锁机制。PMS中为应用及框架层其他组件提供了接口,进行WakeLock的申请和释放。应用在申请WakeLock时,需要在清单文件中配置android.Manifest.permission.WAKE_LOCK
权限。如果没有锁了或者超时了, 内核就会启动休眠的那套机制来进入休眠。WakeLock阻止应用处理器(ApplicationProcessor)挂起,确保关键代码的运行,通过中断唤起应用处理器(ApplicationProcessor),可以阻止屏幕变暗。所有的WakeLock被释放后,系统会挂起。
1、WakeLock分类
根据作用时间,WakeLock可以分为永久锁和超时锁:
- 永久锁:只要获取了WakeLock锁,必须显式进行释放,否则系统会一直持有该锁;
- 超时锁:在到达给定时间后,自动释放WakeLock锁,其实现原理为方法内部维护了一个Handler进行。
根据释放原则,WakeLock可以分为计数锁和非计数锁:
- 计数锁:一次申请必须对应一次释放;
- 非计数锁:不管申请多少次,只需要一次就可以释放该WakeLock。
默认为计数锁。WakeLock机制从上到下架构如下:
WakeLock有三种表现形式:
- PowerManger.WakeLock:PMS暴露给应用层和其他组件用来申请WakeLock的接口;
- PowerManagerService.WakeLock: PowerManager.WakeLock在PMS中的表现形式;
- SuspendBlocker: PowerManagerService.WakeLock在向底层节点操作时的表现形式。
2、申请WakeLock锁
// 获取PowerManager对象
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
// 创建WakeLock锁实例
PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "My Tag");
// 申请WakeLock
wl.acquire();
// 释放WakeLock
wl.release();
在PowerManager中,共定义了以下七种WakeLock:
WakeLock | 说明 |
---|---|
PARTIAL_WAKE_LOCK | 0x00000001,保证CPU处于唤醒状态,但屏幕和键盘灯有可能是关闭的。 |
PROXIMITY_SCREEN_OFF_WAKE_LOCK | 0x00000020,通过PSensor进行亮灭屏工作,PSensor检测到有物体靠近时关闭屏幕,远离时又亮屏 |
DOZE_WAKE_LOCK | 0x00000040,仅用于PMS唤醒状态为Doze时,进入Doze状态后,DreamMangerService会申请该锁,允许CPU挂起 |
DRAW_WAKE_LOCK | 0x00000080,仅用于PMS唤醒状态为Doze时,保证CPU处于运行状态,以进行Doze状态下屏幕的绘制,如AOD、防烧屏显示 |
0x0000001a,保证屏幕、键盘灯都保持常亮状态,按Power键灭屏后,会忽略该锁 | |
0x0000000a,保证屏幕一直保持常亮状态,按Power键灭屏后,会忽略该锁(不推荐) | |
0x00000006,保持CPU运转,保证屏幕一直保持Dim状态但有可能是灰的,允许关闭键盘灯,按Power键灭屏后,会忽略该锁(不推荐) |
还定义了三个Flag,可以在创建、申请及释放WakeLock时和以上几类搭配使用:
Flag | 说明 |
---|---|
ACQUIRE_CAUSES_WAKEUP | 不会唤醒设备,强制屏幕马上高亮显示,键盘灯开启。有一个例外,如果有notification弹出的话,会唤醒设备。,不能和PARTIAL_WAKE_LOCK一起使用 |
ON_AFTER_RELEASE | 在释放有该Flag的WakeLock时,会稍微延长自动休眠时间一小会儿,但不能和PARTIAL_WAKE_LOCK一起使用,WakeLock被释放后,维持屏幕亮度一小段时间,减少WakeLock循环时的闪烁情况。 |
PROXIMITY_SCREEN_OFF_WAKE_LOCK | 在释放PROXIMITY_SCREEN_OFF_WAKE_LOCK锁时,不会立即解除PSensor监听,而是在PSensor上报远离后,才会亮屏并解除Psensor监听,仅用于释放PROXIMITY_SCREEN_OFF_WAKE_LOCK锁 |
3、获取WakeLock对象
PowerManager中提供了接口newWakeLock()
来创建WakeLock对象:
// frameworks/base/core/java/android/os/PowerManager.java
public WakeLock newWakeLock(int levelAndFlags, String tag)
validateWakeLockParameters(levelAndFlags, tag); // 校验Flag
// 创建WakeLock对象
return new WakeLock(levelAndFlags, tag, mContext.getOpPackageName());
首先进行了参数的校验,然后调用WakeLock构造方法创建WakeLock对象:
// frameworks/base/core/java/android/os/PowerManager.java
WakeLock(int flags, String tag, String packageName)
mFlags = flags; //表示wakelock类型
mTag = tag; //一个tag,一般为当前类名
mPackageName = packageName; //申请wakelock的应用包名
mToken = new Binder(); //一个Binder标记
mTraceName = "WakeLock (" + mTag + ")";
除以上几个属性之外,WakeLock中还有如下几个属性:
// frameworks/base/core/java/android/os/PowerManager.java
//表示内部计数
private int mInternalCount;
//表示外部计数
private int mExternalCount;
//表示是否是计数锁,默认true
private boolean mRefCounted = true;
//表示是否已经持有该锁
private boolean mHeld;
//表示和该wakelock相关联的工作源,这在一些服务获取wakelock时很有用,以便计算工作成本
private WorkSource mWorkSource;
//表示一个历史标签
private String mHistoryTag;
4、Wake Lock 申请流程
当创建好WakeLock对象以后,就可以申请WakeLock锁了。不管是永久锁还是超时锁,都是通过acquire()
方法来申请:
mWakeLock.acquire(); //申请一个永久锁
mWakeLock.acquire(int timeout); //申请一个超时锁,指定作用时间
复制代码
PowerManager#acquire()方法如下:
// frameworks/base/core/java/android/os/PowerManager.java
// 申请永久锁
public void acquire()
synchronized (mToken)
acquireLocked();
// 申请超时锁
public void acquire(long timeout)
synchronized (mToken)
acquireLocked();
//通过Handler设置一个延迟消息自动释放锁
mHandler.postDelayed(mReleaser, timeout);
复制代码
这两种申请方式完全一样,只不过如果是申请一个超时锁,会通过Handler发送一个延时消息,到达时间后去自动释放锁。继续看acquireLocked()
方法:
// frameworks/base/core/java/android/os/PowerManager.java
private void acquireLocked()
// 计数器+1
mInternalCount++;
mExternalCount++;
//如果是非计数锁或者内部计数值为1,即第一次申请该锁,才会真正去申请
if (!mRefCounted || mInternalCount == 1)
// 移除释放超时锁的Msg
mHandler.removeCallbacks(mReleaser);
try
// 通过Binder进入PMS中
mService.acquireWakeLock(mToken, mFlags, mTag, mPackageName, mWorkSource,
mHistoryTag);
catch (RemoteException e)
throw e.rethrowFromSystemServer();
mHeld = true; // 表示已持有该锁,申请成功
复制代码
对同一个WakeLock每申请一次,属性值mInternalCount和mExternalCount都会+1,这两个值都用来表示引用计数,前者相对于PowerManager内部,后者则相对于用户操作,之所以有两个引用计数器,主要是为了针对超时锁的释放,如果一个超时锁在已自动释放的情况下,用户手动再释放一次,相当于释放两次。这种情况下由于mExternalCount的存在,就不会导致crash。
mRefCounted用来表示计数锁或非计数锁,默认为true(计数锁),可以通过setReferenceCount()来设置:
public void setReferenceCounted(boolean value)
synchronized (mToken)
mRefCounted = value;
复制代码
mHeld
表示是否已经持有锁,可以通过调用isHeld()
来判断是已申请WakeLock。
以上逻辑都是在APP进程执行的,接下来通过mService进入到system_server,开始执行了PMS中的流程。直接来看PMS#acquireWakeLockInternal()方法:
// frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java
private void acquireWakeLockInternal(IBinder lock, int flags, String tag, String packageName,
WorkSource ws, String historyTag, int uid, int pid)
synchronized (mLock)
// 这是PMS中的WakeLock类
WakeLock wakeLock;
// 通过IBinder标记确认是否已申请该WakeLock
int index = findWakeLockIndexLocked(lock);
boolean notifyAcquire;
// 说明已申请过该WakeLock,则更新下该WakeLock即可
if (index >= 0)
wakeLock = mWakeLocks.get(index);
if (!wakeLock.hasSameProperties(flags, tag, ws, uid, pid))
notifyWakeLockChangingLocked(wakeLock, flags, tag, packageName,
uid, pid, ws, historyTag);
wakeLock.updateProperties(flags, tag, packageName, ws, historyTag, uid, pid);
notifyAcquire = false;
else // 说明没有申请过该WakeLock
......
// 创建一个WakeLock
wakeLock = new WakeLock(lock, flags, tag, packageName, ws, historyTag, uid, pid,
state);
// .......
// 添加到保存系统所有WakeLock的list中
mWakeLocks.add(wakeLock);
// 对于PowerManager.PARTIAL_WAKE_LOCK类型锁,省电机制Doze模式会对其进行disable处理
setWakeLockDisabledStateLocked(wakeLock);
notifyAcquire = true;
// 处理PowerManager.ACQUIRE_CAUSES_WAKEUP标记,带有此标记,进行亮屏处理
applyWakeLockFlagsOnAcquireLocked(wakeLock, uid);
mDirty |= DIRTY_WAKE_LOCKS; // 设置DIRTY_WAKE_LOCKS标记位
updatePowerStateLocked(); // 更新状态
if (notifyAcquire)
// 将申请WakeLock动作通知其他组件
notifyWakeLockAcquiredLocked(wakeLock);
复制代码
首先,通过传入的第一个参数IBinder进行查找WakeLock是否已经存在,若存在,则在原有的WakeLock上更新其属性值;若不存在,则创建一个WakeLock对象,同时将该WakeLock保存到List中,并将相关数据保存到UidState中。
4.1、setWakeLockDisabledStateLocked()
创建WakeLock实例后,接下来调用setWakeLockDisabledStateLocked()
方法,这个方法会对PARTIAL_WAKE_LOCK类型的WakeLock进行disable。系统如果持有该类型锁,会导致CPU一直保持唤醒状态而无法休眠,因此在省电策略DeviceIdle模块中,会在某些特定状态下将该类型的锁进行diable:
// frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java
private boolean setWakeLockDisabledStateLocked(WakeLock wakeLock)
if ((wakeLock.mFlags & PowerManager.WAKE_LOCK_LEVEL_MASK)
== PowerManager.PARTIAL_WAKE_LOCK)
boolean disabled = false;
final int appid = UserHandle.getAppId(wakeLock.mOwnerUid);
// 非系统进程
if (appid >= Process.FIRST_APPLICATION_UID)
// Cached inactive processes are never allowed to hold wake locks.
if (mConstants.NO_CACHED_WAKE_LOCKS)
// 强制进入suspend、对应uid进程没有处于active且进程adj大于PROCESS_STATE_RECEIVER
disabled = mForceSuspendActive // 强制进入suspend
|| (!wakeLock.mUidState.mActive && wakeLock.mUidState.mProcState
!= ActivityManager.PROCESS_STATE_NONEXISTENT &&
wakeLock.mUidState.mProcState > ActivityManager.PROCESS_STATE_RECEIVER);
if (mDeviceIdleMode) //处于idle状态时,将非白名单应用wakeLock 禁用
final UidState state = wakeLock.mUidState;
if (Arrays.binarySearch(mDeviceIdleWhitelist, appid) < 0 &&
Arrays.binarySearch(mDeviceIdleTempWhitelist, appid) < 0 &&
state.mProcState != ActivityManager.PROCESS_STATE_NONEXISTENT &&
state.mProcState >
ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE)
disabled = true;
// 更新mDisabled属性
if (wakeLock.mDisabled != disabled)
wakeLock.mDisabled = disabled;
return true;
return false;
复制代码
主要有三种情况下会禁用Partical WakeLock:
- 强制进入suspend;
- WakeLock所属进程不处于active状态,且进程adj大于PROCESS_STATE_RECEIVER;
- DeviceIdle处于IDLE状态,且所属进程不在doze白名单中;
4.2、applyWakeLockFlagsOnAcquireLocked()处理亮屏标记
接下来调用applyWakeLockFlagsOnAcquireLocked()
方法,对ACQUIRE_CAUSES_WAKEUP
标记进行处理。如果WakeLock带有标志,并且WakeLock类型为FULL_WAKE_LOCK、SCREEN_BRIGHT_WAKE_LOCK、SCREEN_DIM_WAKE_LOCK这三种其中之一,则会点亮屏幕:
// frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java
private void applyWakeLockFlagsOnAcquireLocked(WakeLock wakeLock, int uid)
// 如果持有ACQUIRE_CAUSES_WAKEUP标记,且为亮屏相关三类锁之一
if ((wakeLock.mFlags & PowerManager.ACQUIRE_CAUSES_WAKEUP) != 0
&& isScreenLock(wakeLock))
......
// 亮屏流程
wakeUpNoUpdateLocked(SystemClock.uptimeMillis(),
PowerManager.WAKE_REASON_APPLICATION, wakeLock.mTag,
opUid, opPackageName, opUid);
复制代码
wakeUpNoUpdateLocked()
方法是点亮屏幕的主要方法,会在后面部分分析。
4.3、updatePowerStateLocked()更新全局状态
这个方法在PowerManagerService模块(一) 启动流程和核心方法中进行了分析,其中涉及到WakeLock流程的有两个方法:updateWakeLockSummaryLocked()和updateSuspendBlockerLocked()方法,前者已经分析过了,用来将所有的WakeLock统计到mWakeLockSummary全局变量中,这里对后一个方法进行分析。
4.4、updateSuspendBlockerLocked()更新SuspendBlocker
在这个方法中,将会根据系统所有WakeLock的状态,获得一个SuspendBlocker锁:
// frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java
private void updateSuspendBlockerLocked()
// 是否因持有WakeLock锁而需要CPU保持唤醒
final boolean needWakeLockSuspendBlocker = ((mWakeLockSummary & WAKE_LOCK_CPU) != 0);
// 是否因Display状态而需要CPU保持唤醒
final boolean needDisplaySuspendBlocker = needDisplaySuspendBlockerLocked();
// 是否开启auto_suspend模式
final boolean autoSuspend = !needDisplaySuspendBlocker;
// 是否处于交互状态
final boolean interactive = mDisplayPowerRequest.isBrightOrDim();
// 如果持有Display SuspendBlocker,则关闭auto-suspend模式
if (!autoSuspend && mDecoupleHalAutoSuspendModeFromDisplayConfig)
setHalAutoSuspendModeLocked(false);
// 申请mWakeLockSuspendBlocker锁
if (needWakeLockSuspendBlocker && !mHoldingWakeLockSuspendBlocker)
mWakeLockSuspendBlocker.acquire();
mHoldingWakeLockSuspendBlocker = true;
// 申请mDisplaySuspendBlocker锁
if (needDisplaySuspendBlocker && !mHoldingDisplaySuspendBlocker)
mDisplaySuspendBlocker.acquire();
mHoldingDisplaySuspendBlocker = true;
// 设置交互状态
if (mDecoupleHalInteractiveModeFromDisplayConfig)
if (interactive || mDisplayReady) Android 进阶——性能优化之电量优化全攻略及实战小结
Android 进阶——性能优化之Bitmap位图内存管理及优化概述