关于作者:CSDN内容合伙人、技术专家, 从零开始做日活千万级APP。
专注于分享各领域原创系列文章 ,擅长java后端、移动开发、商业变现、人工智能等,希望大家多多支持。
目录
- 一、导读
- 二、概览
- 三、 查看耗电情况
- 3.1 注册广播 ACTION_BATTERY_CHANGED
- 3.2 battery historion
- 3.3 手机设置
- 3.4 命令行
- 3.5 AOP & 代理hook
- 四、优化思路
- 五、 推荐阅读
一、导读
我们继续总结学习知识,温故知新。
本文主要讲了一些电量相关知识。
二、概览
电量的消耗在线上是难以量化,目前没有很好的方式能精准的获取到线上用户电量消耗情况,所以电量测试在线下非常关键,
我们要测试重点业务耗电相关的场景,以及app处于后台时耗电量**(后台静默测试)**、app网络请求时机及请求次数。
特别是一些比价耗电的场景,如:
- Camera、Audio、Video、Bluetooth、Network、Wakelock、Sensor、Radio、Screen、WIFI、CPU、GPS
三、 查看耗电情况
模块电量(mAh) = 模块电流(mA)* 模块耗时(h)
厂商在 /frameworks/base/core/res/res/xml/power_profile.xml 文件中提供了组件的电源配置文件。
3.1 注册广播 ACTION_BATTERY_CHANGED
这种方式拿到的信息相对较少。
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
Intent intent = registerReceiver(null, filter);
int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
我们可以获取到电池的电量、电池状态,但这个不能反应单个app的耗电情况。
这里得到的数据都是手机整体电量,对排查耗电帮助也不到。
3.2 battery historion
可以拿到精准的电量信息及使用信息
Battery Historian 是一个工具,用于在运行 Android 5.0 Lollipop(API 级别 21)及更高版本的 Android 设备上检查电池相关信息和事件,
而设备未插入电源。它允许应用程序开发人员在时间轴上可视化系统和应用程序级事件通过平移和缩放功能,可以轻松查看自设备上次充满电以来的各种汇总统计数据,
并选择一个应用程序并检查影响所选应用程序特定电池的指标。它还允许对两个错误报告进行 A/B 比较,突出显示关键电池相关指标的差异。
github 地址
google 地址
- 安装docker
- 安装battery historion
- 准备数据
先重置
adb shell dumpsys batterystats --reset
adb shell dumpsys batterystats --enable full-wake-history
导出
adb bugreport bugreport.zip
- 查看数据 & 分析
具体使用可自行学习。
3.3 手机设置
在手机设置里面也可以查看耗电排行,但是只有一个总的数据,不能定位哪里耗电,没啥大作用。
3.4 命令行
adb shell dumpsys batterystats > battery.txt
batterystats 所记录的电量统计数据源自于 BatteryStatsService-电量统计服务,其实现类为 BatteryStatsImpl,内部正是使用的 PowerProfile 。
BatteryStatsImpl 为每一个应用创建与之对应的 UID 来监控器系统资源的使用情况,其统计了 12 大模块的电量消耗,如下所示:
- Camera、Audio、Video、Bluetooth、Network、Wakelock、Sensor、Radio、Screen、WIFI、CPU、GPS
在 battery.txt 搜索 ‘Estimated power use’ 关键字,可以看到大概的信息。
3.5 AOP & 代理hook
我们可以通过 aop 辅助统计耗电组件,如果耗电组件在用户的使用过程中使用过多,
那么则可以辅助断定这个用户可能出现了耗电的情况,那我们就要去了解一下情况。
我们也可以通过代理对应的 Service 实现,完成收集 Wakelock、Alarm、GPS 的申请堆栈、释放信息、手机充电状态等等。
public abstract class ProxyHook extends Hook implements InvocationHandler {
/**
* 要代理的真实对象
* 持有的被代理对象,就是你要代理谁
*/
private Object proxyObj;
public ProxyHook(Context context) {
super(context);
}
public void setProxyObj(Object proxyObj) {
this.proxyObj = proxyObj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
HookedMethodHandler hookedMethodHandler = mBaseHookHandle.getHookedMethodHandler(method);
if (hookedMethodHandler != null){
// beforeInvoke(receiver, method, args); 方法开始
// 执行方法调用
Object invokeResult = hookedMethodHandler.doHookInner(proxyObj, method, args);
// afterInvoke(receiver, method, args, invokeResult); 方法执行结束
return invokeResult;
}
return method.invoke(proxyObj, args);
}
}
@Override
public void onInstall() {
Object oldObj = mContext.getSystemService(Context.ALARM_SERVICE);
Class<?> clazz = oldObj.getClass();
try {
// 获取原始mService字段
Field field = clazz.getDeclaredField("mService");
field.setAccessible(true);
// 返回指定对象上此 Field 表示的字段的值
// IAlarmManager mService;
final Object mService = field.get(oldObj);
// 设置被代理对象,也可以通过构造方法传入
setProxyObj(mService);
// 创建代理
Object proxyObject = Proxy.newProxyInstance(this.getClass().getClassLoader(), mService.getClass().getInterfaces(), this);
// 将指定对象变量上此 Field 对象表示的字段设置为指定的新值。其实就是给 mService 重新赋值代理对象
field.set(oldObj, proxyObject);
/* 这里举个简单的例子,将用户的年龄修改为33
//获取public 修饰的 指定字段名称的Field类,包含父类字段
Field field = clazz.getField("age");
field.set(user, 33);
*/
} catch (NoSuchFieldException e) {
} catch (IllegalAccessException e){
}
}
源码下载
四、优化思路
在实际使用中,一般耗电比较多的场景有视频播放、定位、复杂运算、wakelock、网络等,
所以要避免后台长时间使用耗电组件。
同时,也可以通过 cpu profiler 查看是否处于高cpu运行状态,定位 CPU 占用率异常方法。
然后针对网络请求的优化,能使用wifi就使用wifi(用 WIFI 连接网络时的功耗要低于使用移动网络的功耗),
蜂窝移动网络下需要对请求时机及次数控制,能不请求就不请求,合理设计请求时机,禁止使用轮询,导致网络请求一直处于激活状态。
在就是定位,根据场景谨慎选择定位模式:对定位准确度没那么高的场景可以选择低精度模式,或者网络定位代替 GPS,根据业务来合理
设计请求频率,使用后要及时关闭。
用户页面上比如动画要及时关闭,后台不执行等等。
大家在具体项目中具体分析。
五、 推荐阅读
Java 专栏
SQL 专栏
数据结构与算法
Android学习专栏