Android 性能优化 - Battery 篇

1,069 阅读11分钟

前言

本篇文章是udacity上的Android Performace系列视频-Battery篇的课程纪要。

这个系列是视频是Google和udacity合作推出的视频,在Google的Android Performace Pattern系列视频的基础上加上了练习的章节,同时还有有趣的过场情节,非常不错的视频。

课程地址:

cn.udacity.com/course/andr…

目录

  • 电量消耗简介
  • Batter Historian(电池历史)
  • Track Battery Status using BatteryManager
  • Wakelock and Battery Drain
  • Network and Battery Drain
  • JobSchedule API
  • 总结
  • 写在最后

电量消耗简介

不同硬件的耗电量是不同的,同一个硬件在不同的状态下的耗电量也是不同的。

以Nexus 5为例,如果开启飞行模式不进行任何操作,电池大概能撑到一个月。我们可以把这个当成电池寿命的基准线。一旦活跃使用设备,就会消耗更多电量。这里的“活跃”包括使用CPU、蜂窝无线数据交换、屏幕保持唤醒状态等。

首先我们需要了解硬件的耗电特性和原理才能在写应用时去规避耗电的操作。监测硬件方面的耗电量是个矛盾的问题,因为测量电量并记录本身就是个耗电的过程,所以设备一般都没有这个功能。检测电量消耗最好的方式是再接入一个第三方硬件到安卓设备上,它负责进行记录但不使用手机的电量。

下面我们来看一下获取到的一些数据信息:

当Nexus 5处于待机模式(standby mode),几乎不消耗电量。

一旦使用或点亮屏幕时。使用液晶屏,打开屏幕,所有的工作CPU、GPU都会唤醒屏幕,这都会消耗电量。所以会有一个耗电峰值。

如果设备是被应用唤醒的情况下,比如使用wake lock、alarm manager或其他job scheduler API。在第一次唤醒是,会有一次耗电高峰,接下来是具体执行的消耗。

值得注意的是,当工作完成后,设备会主动进行休眠。这一点非常重要,在不使用或很少使用的情况下,长时间保持屏幕唤醒会迅速消耗电池电量。

另一方面,蜂窝式无线是非常耗电的。当使用蜂窝网发送数据,首先会出现一个唤醒耗电高峰;接下来还有一个高数值,这个发送数据包消耗的电量;然后接收数据包也要消耗大量的电量;开启无线模式这个过程非常耗电,所以完成执行工作后,它会在一小段时间内保持开启模式,防止短时间内还有数据包需要接收。关于蜂窝网络能耗状态的状态机,可以参考my.oschina.net/u/3026396/b…

以上的数据非常有用,可以帮助我们在写程序时作为参考。同时从Android L(5.0)开始,我们可以使用一个系统工具Battery Historian来分析并优化应用的耗电。

Battery Historian(电池历史)

Battery Historian可以帮助我们收集数据,更好地了解应用程序对电量的使用。基本上是使用ADB来从手机上获取数据,然后使用Battery Historian工具将这些数据转化成HTML表格,在浏览器中打开阅读。下面就让我们来看下Battery Historian的使用方法:

1. Battery Historian是一个独立的Python开源脚本historian.py,我们需要从GitHub上下载。github.com/google/batt…

2. 把手机与电脑连接,打开USB调试。

3. 打开终端,分别执行

  • adb kill-server:这一步很重要,因为当我们开发时,在做电量记录时,会打开很多可能造成冲突的东西。保险起见我们要清除ADB。
  • adb devices
  • adb shell dumpsys batterystats --reset:重置电池数据收集,从空白状态开始记录。
  • 将手机从电脑上拔出,使用手机电池进行我们需要的操作。
  • 操作完后重新将电脑与手机连接。
  • adb shell dumpsys batterystats > xxx.txt:得到整个设备的电量消耗信息。
  • adb shell dumpsys batterystats > com.package.xxx > xxx.txt:得到指定app的电量消耗信息。
  • python historian.py xxx.txt > xxx_battery_stats.html:将信息转化成可读性强的html文件。
  • 用浏览器打开html文件即可得到类似于下图的表格

需要注意的是,Battery Historian不会记录某个活动具体的耗电量,而是记录某个活动的好点时长和频率。

 

关于Battery Historian更详细的介绍,可以参考:

Batterystats & Battery Historian Walkthrough

Reading Battery Historian Charts

Track Battery Status Using Battery Manager

我们可以通过下面的代码来获取手机的当前充电状态:

// It is very easy to subscribe to changes to the battery state, but you can get the current
// state by simply passing null in as your receiver.  Nifty, isn't that?
IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent batteryStatus = this.registerReceiver(null, filter);
int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
boolean acCharge = (chargePlug == BatteryManager.BATTERY_PLUGGED_AC);
if (acCharge) {
    Log.v(LOG_TAG,“The phone is charging!”);
}

在上面的例子演示了如何立即获取到手机的充电状态,得到充电状态信息之后,我们可以有针对性的对部分代码做优化。比如我们可以判断只有当前手机为AC充电状态时 才去执行一些非常耗电的操作:

以下的代码片段,checkForPower()会返回当前设备是否在充电。代码中会检查三种充电状态:通过USB充电、通过AC充电、通过无线充电。无线充电在API 17中才被引入,所以代码中必须加入设备版本判断。

/**
 * This method checks for power by comparing the current battery state against all possible
 * plugged in states. In this case, a device may be considered plugged in either by USB, AC, or
 * wireless charge. (Wireless charge was introduced in API Level 17.)
 */
private boolean checkForPower() {
    // It is very easy to subscribe to changes to the battery state, but you can get the current
    // state by simply passing null in as your receiver.  Nifty, isn't that?
    IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
    Intent batteryStatus = this.registerReceiver(null, filter);

    // There are currently three ways a device can be plugged in. We should check them all.
    int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
    boolean usbCharge = (chargePlug == BatteryManager.BATTERY_PLUGGED_USB);
    boolean acCharge = (chargePlug == BatteryManager.BATTERY_PLUGGED_AC);
    boolean wirelessCharge = false;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
        wirelessCharge = (chargePlug == BatteryManager.BATTERY_PLUGGED_WIRELESS);
    }
    return (usbCharge || acCharge || wirelessCharge);
}

Wakelock and Battery Drain

高效的保留更多的电量与不断促使用户使用你的App会消耗电量,这是矛盾的选择题。不过我们可以使用一些更好的办法来平衡两者。

假设你的手机里面装了大量的社交类应用,即使手机处于待机状态,也会经常被这些应用唤醒用来检查同步新的数据信息。Android会不断关闭各种硬件来延长手机的待机时间,首先屏幕会逐渐变暗直至关闭,然后CPU进入睡眠,这一切操作都是为了节约宝贵的电量资源。但是即使在这种睡眠状态下,大多数应用还是会尝试进行工作,他们将不断的唤醒手机。一个最简单的唤醒手机的方法是使用PowerManager.WakeLock的API来保持CPU工作(可以设置不同参数决定屏幕和键盘的亮度)。这使得手机可以被唤醒,执行工作,然后回到睡眠状态。知道如何获取WakeLock是简单的,可是及时释放WakeLock也是非常重要的,不恰当的使用WakeLock会导致严重错误。例如网络请求的数据返回时间不确定,导致本来只需要10s的事情一直等待了1个小时,这样会使得电量白白浪费了。这也是为何使用带超时参数的wakelock.acquice()方法是很关键的。在出现了上述的意外情况时,强制释放唤醒锁。

但是仅仅设置超时并不足够解决问题,例如设置多长的超时比较合适?什么时候进行重试等等?解决上面的问题,正确的方式可能是使用非精准定时器

非精确定时器允许我们将工作转移到将来的某一时刻。如果系统系统检测到为了保存电池寿命,它可以早一点或晚一点执行,那么它会等到那一时刻再执行。例如,如果另一个进程唤醒了手机,我们的应用可能会与那一进程同时唤醒,而不是按照之前设置的时间。

现实中,有很多情况,可能进行密集电池工作更好。比如手机充电时或连接了wifi或已经被另一个进程唤醒。如果我们的工作可以延迟到将来某一时刻,延迟到更理想的时刻,那么会大大提高电池寿命。

我们可以使用Job Scheduler API来实现上述的目的,这个API在规划未来工作、提升电池性能上发挥了很大作用。例如,非用户操作可以等到手机连接电源或接入wifi时再进行,或在某一时间内集中处理任务。

Network and Battery Drain

网络行为是最耗电的操作。网络芯片开启是会消耗大类电量,而且只要保持开启状态,就会持续耗电。

通常应用用到网络有两种场景:

  1. 必须立即执行的任务。这些任务是用户行为的结果 或者 是用户需要更新界面的即时需求。比如刷新微博,这是用户自主行为,所以应用必须立即执行。
  2. 不需要立即执行的任务。比如上传用户数据、同步背景数据、更改图片大小等。

所以第一种任务需要给用户即刻反馈,第二种任务可以延迟,可以在电池方便时进行操作。应用的大部分网络请求都属于第二种。转换网络任务并提升效率可分为2步,下来观察一下下面的history historian结果:

mobile_radio一栏可以看到很多窄的红栏和空隙,这就代表移动网络的活动和休眠切换的很频繁。理想的情况应该如下图一样有大段的红栏和大段的空隙:

这代表我们已经通过降低网络连接次数,提高了性能,甚至没有使用任何移动网络连接。我们可以等手机接入wifi后,让wifi硬件来处理,这样节约了大量电量。

现在的问题是,用编写代码来分批处理(batch)、缓存(cache)并延迟(defer)这些网络连接工作非常困难。Android 5.0引入的任务调度(JobScheduler)API可以帮助我们完成这个工作。

JobScheduler API

JobScheduler API可以让我们进行任务的调度。可以通过API指定job在手机插上电源、或连接网络、或设备空闲的时候执行。

相关的类包括JobScheduler、JobInfo、JobService、JobParameters。

官网参考文档:

JobInfo.Builder developer.android.google.cn/reference/a…

JobScheduler

developer.android.google.cn/reference/a…

总结

本文主要介绍了电量相关的性能优化。

首先介绍了Android系统耗电的原理,系统在日常活动中会触发各个硬件的活动,不同的硬件设备的耗电量不同,同一个硬件设备在不同状态下的耗电量也不同。比如屏幕、CPU、蜂窝网络芯片等。

之后介绍了Android 5.0开始引入的电量信息采集工具 - Battery Historian。通过它我们可以分析在某个周期内系统的耗电情况(也可以针对某个应用做采集),从而让我们更容易地找出电量杀手,并进行优化。

官方给我们介绍了3个电量优化的方向:

1、通过BatteryManger获取当前手机的充电状态:是否在充电、通过哪种方式充电。根据这些信息,我们的应用可以采取不同的策略。

2、避免使用WakeLock唤醒系统,由于某个应用阻塞或者多个应用不停唤醒,可能会导致系统长时间无法得到休眠。应该使用不精确定时器,比如AlarmManager.setInexactXXX()或JobScheduler进行操作,系统会将类似行为的任务合并在某一时间段内同时做,减少了系统唤醒的次数,从而节省的电量。

3、网络操作是耗电大户,有两种类型的网络操作,一种是要立即得到反馈的,比如用户发起的请求;一种是非立即的,比如后台的定时请求。蜂窝网络比wifi的耗电要多得多。在间隔时间不长的情况下经常发起蜂窝网络请求,会导致网络设备不停地启动休眠再启动,这个过程会严重消耗电量。鉴于此,对于实时性不高的网络请求,我们需要减少请求的次数,将相同行为的请求放在某一时间段内统一完成 或者 通过JobScheduler设置在连接wifi时再进行操作。

写在最后

文中提到的一些电量优化方法也许实际编写应用的过程中用到的不多,除非要完成一些特定的需求才会用到,比如等到插上电源或wifi连接再进行操作这样的方法。我们需要理解的是文中通过分析原理找到优化方案的这种思想。当然如果我们在编写应用的时候需要实时记得我们的行为对电量的影响,在有文中类似场景的时候应用本文的方法或加以变通。