You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

225 lines
17 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 19 | 耗电优化(下):耗电的优化方法与线上监控
相比启动、卡顿、内存和网络的优化来说,可能大多数应用对耗电优化的关注不是太多。当然并不是我们不想做耗电优化,更多时候是感觉有些无从下手。
不同于启动时间、卡顿率耗电在线上一直缺乏一个可以量化的指标。Android系统通过计算获得的应用耗电数据只是一个估算值从Android 4.4开始,连这个估算值也无法拿到了。当有用户投诉我们应用耗电的时候,我们一般也无所适从,不知道该如何定位、如何分析。
耗电优化究竟需要做哪些工作?我们如何快速定位代码中的不合理调用,并且持续监控应用的耗电情况呢?今天我们就一起来学习耗电的优化方法和线上监控方案。
## 耗电优化
在开始讲如何做耗电优化之前,你需要先明确什么是耗电优化,做这件事情的目的究竟是什么。
**1\. 什么是耗电优化**
有些同学可能会疑惑所谓的耗电优化不就是减少应用的耗电增加用户的续航时间吗但是落到实践中如果我们的应用需要播放视频、需要获取GPS信息、需要拍照这些耗电看起来是无法避免的。
如何判断哪些耗电是可以避免或者是需要去优化的呢你可以看下面这张图当用户去看耗电排行榜的时候发现“王者荣耀”使用了7个多小时这时用户对“王者荣耀”的耗电是有预期的。
![](https://static001.geekbang.org/resource/image/5f/90/5f98c8a117745ce2fd7ef8f873894090.png)
假设这个时候发现某个应用他根本没怎么使用(前台时间很少),但是耗电却非常多。这种情况会跟用户的预期差别很大,他可能就会想去投诉。
**所以耗电优化的第一个方向是优化应用的后台耗电**。知道了系统是如何计算耗电的那反过来看我们也就可以知道应用在后台不应该做什么例如长时间获取WakeLock、WiFi和蓝牙的扫描等。为什么说耗电优化第一个方向就是优化应用后台耗电因为大部分厂商预装项目要求最严格的正是应用后台待机耗电。
![](https://static001.geekbang.org/resource/image/b0/2b/b01e359b45d22bd80efda51eee2f5f2b.png)
当然前台耗电我们不会完全不管,但是标准会放松很多。你再来看看下面这张图,如果系统对你的应用弹出这个对话框,可能对于微信来说,用户还可以忍受,但是对其他大多数的应用来说,可能很多用户就直接把你加入到后台限制的名单中了。
![](https://static001.geekbang.org/resource/image/c6/1b/c6d2c20c09e84190c7b4a64578d0cc1b.png)
**耗电优化的第二个方向是符合系统的规则,让系统认为你耗电是正常的**。而Android P是通过Android Vitals监控后台耗电所以我们需要符合Android Vitals的规则目前它的具体规则如下
![](https://static001.geekbang.org/resource/image/62/15/620748a58e45e50fdea1098f15c77d15.png)
虽然上面的标准可能随时会改变但是可以看到Android系统目前比较关心后台Alarm唤醒、后台网络、后台WiFi扫描以及部分长时间WakeLock阻止系统后台休眠。
**2\. 耗电优化的难点**
既然已经明确了耗电优化的目的和方向,那我们就开始动手吧。但我想说的是,只有当你跳进去的时候,才能发现耗电优化这个坑有多深。它主要有下面几个问题:
* **缺乏现场,无法复现**。用户上传某个截图你的应用耗电占比30%。通过电量的详细使用情况,我们可能会有一些猜测。但是用户也无法给出更丰富的信息,以及具体是在什么场景发生的,可以说是毫无头绪。
![](https://static001.geekbang.org/resource/image/7a/b2/7ae7234370738c60d2685c8b096a19b2.png)
* **信息不全,难以定位**。如果是开发人员或者厂商可以提供bug report利用Battery Historian可以得到非常全的耗电统计信息。但是Battery Historian缺失了最重要的堆栈信息代码调用那么复杂可能还有很多的第三方SDK我们根本不知道是哪一行代码申请了WakeLock、使用了Sensor、调用了网络等。
![](https://static001.geekbang.org/resource/image/8e/75/8e5d2527d61cefbd4e457deafde91c75.png)
* **无法评估结果**。通过猜测我们可能会尝试一些解决方案。但是从Android 4.4开始,我们无法拿到应用的耗电信息。尽管我们解决了某个耗电问题,也很难去评估它是否已经生效,以及对用户产生的价值有多大。
**3\. 耗电优化的方法**
无法复现、难以定位,也无法评估结果,耗电优化之路实在是不容易。在真正去做优化之前,先来看看我们的应用为什么需要在后台耗电?
大部分的开发者不是为了“报复社会”,故意去浪费用户的电量,主要可能有以下一些原因:
* **某个需求场景**。最普遍的场景就是推送,为了实现推送我们只能做各种各样的保活。在需求面前,用户的价值可能被排到第二位。
* **代码的Bug**。因为某些逻辑考虑不周可能导致GPS没有关闭、WakeLock没有释放。
所以相反地,耗电优化的思路也非常简单。
* **找到需求场景的替代方案**。以推送为例我们是否可以更多地利用厂商通道或者定时的拉取最新消息这种模式。如果真是迫不得已是不是可以使用foreground service或者引导用户加入白名单。后台任务的总体指导思想是**减少、延迟和合并**,可以参考微信一个小伙写的[《Android后台调度任务与省电》](https://blog.dreamtobe.cn/2016/08/15/android_scheduler_and_battery/)。在后台运行某个任务之前,我们都需要经过下面的思考:
![](https://static001.geekbang.org/resource/image/67/ac/67488fb06348423717cb0adba242bdac.png)
* **符合Android规则**。首先系统的大部分耗电监控,都是在手机在没有充电的时候。我们可以选择在用户充电时才去做一些耗电的工作,具体方法可查看官方文档[《监控电池电量和充电状态》](https://developer.android.com/training/monitoring-device-state/battery-monitoring?hl=zh-cn)。其次是尽早适配最新的Target API因为高版本系统后台限制本来就非常严格应用在后台耗电本身就变得比较困难了。
```
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;
```
* **异常情况监控**。即使是[最严格的Android P](https://mp.weixin.qq.com/s/APhUH7MBDUZ6tQv0xDgaWQ)系统也会允许应用部分地使用后台网络、Alarm以及JobSheduler事件[不同的分组,限制次数不同](https://developer.android.google.cn/topic/performance/power/power-details)。因此出现异常情况的可能性还是存在的更不用说低版本的系统。对于异常的情况我们需要类似Android Vitals电量监控一样将规则抽象出来并且增加上更多辅助我们定位问题的信息。
## 耗电监控
在I/O监控中我指定了重复I/O、主线程I/O、Buffer过大以及I/O泄漏这四个规则。对于耗电监控也是如此我们首先需要抽象出具体的规则然后收集尽量多的辅助信息帮助问题的排查。
**1\. Android Vitals**
前面已经说过Android Vitals的几个关于电量的监控方案与规则我们先复习一下。
* [Alarm Manager wakeup 唤醒过多](https://developer.android.com/topic/performance/vitals/wakeup)
* [频繁使用局部唤醒锁](https://developer.android.google.cn/topic/performance/vitals/wakelock)
* [后台网络使用量过高](https://developer.android.com/topic/performance/vitals/bg-network-usage)
* [后台WiFi scans过多](https://developer.android.com/topic/performance/vitals/bg-wifi)
在使用了一段时间之后我发现它并不是那么好用。以Alarm wakeup为例Vitals以每小时超过10次作为规则。由于这个规则无法做修改很多时候我们可能希望针对不同的系统版本做更加细致的区分。
其次跟Battery Historian一样我们只能拿到wakeup的标记的组件拿不到申请的堆栈也拿不到当时手机是否在充电、剩余电量等信息。
![](https://static001.geekbang.org/resource/image/33/1d/33aa19f951d577b759527c717c7d6e1d.png)
对于网络、WiFi scans以及WakeLock也是如此。虽然Vitals帮助我们缩小了排查的范围但是依然需要在茫茫的代码中寻找对应的可疑代码。
**2\. 耗电监控都监控什么**
Android Vitals并不是那么好用而且对于国内的应用来说其实也根本无法使用。不管怎样我们还是需要搭建自己的耗电监控系统。
那我们的耗电监控系统应该监控哪些内容怎么样才能比Android Vitals做得更好呢
* **监控信息**。简单来说系统关心什么,我们就监控什么,而且应该**以后台耗电监控为主**。类似Alarm wakeup、WakeLock、WiFi scans、Network都是必须的其他的可以根据应用的实际情况。如果是地图应用后台获取GPS是被允许的如果是计步器应用后台获取Sensor也没有太大问题。
* **现场信息**。监控系统希望可以获得完整的堆栈信息比如哪一行代码发起了WiFi scans、哪一行代码申请了WakeLock等。还有当时手机是否在充电、手机的电量水平、应用前台和后台时间、CPU状态等一些信息也可以帮助我们排查某些问题。
* **提炼规则**。最后我们需要将监控的内容抽象成规则,当然不同应用监控的事项或者参数都不太一样。
由于每个应用的具体情况都不太一样,下面是一些可以用来参考的简单规则。
![](https://static001.geekbang.org/resource/image/d4/be/d48b7e4d3fdceb101fa7716b5892b0be.png)
在安卓绿色联盟的会议中,华为公开过他们后台资源使用的“红线”,你也可以参考里面的一些规则:
![](https://static001.geekbang.org/resource/image/86/ff/86a65ea0d9216a11a341d7224fce93ff.png)
**2\. 如何监控耗电**
明确了我们需要监控什么以及具体的规则之后终于可以来到实现这个环节了。跟I/O监控、网络监控一样我首先想到的还是Hook方案。
**Java Hook**
Hook方案的好处在于使用者接入非常简单不需要去修改自己的代码。下面我以几个比较常用的规则为例看看如果使用Java Hook达到监控的目的。
* [WakeLock](https://developer.android.com/training/scheduling/wakelock)。WakeLock用来阻止CPU、屏幕甚至是键盘的休眠。类似Alarm、JobService也会申请WakeLock来完成后台CPU操作。WakeLock的核心控制代码都在[PowerManagerService](http://androidxref.com/7.0.0_r1/xref/frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java)中,实现的方法非常简单。
```
// 代理PowerManagerService
ProxyHook().proxyHook(context.getSystemService(Context.POWER_SERVICE), "mService", this)
@Override
public void beforeInvoke(Method method, Object[] args) {
// 申请Wakelock
if (method.getName().equals("acquireWakeLock")) {
if (isAppBackground()) {
// 应用后台逻辑,获取应用堆栈等等
} else {
// 应用前台逻辑,获取应用堆栈等等
}
// 释放Wakelock
} else if (method.getName().equals("releaseWakeLock")) {
// 释放的逻辑
}
}
```
* [Alarm](https://developer.android.com/training/scheduling/alarms)。Alarm用来做一些定时的重复任务它一共有四个类型其中[ELAPSED\_REALTIME\_WAKEUP](https://developer.android.com/reference/android/app/AlarmManager.html#ELAPSED_REALTIME_WAKEUP)和[RTC\_WAKEUP](https://developer.android.com/reference/android/app/AlarmManager.html#RTC_WAKEUP)类型都会唤醒设备。同样Alarm的核心控制逻辑都在[AlarmManagerService](http://androidxref.com/7.0.0_r1/xref/frameworks/base/services/core/java/com/android/server/AlarmManagerService.java)中,实现如下:
```
// 代理AlarmManagerService
new ProxyHook().proxyHook(context.getSystemService
(Context.ALARM_SERVICE), "mService", this)
public void beforeInvoke(Method method, Object[] args) {
// 设置Alarm
if (method.getName().equals("set")) {
// 不同版本参数类型的适配,获取应用堆栈等等
// 清除Alarm
} else if (method.getName().equals("remove")) {
// 清除的逻辑
}
}
```
* 其他。对于后台CPU我们可以使用卡顿监控学到的方法。对于后台网络同样我们可以通过网络监控学到的方法。对于GPS监控我们可以通过Hook代理[LOCATION\_SERVICE](http://androidxref.com/7.0.0_r1/xref/frameworks/base/services/core/java/com/android/server/LocationManagerService.java)。对于Sensor我们通过Hook [SENSOR\_SERVICE](http://androidxref.com/7.0.0_r1/xref/frameworks/base/core/java/android/hardware/SystemSensorManager.java)中的“mSensorListeners”可以拿到部分信息。
**通过Hook我们可以在申请资源的时候将堆栈信息保存起来。当我们触发某个规则上报问题的时候可以将收集到的堆栈信息、电池是否充电、CPU信息、应用前后台时间等辅助信息也一起带上。**
**插桩**
虽然使用Hook非常简单但是某些规则可能不太容易找到合适的Hook点。而且在Android P之后很多的Hook点都不支持了。
出于兼容性考虑我首先想到的是写一个基础类然后在统一的调用接口中增加监控逻辑。以WakeLock为例
```
public class WakelockMetrics {
// Wakelock 申请
public void acquire(PowerManager.WakeLock wakelock) {
wakeLock.acquire();
// 在这里增加Wakelock 申请监控逻辑
}
// Wakelock 释放
public void release(PowerManager.WakeLock wakelock, int flags) {
wakelock.release();
// 在这里增加Wakelock 释放监控逻辑
}
}
```
Facebook也有一个耗电监控的开源库[Battery-Metrics](https://github.com/facebookincubator/Battery-Metrics)它监控的数据非常全包括Alarm、WakeLock、Camera、CPU、Network等而且也有收集电量充电状态、电量水平等信息。
Battery-Metrics只是提供了一系列的基础类在实际使用中接入者可能需要修改大量的源码。但对于一些第三方SDK或者后续增加的代码我们可能就不太能保证可以监控到了。这些场景也就无法监控了所以Facebook内部是使用插桩来动态替换。
遗憾的是Facebook并没有开源它们内部的插桩具体实现方案。不过这实现起来其实并不困难事实上在我们前面的Sample中已经使用过ASM、Aspectj这两种插桩方案了。后面我也安排单独一期内容来讲不同插桩方案的实现。
插桩方案使用起来兼容性非常好并且使用者也没有太大的接入成本。但是它并不是完美无缺的对于系统的代码插桩方案是无法替换的例如JobService申请PARTIAL\_WAKE\_LOCK的场景。
## 总结
从Android系统计算耗电的方法我们知道了需要关注哪些模块的耗电。从Android耗电优化的演进历程我们知道了Android在耗电优化的一些方向以及在意的点。从Android Vitals的耗电监控我们知道了耗电优化的监控方式。
但是系统的方法不一定可以完全适合我们的应用,还是需要通过进一步阅读源码、思考,沉淀出一套我们自己的优化实践方案。这也是我的**性能优化方法论**,在其他的领域也是如此。
## 课后作业
在你的项目中,做过哪些耗电优化和监控的工作吗?你的实现方案是怎样的?欢迎留言跟我和其他同学一起讨论。
今天的课后练习是按照文中的思路使用Java Hook实现Alarm、WakeLock和GPS的耗电监控。具体的规则跟文中表格一致请将完善后的代码通过Pull requests提交到[Chapter19](https://github.com/AndroidAdvanceWithGeektime/Chapter19)中。
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。最后别忘了在评论区提交今天的作业,我也为认真完成作业的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。