你是否了解 APP 耗电问题?深入探索 Android 电量优化,android 物联网开发李天祥
作用
将电量测量转化为功能模块的使用时间或者次数。
adb shell dumpsys batterystats > battery.txt
在 battery.txt 搜索 ‘Estimated power use’ 关键字,下面粗略统计了各个 Uid 的总耗电量。
Estimated power use (mAh):
Capacity: 3350, Computed drain: 2767, actual drain: 3752-3853
Uid 1000: 1014 ( cpu=999 wake=1.36 radio=11.4 wifi=1.24 gps=0.435 sensor=0.808 ) Excluded from smearing
Unaccounted: 985 ( ) Including smearing: 0 ( ) Excluded from smearing
Uid 0: 416 ( cpu=157 wake=210 radio=38.8 wifi=9.51 ) Excluded from smearing
...
batterystats 所记录的电量统计数据源自于 BatteryStatsService-电量统计服务,其实现类为 BatteryStatsImpl,内部正是使用的 PowerProfile 。
BatteryStatsImpl 为每一个应用创建与之对应的 UID 来监控器系统资源的使用情况,其统计了 12 大模块的电量消耗,如下所示:
Camera、Audio、Video
Bluetooth、Network、Wakelock
Sensor、Radio、Screen
WIFI、CPU、GPS
特点
1)、查看自设备上次充电以来各种汇总统计信息,而且可以选择对应的 App 查看详细信息。
2)、可视化展示指标:
耗电比例。
执行时间、次数。
3)、仅适合线下使用。
安装
1)、安装 Docker
2)、docker – run -p:9999 gcr.io/android-battery-historian/stable:3.0 --port 9999 (需要翻墙)
导出电量信息
1)、使用 batterystats 命令重置手机电量:adb shell dumpsys batterystats --reset
2)、使用 batterystats 命令获取电池数据权限并开启记录全面的电量信息:adb shell dumpsys batterystats --enable full-wake-history
3)、测试完成后,使用 bugreport 导出电量信息:
7.0 和 7.0 以后:adb bugreport bugreport.zip
6.0 和 6.0 之前:adb bugreport > bugreport.txt
通过 historian 图形化展示结果:python historian.py -a bugreport.txt > battery.html
上传分析
1)、打开 http://localhost:

如果打不开,可以使用备用网站 https://bathist.ef.lc/
2)、上传 bugreport 文件,点 Submit 提交即可。
Battery Historian 数据分析
Hitorian V2 — 电量统计图表
Add Metrics

在 Add Metrics 中我们可以增加更多的测量项。
CPU running

如果一直处于 running,则表明电量消耗比较高。
JobScheduler

选中 Job Scheduler 的某一个工作时间片,我们可以查看具体的 发生的时间、耗时以及次数,最重要的是它统计出来了是哪一个进程在使用这个 JobScheduler。
App Selection

1)、选择要分析电量的指定 App。
2)、点击右边区域的 System Stats 一栏可以在下方查看各个系统组件的电量百分比消耗详情,例如 Userspace Wakelocks。
主入口处的 Switch to Bugreport Comparison

选择多个文件进行上传对比。
1)、耗电场景测试
复杂计算。
音视频播放。
2)、传感器相关
使用时长
耗电量
发热
3)、后台静默测试
===============================================================
1)、「缺乏现场,无法复现」。
2)、「信息不全,难以定位」。
3)、「无法评估结果」。
在 App 开发中,经常会由于某个需求场景或 代码 bug 而导致大量耗电。
思考步骤
需要后台运行
长时间下载:DownloadManager
数据同步:SyncAdapter
本地任务:JobScheduler
特定时间执行:AlarmManager
实时通信:推送服务
立刻执行:Foreground Service
对于耗电优化中,我们最常用的就是 JobScheduler,下面??,我们来实战一下。
Job Scheduler 实战
/**
开启 JobScheduler
*/
private void startJobScheduler() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
JobInfo.Builder builder = new JobInfo.Builder(1, new ComponentName(getPackageName(), JobSchedulerService.class.getName()));
// 设置仅在 充电和 WIFI 下才使用 JobScheduler 进行批量任务处理
builder.setRequiresCharging(true)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
jobScheduler.schedule(builder.build());
}
}
其中,「JobSchedulerService 就是用于进行批量任务处理的服务」,示例代码如下所示:
/**
用于进行批量任务处理的 JobSchedulerService
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class JobSchedulerService extends JobService {
@Override
public boolean onStartJob(JobParameters params) {
// 此处执行在主线程
// 模拟一些处理:批量网络请求,APM 日志上报
return false;
}
@Override
public boolean onStopJob(JobParameters params) {
return false;
}
}
特点
1)、「仅支持 API 21 及之上」。
2)、「在符合某些条件时创建执行在后台的任务」。
3)、「把不紧急的任务放到更合适的时机批量处理」。
符合 Android 规则,手机在充电状态才去做耗电工作。示例代码如下所示:
IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent batteryStatus = context.registerReceiver(null, ifilter);
//获取用户是否在充电的状态或者已经充满电了
int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL;
1、优化应用的后台耗电
避免后台长时间获取 WakeLock、WIFI 和蓝牙的扫描等。
2、符合系统的耗电规则
Android P 使用了 Android Vitals 监控后台耗电,其规则如下所示:
1)、Alarm Manager wakeup 唤醒过多:当手机不在充电状态,每小时 wakeup 唤醒次数大于 10 次。
2)、频繁使用局部唤醒锁:当手机不在充电状态,partial wake lock 持有超过 1 小时。
3)、后台网络使用量过高:当手机不在充电状态而且应用在后台,每小时网络使用量超过 50MB。
4)、后台 WiFi scans 过多:当手机不在充电状态而且应用在后台,每小时大于 4 次 WiFi scans。
3、CPU 时间片
「Android 手机保护 AP 和 BP 两个 CPU。AP 即 Application Processor,所有的用户界面以及 App 都是运行在 AP 上的。BP 即 Baseband Processor,手机射频都是运行在这个 CPU 上的。而一般我们所说的耗电,PowerProfile 文件里面的 CPU,指的是 AP」。
CPU 耗电通常有两种情况:
1)、「长期频繁唤醒:原本可以仅仅在 BP 上运行,消耗 5mA 左右,但是因为唤醒,AP 就会运作,不同手机情况不一样,至少会导致 20~30 mA 左右的耗电」。
2)、「CPU 长期高负荷:例如 App 退到后台的时候没有停止动画,或者程序有不退出的死循环等等,导致 CPU 满频、满核地跑」。
常用优化 CPU 时间片的方式有:
1)、「获取运行过程线程 CPU 消耗,定位 CPU 占用率异常方法」。
2)、「减少后台应用的主动运行」。
4、网络相关
通常情况下,使用 WIFI 连接网络时的功耗要低于使用移动网络的功耗。而使用移动网络传输数据,电量的消耗有以下 3 种状态:
「Full power:高功率状态,移动网络连接被激活,允许设备以最大的传输速率进行操作」。
「Low power:低功耗状态,对电量的消耗差不多是 Full power 状态下的 50%」。
「Standby:空闲态,没有数据连接需要传输,电量消耗最少」。
因此,为了避免网络连接所带来的电量消耗,我们可以采用如下几种方案:
1)、尽量在 WIFI 环境下进行数据传输,在使用 WIFI 传输数据时,应该尽可能增大每个包的大小(不超过 MTU),并降低发包的频率。
2)、在蜂窝移动网络下需要对请求时机及次数控制:可以延迟执行的网络请求稍后一起发送,最好做到批量执行,尽量避免频繁的间隔网络请求,以尽量多地保持在 Radio Standby 状态。
3)、使用 JSON 和 Protobuf 进行数据压缩,减少时间。
4)、禁止使用轮询功能:轮询会导致网络请求一直处于被激活的状态,耗电过高。
5、定位相关
1)、「根据场景谨慎选择定位模式:对定位准确度没那么高的场景可以选择低精度模式」。
2)、「可以考虑网络定位代替 GPS」。
3)、「使用后务必及时关闭,减少更新频率,例如定位开启一定时间后超过某个阈值可以执行一个兜底策略:强制关闭 GPS」。
6、界面相关
1)、**「离开界面后停止相关活动,例如关闭动画」*
《Android 学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享
*。
2)、「耗电操作判断前后台,如果是后台则不执行相关操作」。
7、WakeLock 相关
WakeLock 常用于后台播放音视频、录制音视频、下载文件的情况。如果没有合理使用 WakeLock,则会造成严重的耗电问题,为了避免该问题,「我们应该定期针对使用了 WakeLock 的模块进行重点排查」。
我们可以使用 adb shell dumpsys power 命令查看系统当前的耗电信息,其中我们可以看到 WakeLock 列表,它通常会以**「”mLocks.size“ 或者 ”Wake Locks:size“」** 开头。关于 WakeLock 的使用我们要着重注意以下几点:
1)、「注意成对使用 acquire、release」。
2)、「建议使用带参数的 acquire,避免没有及时释放而导致电量消耗过大」。
3)、「使用 finally 确保 release 一定会被调用」。
4)、「常亮场景使用 keepScreenOn 即可」。
5)、「WakeLock 有一个接口 setReferenceCounted,用来设置 WakeLock 的技术机制,官方默认为计数。true 为计数,false 为不计数。所谓计数即每一个 acquire 必须对应一个 release;不计数则是无论有多少个 acquire,一个 release 就可以释放。但是问题是有的第三方 ROM 它将默认设置为了不计数,以为我们需要在调用 newWakeLock 之后再调用 setReferenceCounted 为 false」。
8、计算优化
「浮点运算比整数运算更消耗 CPU 时间片,因此耗电也会增加」。避开浮点运算的优化方法如下所示:
1)、「除法变乘法」。
2)、「充分利用移位」。
3)、「在 native 层开发时,可以利用 ARM neon 指令集做并行运算,注意需要 ARM V7 及以上架构 CPU 才能支持」。
9、灭屏时停止动画
「我们可以监听灭屏以及亮屏的广播,在灭屏的时候停止 surfaceView 的动画绘制。在亮屏的时候,恢复动画的绘制」。
===============================================================
以后台耗电监控为主,必须监控的模块有:
1)、「Alarm wakeup」
2)、「WakeLock」
3)、「WiFi scans」
4)、「Network」
「必须监控的现场信息有」 :
1)、「堆栈信息」
2)、「是否充电」
3)、「电量水平」
4)、「应用前后台时间」
5)、「CPU 状态信息」
最后,我们需要 「提炼规则,将监控内容 => 抽象成规则」。
我们可以通过代理对应的 Service 实现,完成收集 Wakelock、Alarm、GPS 的申请堆栈、释放信息、手机充电状态等等。
?
示例项目
?
1)、获取运行时能耗文件
1)、adb pull /system/framework/framework-res.apk
2)、反编译,xml—》power_profile
2)、电量辅助监控
线下使用 epic 进行 AOP 电量辅助统计
这里我们就以 WakeLock 的监控为例,切面代码如下所示:
public static long sStartTime = 0;
@Insert(value = "acquire")
@TargetClass(value = "com.optimize.performance.wakelock.WakeLockUtils",scope = Scope.SELF)
public static void acquire(Context context){
trace = Log.getStackTraceString(new Throwable());
sStartTime = System.currentTimeMillis();
Origin.callVoid();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
WakeLockUtils.release();
}
},1000);
}
@Insert(value = "release")
@TargetClass(value = "com.optimize.performance.wakelock.WakeLockUtils",scope = Scope.SELF)
public static void release(){
LogUtils.i("PowerManager "+(System.currentTimeMillis() - sStartTime)+"/n"+trace);
此外,我们也可以利用 epic 来监控每个线程的执行时间,超过阈值则警告,示例代码如下所示:
public static long runTime = 0;
@Insert(value = "run")
@TargetClass(value = "java.lang.Runnable",scope = Scope.ALL)
public void run(){
runTime = System.currentTimeMillis();
Origin.callVoid();
LogUtils.i("runTime "+(System.currentTimeMillis() - runTime));
}
「写一个基础类,然后在统一的调用接口中添加监控逻辑」。这里我们可以参考 Facebook Battery-Metrics 获取、监控数据的方式。其代码如下所示:
public class WakelockMetrics {
/**
获取 WakeLock
@param wakeLock WakeLock
@param timeout 超时时间
*/
public static void acquire(PowerManager.WakeLock wakeLock, long timeout) {
wakeLock.acquire(timeout);
// 监控 wakelock 相关信息
Log.e("HOOOOOOOOK", "--acquireWakeLock--");
Log.e("HOOOOOOOOK", Utils.getStackTrace());
// 使用 Battery-Metrics 库统计其它维度的电量信息
}
/**
释放 WakeLock
@param wakeLock WakeLock
*/
public static void release(PowerManager.WakeLock wakeLock) {
wakeLock.release();
Log.e("HOOOOOOOOK", "--releaseWakeLock--");
Log.e("HOOOOOOOOK", Utils.getStackTrace());
// 使用 Battery-Metrics 库统计其它维度的电量信息
}
}
Gradle 耗电量统计插件中 BatteryCreateMethodVisitor 的核心实现代码如下所示:
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
// 监控 Wakelock
String monitorClass = "com/ss/android/ugc/bytex/example/battery_monitor/WakelockMetrics";
if (!monitorClass.equals(className)
&& "android/os/PowerManager$WakeLock".equals(owner)
&& opcode == Opcodes.INVOKEVIRTUAL
&& "acquire".equals(name)) {
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
monitorClass,
name,
"(Landroid/os/PowerManager$WakeLock;J)V",
isInterface
);
return;
}
if (!monitorClass.equals(className)
&& "android/os/PowerManager$WakeLock".equals(owner)
&& opcode == Opcodes.INVOKEVIRTUAL
&& "release".equals(name)) {
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
monitorClass,
name,
"(Landroid/os/PowerManager$WakeLock;)V",
isInterface
);
return;
}
// 监控 Gps
monitorClass = "com/pingan/bank/customplugin/GpsMetrics";
if (!monitorClass.equals(className)
&& "android/location/LocationManager".equals(owner)
&& opcode == Opcodes.INVOKEVIRTUAL
&& "requestLocationUpdates".equals(name)) {
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
monitorClass,
name,
"(Landroid/location/LocationManager;Ljava/lang/String;JFLandroid/location/LocationListener;)V",
isInterface
);
return;
}
if (!monitorClass.equals(className)
&& "android/location/LocationManager".equals(owner)
&& opcode == Opcodes.INVOKEVIRTUAL
&& "removeUpdates".equals(name)) {
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
monitorClass,
name,
"(Landroid/location/LocationManager;Landroid/location/LocationListener;)V",
isInterface
);
return;
}
// 监控 Alarm Service
monitorClass = "com/pingan/bank/customplugin/AlarmMetrics";
if (!monitorClass.equals(className)
&& "android/app/AlarmManager".equals(owner)
&& opcode == Opcodes.INVOKEVIRTUAL
&& "set".equals(name)) {
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
monitorClass,
name,
"(Landroid/app/AlarmManager;IJLandroid/app/PendingIntent;)V",
isInterface
);
return;
}
if (!monitorClass.equals(className)
&& "android/app/AlarmManager".equals(owner)
&& opcode == Opcodes.INVOKEVIRTUAL
&& "cancel".equals(name)) {
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
monitorClass,
name,
"(Landroid/app/AlarmManager;Landroid/app/PendingIntent;)V",
isInterface
);
return;
}
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
缺点
系统的代码插桩方案无法替换。
===================================================================
最后
对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!
最后,我再重复一次,如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。
对于很多初中级 Android 工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。整理的这些架构技术希望对 Android 开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。
为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品 Android 架构师教程,保证你学了以后保证薪资上升一个台阶。
以下是今天给大家分享的一些独家干货:

评论