在 Android 应用开发中,咱们须要考虑的是如何优化电量使用,让咱们的 App 不会由于电量消耗太高被用户排斥,或者被其余安全应用报告,以此确保用户黏性。html
开发中一直链接手机,不知道电量消耗有多快。java
咱们没有办法拿到每个用户手机的组件能耗,其中不一样的硬件模块使用了不一样的参数,而后使用了不一样的算法来进行估算。可是,具体的参数值根据手机所使用的硬件来讲是不同的。python
如今通常手机的电池容量会占用内部组件将近一半的空间。android
P=UI(电功率=电压 * 电流)git
一般使用充电循环次数衡量。github
严格控制电池容量,例如 VOOC 就使用了各类安全检测技术。web
分批有效地收集和传递传感器事件。算法
批处理在合理的类似时间内的全部应用的闹铃,以便系统仅唤醒一次。docker
对于电量的统计有一个公式,以下所示:shell
模块电量(mAh) = 模块电流(mA)* 模块耗时(h)
复制代码
Android 系统要求 ROM 厂商必须在 /frameworks/base/core/res/res/xml/power_profile.xml 提供组件的电源配置文件。而 Android 系统的电量计算 PowerProfile 正是经过读取 power_profile.xml 的数据。
获取电池电量、充电状态、电池状态等信息。
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_BATTERY_CHANGED); Intent intent = registerReceiver(null, filter); LogUtils.i("battery " + intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)); 复制代码
batterystats 是 Android 5.0 提供的工具,它能够获取各个 App 的 WakeLock、CPU 时间占用等信息,同时增长了一个 Estimated power use(mAh)功能,预估耗电量。
将电量测量转化为功能模块的使用时间或者次数。
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 大模块的电量消耗,以下所示:
若是打不开,可使用备用网站 bathist
在 Add Metrics 中咱们能够增长更多的测量项。
若是一直处于 running,则代表电量消耗比较高。
选中 Job Scheduler 的某一个工做时间片,咱们能够查看具体的 发生的时间、耗时以及次数,最重要的是它统计出来了是哪个进程在使用这个 JobScheduler。
选择多个文件进行上传对比。
在 App 开发中,常常会因为某个需求场景或 代码 bug 而致使大量耗电。
对于耗电优化中,咱们最经常使用的就是 JobScheduler,下面👇,咱们来实战一下。
/** * 开启 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; } } 复制代码
符合 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; 复制代码
避免后台长时间获取 WakeLock、WIFI 和蓝牙的扫描等。
Android P 使用了 Android Vitals 监控后台耗电,其规则以下所示:
「Android 手机保护 AP 和 BP 两个 CPU。AP 即 Application Processor,全部的用户界面以及 App 都是运行在 AP 上的。BP 即 Baseband Processor,手机射频都是运行在这个 CPU 上的。而通常咱们所说的耗电,PowerProfile 文件里面的 CPU,指的是 AP」。
CPU 耗电一般有两种状况:
经常使用优化 CPU 时间片的方式有:
一般状况下,使用 WIFI 链接网络时的功耗要低于使用移动网络的功耗。而使用移动网络传输数据,电量的消耗有如下3种状态:
所以,为了不网络链接所带来的电量消耗,咱们能够采用以下几种方案:
WakeLock 经常使用于后台播放音视频、录制音视频、下载文件的状况。若是没有合理使用 WakeLock,则会形成严重的耗电问题,为了不该问题,「咱们应该按期针对使用了 WakeLock 的模块进行重点排查」。
咱们可使用 adb shell dumpsys power
命令查看系统当前的耗电信息,其中咱们能够看到 WakeLock 列表,它一般会以 「”mLocks.size“ 或者 ”Wake Locks:size“」 开头。关于 WakeLock 的使用咱们要着重注意如下几点:
「浮点运算比整数运算更消耗 CPU 时间片,所以耗电也会增长」。避开浮点运算的优化方法以下所示:
「咱们能够监听灭屏以及亮屏的广播,在灭屏的时候中止 surfaceView 的动画绘制。在亮屏的时候,恢复动画的绘制」。
之后台耗电监控为主,必须监控的模块有:
「必须监控的现场信息有」 :
最后,咱们须要 「提炼规则,将监控内容 => 抽象成规则」。
咱们能够经过代理对应的 Service 实现,完成收集 Wakelock、Alarm、GPS 的申请堆栈、释放信息、手机充电状态等等。
这里咱们就以 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/ss/android/ugc/bytex/example/battery_monitor/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/ss/android/ugc/bytex/example/battery_monitor/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); } 复制代码
系统的代码插桩方案没法替换。
电量相关的测试相对来讲难度较大,由于 App 在具体手机上的耗电量没法准确统计,每个手机所使用的硬件不同,那么它相应的功耗就不同。并且这个功耗值咱们只能在线下经过导出手机的 power_profile.xml 文件拿到。
因为咱们没法获取准确的耗电量,因此咱们只能增长多个维度来辅助判断 App 是否耗电。
最后,咱们能够分场景各个突破。
关于电量测试,咱们能够针对各个功能场景进行针对性的专项测试。操做一段时间后,咱们能够在手机设置—电量消耗里面,利用其数据做为判断依据。这样虽然直观,但精确度不行。
介绍 Battery Historian:
由于咱们不能在线上统计出 App 的电量消耗,所以须要在尽可能保证 App 在正常使用下的耗电。对此咱们采起了一系列的电量优化措施:
根据场景谨慎地选择传感器使用的模式,好比说在使用 GPS 的时候通常要避免使用高精度的模式,或者是尽可能复用上一次的定位结果。
咱们在实际项目中使用 WakeLock 有几个注意事项,第一,acquire、release 要成对地释放,第二,尽可能使用 acquire 的超时方法来设置超时时间,避免由于异常状况从而致使 WakeLock 而没法释放的状况,第三,关于 WakeLock 的释放必定要写在 try-catch-finally 的 finally 当中,保证 WakeLock 在异常状况下的释放。
JobScheduler 能够容许开发者在符合某些条件下创造执行在后台的任务,咱们能够设置执行一些耗电操做的场景,好比说 处于 WIFI 状态下同时链接电源 的状况下。同时,要注意用户在离开界面后,要避免耗电的操做,好比说中止播放动画。经过这些操做,咱们的 App 就不会比以前耗电了。
对于电量优化来讲,最重要的就是 「创建监控与自动化报警的一整套体系,只有发现了耗电的问题所在,才能使用针对性的解决措施」。
个人公众号 JsonChao
开通啦,欢迎关注~
❝欢迎关注个人微信:
❞bcce5360
❝「因为微信群人数过多,麻烦你们想进微信群的朋友们,加我微信拉你进群。」
❞
❝2千人QQ群,「Awesome-Android学习交流群,QQ群号:959936182」, 欢迎你们加入~
❞