性能优化总纲:
大概会花一个月左右的时间出7-8个专题来分享一下在工作和学习中积累下来的Android性能优化经验。
希望大家会持续关注。
现在是专题三:电池电量优化
但这也仅仅是为大家提供一些思路与较为全面的总结,算不上什么,希望有错误或问题在下面评论。最后完结以后会将思维导图与优化框架整理出来,请期待。
题记
电池虽小,地位却非常重要。移动设备使用电池,做任何事情都要费电。而大多数情况下,白天很少有机会给电池充电,哪怕你带了电宝,也可能出现不够用的情况。而作为开发者,如果你的程序被用户发现耗电量过多很容易被卸载,再也不用,是非常致命的,因此我们要制定一系列解决方案,防止此类事情发生。本章带领大家探讨如何测量电池的使用量,以及即可以省电,又不影响用户体验的方法。
一、电池
一般来说,充满电的状态可以保证手机正常使用1-2天,除去屏幕和CPU所消耗的电量以外,设备使用多少电量严重以来所有应用都做了什么,也就是取决于你的应用是如何设计和实现的。
一般有如下功能:
- 执行代码(显而易见)
- 数据传输(上传和下载,使用WiFi,2G,3G,4G)
- 定位
- 传感器
- 渲染图像
- 唤醒任务在学习如何最大限度的减少耗电量之前,我们想要有办法来测量。
测量电池用量####
1、Battery Historian
Battery Historian是Android 5.0开始引入的新API。通过下面的指令,可以得到设备上的电量消耗信息:
$ adb shell dumpsys batterystats > xxx.txt //得到整个设备的电量消耗信息
$ adb shell dumpsys batterystats > com.package.name > xxx.txt //得到指定app相关的电量消耗信息
得到了原始的电量消耗数据之后,我们需要通过Google编写的一个python脚本把数据信息转换成可读性更好的html文件:
$ python historian.py xxx.txt > xxx.html
打开这个转换过后的html文件,可以看到类似TraceView生成的列表数据,这里的数据信息量很大,这里就不展开了。
- Track Battery Status & Battery Manager
我们可以通过下面的代码来获取手机的当前充电状态:
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,“Thephoneischarging!”);
}
在上面的例子演示了如何立即获取到手机的充电状态,得到充电状态信息之后,我们可以有针对性的对部分代码做优化。比如我们可以判断只有当前手机为AC充电状态时 才去执行一些非常耗电的操作。
private boolean checkForPower(){
IntentFilter filter=new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent batteryStatus=this.registerReceiver(null,filter);
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);
}
2、另一种方法得到耗电量
APP获取电量算法。经过查看源码,我们看到app计算电量的算法如下:
-
在主Activity里面 info.getBatteryStats() 就搞定了。 首先 load(),如果load失败,走CPU时间计算,通过getAppListCpuTime这样函数。 CPU的时间计算,有3个核心步骤:
- ActivityManager遍历runningApp进程,获取对应pid
- getAppProcessTime(pid)通过读取/proc/pid/stat文件,拿取APP在CPU的运行时间。
- 重新为BatterySipper附值:+time;
获取APP消耗 processAppUsage();也分三步走:
- 通过PowerProfile 获取cpu的速度层次(speedsteps),方便后面使用
- 根据不同CPU的速度等级,计算cpu在某个速度下的电量,mA毫安
- mPowerProfile.getAveragePower(PowerProfile.POWERCPUACTIVE,p)
很多地方都用到这个API获取power。那它究竟做了些什么呢?查看系统源码可以知道:
实际上这句话是获取1个叫PowerMap的数据结果,获得电量。
而PoweMap的赋值,是来源于com.android.internal.R.xml.powerprofile 的文件。
关于该文件的获取 android-版本号/core/res/res/xml/powerprofile.xml
计算各种耗电量的详细算法是:
来自深入浅出Android App耗电量统计
总结App耗电量计算公式:
( Uid_Power(App耗电量,单位:mAh) = Uid_Power1 + Uid_Power2 + Uid_Power3 + Uid_Power4 + Uid_Power5
Uid_Power1 = (Process1_Power + … + ProcessN_Power);
Process_Power = (CPUSpeed_Time * POWER_CPU_ACTIVE);
Uid_Power2 = PartialWakeLock_Time * POWER_CPU_WAKE
Uid_Power3 = ( tcpBytesReceived + tcpBytesSent ) *
averageCostPerByte Uid_Power4 = wifiRunningTimeMs * POWER_WIFI_ON
Uid_Power5 = (Sensor1_Power + … + SensorN_Power) Sensor_Power = Sensor_Time * Power_Sensor
二、禁用电池广播
系统除了定义了ACTION_BATTERY_CHANGED包含了电池信息,还定义了应用可以使用的4个广播Intent:
- ACTION_BATTERY_LOW
- ACTION_BATTERY_OKAY
- ACTION_POWER_CONNECRED
- ACTION_POWER_DISCONNECTED
当我们在一个广播接收器接收系统发送的这四个广播时,只要有一个发生,应用就会启动。这样有一个严重的缺陷,如果你在前台运行时,是没有问题的,但是如果在后台时,还出现Toast消息(非系统提醒),就有可能干扰其他应用,损害用户体验。
所以我们有一个很好的解决:
- 只有当应用在前台运行时才可以启用广播
- 步骤:
1、广播接收器默认必须是禁用
2、广播接收器必须在onResume()中启用,在onPause()被禁用
三、控制网络
现在基本所有的应用都必须在设备和服务器之间传递数据,就像获取电池状态一样,应用需要获取设备商的网络链接信息。
ConnectivityManager类提供了API。供应用调用以此访问网络信息。
Android设备通常由多个数据连接:
- Bluetooth
- Ethernet
- Wi_Fi
- WiMax
- 移动网络(EDGE、UMTS、LTE)
为了最大限度延长电池的使用时间,我们需要直到如下事情:
- 后台数据设置
- 数据传输频度
后台数据
可以通过下面这个方法来获取后台数据的设置,
不过在4.0以后,这个方法始终返回为true,当强行不允许时,网络会断开。
ConnectivityManager的getBackgroundDataSetting()
数据传输传输速率的差异非常大,从小于每秒100Kb的GPRS数据连接到每秒几Mb的LTE或Wifi都有。除了连接类型,NetWorkInfo类还指定了连接的子类型,例如:
- NETWORK_TYPE_GPRS(API 1)
- NETWORK_TYPE_LTE(API 11)
- NETWORK_TYPE_HSPAP(API 13)
如果创建和部署了新技术,也会增加新的子类型。留意一下每个SDK版本的改动。人们都习惯更快的连接,即使WiFi芯片耗电量比较多,但Wifi的速率和免费可以让数据在最短时间最小成本完成传输,从而降低电池消耗。
如果能控制数据的传输类型,就可以现压缩数据,再传输到设备上。虽然解压缩数据耗费CPU,也多用了些电量,但传输速度大大加快,数据通讯设备可以很快关闭,从而延长了电池寿命。通常我们这么做:
使用GZIP压缩文本数据,使用GZIPInputStream类访问数据;
使用匹配设备分辨率的资源(比如:不必为320480的屏幕下载19201080的图片)
四、定位
现在很多的App都会做一件事:获取你的定位信息。一些用户可能愿意牺牲电池寿命来频繁的更新位置,而其他人定远县志更新次数,以确保设备点亮不会很快消耗完,所以需要提供不同的昔阳县来满足用户需求。
1、注销监听器还是和处理广播那样,在onPause()中调用removeUpdates()可以注销监听器。
2、并且可以用requestLocationUpdates()调整更新频率。选择合适的时间间隔和最小间隔距离可以适应不同的场景。
3、通过选择不同的位置服务来控制,如下:
- GPS定位
- WIFI定位
- 基站定位
- AGPS定位
这四种定位的概念想必大多数都知道了,不知道的戳这里
五、传感器
传感器是个很有意思的东西,与定位服务有点类似:应用向特定的传感器注册监听器,获得更新通知。
也是可以通过降低通知频率来省点,由于,每个设备不同,应用可以测量这4种延迟通知的频率,选择兼顾用户体验和省点的那一个。另一种策略是,当发现值不变化时,使用NORMAL或UI延迟,当发现有突然变化的时候,且话到GAME或FASTEST延迟。
如下:
- SENSOR_DELAY_NORMAL
- SENSOR_DELAY_UI
- SENSOR_DELAY_GAME
- SENSOR_DELAY_FASTEST
六、图形
应用花费了很多时间在屏幕上画东西,无论是使用GPU渲染的3D游戏还是使用CPU的日历程序,都想只以最少的代价赖在屏幕上展示期望的结果,以延长电池寿命。
如前所述,CPU非全速时使用的电量相对少一些。现代的CPU使用动态调频喝点呀来节省电力和减少发热量。这两种技术通常一起使用,成为DVFS技术(动态电压和频率调整),Linux的内核、Android以及现代处理器都支持这种技术。
虽然你不能直接控制电压和频率,或将内部组建断电,但可以控制应用渲染的方式。流畅的帧速率还是要达到的。虽然在Android上帧率有上线(每秒60帧),但优化渲染还是有效果的。除了可能降低能耗,你可可以为后台运行的应用留出更多的空间,提供了更好的整体用户体验。
例如:比如我们手机里的壁纸,可以调用onVisibilityChanged()方法。事实上,壁纸可以是不可见的。但很容易忘记,持续绘制壁纸会消耗很多的电量。
七、唤醒
有时候你得应用可能出于某种原因,需不时的被唤醒,去执行一些操作。>
但是很少应用真正需要到提醒时间去强行唤醒设备。当然闹钟这类程序会需要这种功能,但大多是等到用户主动唤醒才工作。多数情况下,应用需要将未来某一刻安排提醒到时,但对时间要求并不是很严格。为此Android定义了AlarmManager.setInexactRepeating(),它的参数和其“兄弟”setPepeating()基本相同,这种题型更节能,系统也避免了出现不必要的唤醒,Android定义了5个提醒间隔:
- INTERVAL_FIFTEEN_MINUTES
- INTERVAL_HALF_HOUR
- INTERVAL_HOUR
- INTERVAL_HALF_DAY
- INTERVAL_DAY
最好的结果就是所有应用都使用这种提醒,而不用精确的触发提醒。为了尽可能的节电,应用还可以让用户配置提醒的调度,因为有人发现较长时间间隔并不会对用户体验有不好的影响。
八、WakeLock
有些时候,一些应用即使长时间不和设备交互,也要阻止进入休眠状态,来保持良好的用户体验,就比如在看视频的时候,这种情况下,CPU需要做视频解码,同时屏幕保持开启,让用户能够观看。此外,视频播放时屏幕不能变暗。
Android为这种情况设计了WakeLoac类
private void runInWakeLoac(Runnable runnable,int flags){
PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
PowerManager.WakeLoac wl = pm.newWakeLock(flag,"My WakeLock");
wl.acquire();
runnable.run();
wl.release();
}
有一点需要注意:需要WAKE_LOCK权限。
系统的行为取决于WakeLock对象创建时传入的flags:
- PARTIAL_WAKE_LOCK(CPU开)
- SCREEN_DIM_WAKE_LOCK(CPU开、暗色显示)
- SCREEN_BRIGHT_WAKE_LOCK(CPU开、明亮显示)
- FULL_WAKE_LOCK(CPU开、明亮显示、键盘开)这些标记可以结合使用
- ACQUIRE_CAUSES_WAKEUP(打开屏幕和键盘)
- ON_AFTER_RELEASE(WakeLock是放后继续保持屏幕和键盘开启片刻)
特别重要的一点:一定要释放WakeLock,在退出和暂停的时候,否则可能一直显示,电量很快耗光。
预防问题
防止出现特殊的问题,建议使用带超时的WakeLoac.acquire()版本,它会在超时后自动释放。另外:可以用setKeepScreenOn()方法控制是否要保持屏幕,只要可见的View指定了要保持屏幕,屏幕就会一直保留。
九、总结
用户不会注意到应用是否延长了电池寿命。但是如果不做任何处理,那就有可能被注意到了。因为单个应用耗电过多,用户几天还是可以感受出来的,用户到时候就会卸载用用,所以要对电量使用做一些优化处理,并且给用户配置选项的自由,来应对用户产生的各种需求。