写点什么

你是否了解 APP 耗电问题?深入探索 Android 电量优化,android 物联网开发李天祥

作者:嘟嘟侠客
  • 2021 年 11 月 28 日
  • 本文字数:6869 字

    阅读完需:约 23 分钟

作用


将电量测量转化为功能模块的使用时间或者次数。


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


4、Battery Historian




特点


  • 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



选择多个文件进行上传对比。


5、电量专项测试




1)、耗电场景测试


  • 复杂计算。

  • 音视频播放。


2)、传感器相关


  • 使用时长

  • 耗电量

  • 发热


3)、后台静默测试


四、耗电优化


===============================================================


1、耗电优化的难点




  • 1)、「缺乏现场,无法复现」

  • 2)、「信息不全,难以定位」

  • 3)、「无法评估结果」


在 App 开发中,经常会由于某个需求场景或 代码 bug 而导致大量耗电。


2、后台调度任务省电




思考步骤


  • 需要后台运行

  • 长时间下载: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;


3、电量优化套路总结



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 状态信息」


最后,我们需要 「提炼规则,将监控内容 => 抽象成规则」


1、Java Hook




我们可以通过代理对应的 Service 实现,完成收集 Wakelock、Alarm、GPS 的申请堆栈、释放信息、手机充电状态等等。


?



示例项目



?


2、电量辅助监控实战



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));


}


3、编译插桩




「写一个基础类,然后在统一的调用接口中添加监控逻辑」。这里我们可以参考 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);


}


缺点


系统的代码插桩方案无法替换。


六、电量优化常见问题


===================================================================


1、怎么做电量测试?



最后

对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!


最后,我再重复一次,如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究


对于很多初中级 Android 工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。整理的这些架构技术希望对 Android 开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。


为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品 Android 架构师教程,保证你学了以后保证薪资上升一个台阶。


以下是今天给大家分享的一些独家干货:



本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

用户头像

嘟嘟侠客

关注

还未添加个人签名 2021.03.19 加入

还未添加个人简介

评论

发布
暂无评论
你是否了解APP耗电问题?深入探索 Android 电量优化,android物联网开发李天祥