目前行业内有很多电量测试的方法:
1.1 Batterystats & bugreport
Android 5.0及以上的设备, 允许我们通过adb命令dump出电量使用统计信息.
1, 因为电量统计数据是持续的, 会非常大, 统计我们的待测试App之前先reset下, 连上设备, 命令行执行:
$ adb shell dumpsys batterystats --reset
Battery stats reset.
2, 断开测试设备, 操作我们的待测试App.
3, 重新连接设备, 使用adb命令导出相关统计数据:
// 此命令持续记录输出, 想要停止记录时按Ctrl+C退出.
$ adb bugreport > bugreport.txt
导出的统计数据存储到bugreport.txt, 此时我们可以借助如下工具来图形化展示电池的消耗情况.
注意, 官方SDK文档导出文件方式为:
adb shell dumpsys batterystats > batterystats.txt
使用python historian.py batterystats.txt > batterystats.html查看数据
是battery-historian老版本的使用方式. 目前Battery Historian已更新2.0版本, 推荐使用bugreport方式导出数据分析, 可以看到更多信息.
1.2 Battery Historian
Google提供了一个开源的电池历史数据分析工具 - Battery Historian,支持5.0(API 21)及以上系统手机的电量分析。
1.2.1 安装
按照Battery Historian在github上的readme, 一步步安装即可.
需要注意的是, Battery Historian是Go语言的, 安装Go的时候需要配置其bin的环境变量.
Python环境需要是2.7的(3.x不行), 建议使用pyenv管理本地的python环境.
另外, 因为Battery Historian是一个网页版工具, 涉及一些JS引用, 有时需要FQ.
安装完成后, 执行:
cd $GOPATH/src/github.com/google/battery-historian
go run cmd/battery-historian/battery-historian.go [--port <default:9999>]
程序运行在http://localhost:9999, 如下:
1.2.2 界面
导入我们在第一步通过adb bugreport生成的bugreport.txt文件:
如下短视频app是Battery Historian测试结果部分截图:
视频列表页
视频详情页
对测试结果数据进行汇总整理:
CPU负载高,会导致耗电量高是显而易见的
二、耗电量计算原理
根据物理学中的知识,功=电压*电流*时间,但是一部手机中,电压值U正常来说是不会变的,所以可以忽略,只通过电流和时间就可以表示电量。模块电量(mAh)=模块电流(mA)*模块耗时(h)。模块耗时比较容易理解,但是模块电流怎样获取呢,不同厂商的手机,硬件不同,是否会影响模块的电流呢。看一下系统提供的接口:./frameworks/base/core/java/com/Android/internal/os/PowerProfile.java
该类提供了public double getAveragePower(String type)接口,type可取PowerProfile中定义的常量值,包括POWER_CPU_IDLE(CPU空闲时),POWER_CPU_ACTIVE(CPU处于活动时),POWER_WIFI_ON(WiFi开启时)等各种状态。并且从接口可以看出来,每个模块的电流值,是从power_profile.xml文件取的值。PowerProfile.java只是用于读取power_profile.xml的接口而已,后者才是存储系统耗电信息的核心文件。power_profile.xml文件的存放路径是/system/framework/framework-res.apk。
以Nexus 6P为例,在该路径获取到framework-res.apk文件。使用apktool,对framework-res.apk进行反解析,获取到手机里面的power_profile.xml文件,内容如下所示:
1 <?xml version="1.0" encoding="utf-8"?> 2 <device name="Android"> 3 <item name="none">0</item> 4 <item name="screen.on">169.4278765</item> 5 <item name="screen.full">79.09344216</item> 6 <item name="bluetooth.active">25.2</item> 7 <item name="bluetooth.on">1.7</item> 8 <item name="wifi.on">21.21733311</item> 9 <item name="wifi.active">98.04989804</item> 10 <item name="wifi.scan">129.8951166</item> 11 <item name="dsp.audio">26.5</item> 12 <item name="dsp.video">242.0</item> 13 <item name="gps.on">5.661105191</item> 14 <item name="radio.active">64.8918361</item> 15 <item name="radio.scanning">19.13559783</item> 16 <array name="radio.on"> 17 <value>17.52231575</value> 18 <value>5.902211798</value> 19 <value>6.454893079</value> 20 <value>6.771166916</value> 21 <value>6.725541238</value> 22 </array> 23 <array name="cpu.speeds.cluster0"> 24 <value>384000</value> 25 <value>460800</value> 26 <value>600000</value> 27 <value>672000</value> 28 <value>768000</value> 29 <value>864000</value> 30 <value>960000</value> 31 <value>1248000</value> 32 <value>1344000</value> 33 <value>1478400</value> 34 <value>1555200</value> 35 </array> 36 <array name="cpu.speeds.cluster1"> 37 <value>384000</value> 38 <value>480000</value> 39 <value>633600</value> 40 <value>768000</value> 41 <value>864000</value> 42 <value>960000</value> 43 <value>1248000</value> 44 <value>1344000</value> 45 <value>1440000</value> 46 <value>1536000</value> 47 <value>1632000</value> 48 <value>1728000</value> 49 <value>1824000</value> 50 <value>1958400</value> 51 </array> 52 <item name="cpu.idle">0.144925583</item> 53 <item name="cpu.awake">9.488210416</item> 54 <array name="cpu.active.cluster0"> 55 <value>202.17</value> 56 <value>211.34</value> 57 <value>224.22</value> 58 <value>238.72</value> 59 <value>251.89</value> 60 <value>263.07</value> 61 <value>276.33</value> 62 <value>314.40</value> 63 <value>328.12</value> 64 <value>369.63</value> 65 <value>391.05</value> 66 </array> 67 <array name="cpu.active.cluster1"> 68 <value>354.95</value> 69 <value>387.15</value> 70 <value>442.86</value> 71 <value>510.20</value> 72 <value>582.65</value> 73 <value>631.99</value> 74 <value>812.02</value> 75 <value>858.84</value> 76 <value>943.23</value> 77 <value>992.45</value> 78 <value>1086.32</value> 79 <value>1151.96</value> 80 <value>1253.80</value> 81 <value>1397.67</value> 82 </array> 83 <array name="cpu.clusters.cores"> 84 <value>4</value> 85 <value>4</value> 86 </array> 87 <item name="battery.capacity">3450</item> 88 <array name="wifi.batchedscan"> 89 <value>.0003</value> 90 <value>.003</value> 91 <value>.03</value> 92 <value>.3</value> 93 <value>3</value> 94 </array> 95 </device>
从文件内容中可以看到,power_profile.xml文件中,定义了消耗电量的各模块。如下图所示:
文件中定义了该手机各耗电模块在不同状态下的电流值。刚刚提到,电量只跟电流值和时间相关,所以通过这个文件,再加上模块的耗时,就可以计算出App消耗的电量,App电量=∑App模块电量。划重点,手机系统里面的电量排行,也是根据这个原理计算的。
了解原理对于平常在App耗电量的测试有很大的帮助。因为获取到手机power_profile.xml文件,就可以清楚的知道这个手机上,哪些模块会耗电,以及哪些模块在什么状态下耗电量最高。那么测试的时候,应该重点关注调用了这些模块的地方。比如App在哪些地方使用WiFi、蓝牙、GPS等等。
例如最近对比测试其他App发现,在一些特定的场景下,该App置于前台20min内,扫描了WiFi 50次,这种异常会导致App耗电量大大增加。并且反过来,当有case报App耗电量异常时,也可以从这些点去考虑,帮助定位问题。
三、电量测试方法总结
如上,列出的一些常用的电量测试方法。综合各方法的优缺点,在定制个性化电量测试工具之前,目前采用的方法是Battery Historian。目前行业内,App耗电测试有很多种方案,如果仅仅测试出一个整体的电量值,对于定位问题是远远不够的。借助Battery Historian,可以查看自设备上次充满电以来各种汇总统计信息,并且可以选择一个App查看详细信息。所以QA的测试结果反馈从“这个版本App耗电量”高,变成“这个版本CPU占用高”“这个版本WiFi扫描异常”,可以帮助更快的定位到问题原因及解决问题。
当然,除了测试方法和测试工具,测试场景设计也非常重要。如果是在App内毫无规律的浏览,即使发现页面有问题,有很难定位到是哪个模块的问题。所以要针对性的设计场景,并且进行一些场景的对比,找出差异的地方。
介绍关于App电量测试中使用的一些基本方法和思路。
电量测试采用的Battery Historian方法,虽然能初步解决问题,但是在实际的应用场景中还存在很多不足。目前美团点评云测平台,已经集成了电量测试方法,通过自动化操作,获取电量测试文件并进行解析,极大的提高了测试效率。目前每个版本发布之前,我们都会进行专门的电量测试,保障用户的使用体验。在电量测试方面,美团点评测试团队还在持续的实践和优化中。
对于一个App, 对应因素主要有:
1 网络请求
我们可能会有发现:
- 测试用的手机充满电放了一个十一假期还有电, 是因为测试手机没有上SIM卡.
- 飞行模式下的手机灭屏下, 可能可以放一个月都还有电.
这是因为:
- 手机的通过内置的射频模块和基站几乎, 从而链接上网的, 而这个射频模块(radio)是非常耗电的.
- 为了控制这个射频模块的耗电, 硬件驱动及Android RIL层做了很多处理. 例如可以单独关闭radio(飞行模式), 间歇性假休眠radio(有数据发生时才上电, 保持一个频率的与基站交互)等等.
现如今App都是移动互联网App, 不可避免的会有大量的网络请求, 会导致radio一直处于活跃状态, 从而耗电量增加.
2 WakeLock
Android系统本身为了优化电量的使用, 会在没有操作时进入休眠状态, 来节省电量. 当然, 为了便于开发(很多应用不可避免的希望在灭屏后还能运行一些事儿, 或是要保持屏幕一直亮着--比如播放视频), Android提供了一个PowerManager.WakeLock的东西.
我们可以用WakeLock来保持CPU运行, 或是防止屏幕变暗/关闭, 让手机可以在用户不操作时依然可以做一些事儿. 然而, 获取WakeLock很容易, 释放不好就会成为难题, 消耗电量.
例如我们获取了一个WakeLock来保持CPU运转, 做一个复杂运算并将数据上传到后台服务器, 然后释放该WakeLock. 然而这个过程可能并不像我们想象的那么快, 可能因为比如服务器挂掉, 计算出了异常等等WakeLock没有释放. 问题就来了, CPU会一直得不到休眠, 而大大增加耗电.
另外, WakeLock还有android:keepScreenOn属性, 还可以让屏幕常量, 这可是耗电大户.
3 GPS
应用中经常会用到定位服务, Android提供了Network定位和GPS定位. 相对来说, GPS会精确得多, 对于一些诸如跑步, 导航类的应用基本会使用GPS定位. 然而, GPS定位也会消耗大量的电量.
尽可能减少App的电量消耗的建议
了解了上述的主要的耗电因素, 还有一些程序的耗电问题, 我们通过Battery Historian也可以分析.
针对这些耗电情况, 给出如下优化建议:
1 优化网络请求
1.1 接口设计
1.1.1 API设计
App与Server之间的API设计要考虑网络请求的频次, 资源的状态等. 以便App可以以较少的请求来完成业务需求和界面的展示.
1.1.2 Gzip压缩
使用Gzip来压缩request和response, 减少传输数据量, 从而减少流量消耗.
1.1.3 考虑使用Protocol Buffer代替JSON
从前我们传输数据使用XML, 后来使用JSON代替了XML, 很大程度上也是为了可读性和减少数据量(当然还有映射成POJO的方便程度).
Protocol Buffer是Google推出的一种数据交换格式.
如果我们的接口每次传输的数据量很大的话, 可以考虑下protobuf, 会比JSON数据量小很多.
当然相比来说, JSON也有其优势, 可读性更高.
本文以网络流量优化的角度推荐protobuf作为一个选择, 具体还需更具实际情况考虑.
1.1.4 图片的Size
可以在请求图片的url中添加诸如质量, 格式, width, height等path来获取合适的图片资源
1.1.5 网络缓存
适当的缓存, 既可以让我们的应用看起来更快, 也能避免一些不必要的流量消耗.
1.1.6 打包网络请求
当接口设计不能满足我们的业务需求时. 例如可能一个界面需要请求多个接口, 或是网络良好, 处于Wifi状态下时我们想获取更多的数据等.
这时就可以打包一些网络请求, 例如请求列表的同时, 获取Header点击率较高的的item项的详情数据.
2 网络代理工具
一般来说, 网络代理工具有两个作用:
- 截获网络请求响应包, 分析网络请求
- 设置代理网络, 移动App开发中一般用来做不同网络环境的测试, 例如Wifi/4G/3G/弱网等.
代理工具很多, 诸如Wireshark, Fiddler, Charles等
4 监听相关状态
通过监听设备的状态:
- 休眠状态
- 充电状态
- 网络状态
结合JobScheduler来根据实际情况做网络请求. 比方说Splash闪屏广告图片, 我们可以在连接到Wifi时下载缓存到本地; 新闻类的App可以在充电, Wifi状态下做离线缓存.
3.5 弱网测试&优化
除了正常的网络优化, 我们还需考虑到弱网情况下, App的表现.
3.5.1 Android Emulator
创建和启动Android模拟器可以设置网络速度和延迟
3.5.2 网络代理工具
设置代理网络, 以Charles为例:
保持手机和PC处于同一个局域网, 在手机端wifi设置高级设置中设置代理方式为手动, 代理ip填写PC端ip地址, 端口号默认8888.
弱网优化, 本质上是在弱网的情况下能让用户流畅的使用我们的App. 我们要做的就是结合上述的优化项:
- 压缩/减少数据传输量
- 利用缓存减少网络传输
- 针对弱网(移动网络), 不自动加载图片
- 界面先反馈, 请求延迟提交
例如, 用户点赞操作, 可以直接给出界面的点赞成功的反馈, 使用JobScheduler在网络情况较好的时候打包请求.
2 谨慎使用WakeLock
- WakeLock获取释放成对出现.
- 使用超时WakeLock, 以防出异常导致没有释放.
1 // Acquires the wake lock with a timeout. 2 acquire(long timeout);
3 监听手机充电状态
BatteryManager会发送一个包含充电状态的持续广播, 我们可以通过此广播获取充电状态和电量详情:
1 // 注意: 因为这是一个持续广播, 我们无需写receiver, 可以直接通过intent获取相关数据. 2 IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); 3 Intent batteryStatus = context.registerReceiver(null, ifilter);
例如, 如果设备正在充电:
1 // Are we charging / charged? 2 int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1); 3 boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || 4 status == BatteryManager.BATTERY_STATUS_FULL; 5 6 // How are we charging? 7 int chargePlug = battery.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); 8 boolean usbCharge = chargePlug == BATTERY_PLUGGED_USB; 9 boolean acCharge = chargePlug == BATTERY_PLUGGED_AC;
另外我们也可以监听充电状态的变化, 只要设备连接或断开电源, BatteryManager就会广播相应的操作, 我们可以注册receiver来监听:
1 <receiver android:name=".PowerConnectionReceiver"> 2 <intent-filter> 3 <action android:name="android.intent.action.ACTION_POWER_CONNECTED"/> 4 <action android:name="android.intent.action.ACTION_POWER_DISCONNECTED"/> 5 </intent-filter> 6 </receiver>
监听电池状态, 可以让我们将一些操作放在充电或是电量足够的情况下进行, 以提升用户体验. 例如用户数据同步, Log上传等.
4 Doze and App Standby
Android 6.0提供了两个用来节省电量的技术Doze和App Standby.
-
Doze
瞌睡. 如果设备闲置了一段较长时间, Doze技术将通过延迟后台网络活动, CPU运行等来减少电量损耗. -
App Standy
应用待机. 不是最近得到过用户"宠幸"的App, App Standy将延缓这个应用的后台网络活动.
5 关于定位
- 定位中使用GPS, 请记得及时关闭
1 // Remove the listener you previously added 2 locationManager.removeUpdates(locationListener);
- 减少更新频率
- 根据实际情况选择GPS或网络或两者. 只使用一个会降低电量损耗.