<audio id="audio" title="19 | 耗电优化（下）：耗电的优化方法与线上监控" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/73/93/73a7c411cd7a790e4f8bc142c408fe93.mp3"></audio>

相比启动、卡顿、内存和网络的优化来说，可能大多数应用对耗电优化的关注不是太多。当然并不是我们不想做耗电优化，更多时候是感觉有些无从下手。

不同于启动时间、卡顿率，耗电在线上一直缺乏一个可以量化的指标。Android系统通过计算获得的应用耗电数据只是一个估算值，从Android  4.4开始，连这个估算值也无法拿到了。当有用户投诉我们应用耗电的时候，我们一般也无所适从，不知道该如何定位、如何分析。

耗电优化究竟需要做哪些工作？我们如何快速定位代码中的不合理调用，并且持续监控应用的耗电情况呢？今天我们就一起来学习耗电的优化方法和线上监控方案。

## 耗电优化

在开始讲如何做耗电优化之前，你需要先明确什么是耗电优化，做这件事情的目的究竟是什么。

**1. 什么是耗电优化**

有些同学可能会疑惑，所谓的耗电优化不就是减少应用的耗电，增加用户的续航时间吗？但是落到实践中，如果我们的应用需要播放视频、需要获取GPS信息、需要拍照，这些耗电看起来是无法避免的。

如何判断哪些耗电是可以避免，或者是需要去优化的呢？你可以看下面这张图，当用户去看耗电排行榜的时候，发现“王者荣耀”使用了7个多小时，这时用户对“王者荣耀”的耗电是有预期的。

<img src="https://static001.geekbang.org/resource/image/5f/90/5f98c8a117745ce2fd7ef8f873894090.png" alt="">

假设这个时候发现某个应用他根本没怎么使用（前台时间很少），但是耗电却非常多。这种情况会跟用户的预期差别很大，他可能就会想去投诉。

**所以耗电优化的第一个方向是优化应用的后台耗电**。知道了系统是如何计算耗电的，那反过来看，我们也就可以知道应用在后台不应该做什么，例如长时间获取WakeLock、WiFi和蓝牙的扫描等。为什么说耗电优化第一个方向就是优化应用后台耗电，因为大部分厂商预装项目要求最严格的正是应用后台待机耗电。

<img src="https://static001.geekbang.org/resource/image/b0/2b/b01e359b45d22bd80efda51eee2f5f2b.png" alt="">

当然前台耗电我们不会完全不管，但是标准会放松很多。你再来看看下面这张图，如果系统对你的应用弹出这个对话框，可能对于微信来说，用户还可以忍受，但是对其他大多数的应用来说，可能很多用户就直接把你加入到后台限制的名单中了。

<img src="https://static001.geekbang.org/resource/image/c6/1b/c6d2c20c09e84190c7b4a64578d0cc1b.png" alt="">

**耗电优化的第二个方向是符合系统的规则，让系统认为你耗电是正常的**。而Android P是通过Android Vitals监控后台耗电，所以我们需要符合Android Vitals的规则，目前它的具体规则如下：

<img src="https://static001.geekbang.org/resource/image/62/15/620748a58e45e50fdea1098f15c77d15.png" alt="">

虽然上面的标准可能随时会改变，但是可以看到，Android系统目前比较关心后台Alarm唤醒、后台网络、后台WiFi扫描以及部分长时间WakeLock阻止系统后台休眠。

**2. 耗电优化的难点**

既然已经明确了耗电优化的目的和方向，那我们就开始动手吧。但我想说的是，只有当你跳进去的时候，才能发现耗电优化这个坑有多深。它主要有下面几个问题：

- **缺乏现场，无法复现**。用户上传某个截图，你的应用耗电占比30%。通过电量的详细使用情况，我们可能会有一些猜测。但是用户也无法给出更丰富的信息，以及具体是在什么场景发生的，可以说是毫无头绪。

<img src="https://static001.geekbang.org/resource/image/7a/b2/7ae7234370738c60d2685c8b096a19b2.png" alt="">

- **信息不全，难以定位**。如果是开发人员或者厂商可以提供bug report，利用Battery Historian可以得到非常全的耗电统计信息。但是Battery Historian缺失了最重要的堆栈信息，代码调用那么复杂，可能还有很多的第三方SDK，我们根本不知道是哪一行代码申请了WakeLock、使用了Sensor、调用了网络等。

<img src="https://static001.geekbang.org/resource/image/8e/75/8e5d2527d61cefbd4e457deafde91c75.png" alt="">

- **无法评估结果**。通过猜测，我们可能会尝试一些解决方案。但是从Android 4.4开始，我们无法拿到应用的耗电信息。尽管我们解决了某个耗电问题，也很难去评估它是否已经生效，以及对用户产生的价值有多大。

**3. 耗电优化的方法**

无法复现、难以定位，也无法评估结果，耗电优化之路实在是不容易。在真正去做优化之前，先来看看我们的应用为什么需要在后台耗电？

大部分的开发者不是为了“报复社会”，故意去浪费用户的电量，主要可能有以下一些原因：

<li>
**某个需求场景**。最普遍的场景就是推送，为了实现推送我们只能做各种各样的保活。在需求面前，用户的价值可能被排到第二位。
</li>
<li>
**代码的Bug**。因为某些逻辑考虑不周，可能导致GPS没有关闭、WakeLock没有释放。
</li>

所以相反地，耗电优化的思路也非常简单。

- **找到需求场景的替代方案**。以推送为例，我们是否可以更多地利用厂商通道，或者定时的拉取最新消息这种模式。如果真是迫不得已，是不是可以使用foreground service或者引导用户加入白名单。后台任务的总体指导思想是**减少、延迟和合并**，可以参考微信一个小伙写的[《Android后台调度任务与省电》](https://blog.dreamtobe.cn/2016/08/15/android_scheduler_and_battery/)。在后台运行某个任务之前，我们都需要经过下面的思考：

<img src="https://static001.geekbang.org/resource/image/67/ac/67488fb06348423717cb0adba242bdac.png" alt="">

- **符合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的几个关于电量的监控方案与规则，我们先复习一下。

<li>
[Alarm Manager wakeup 唤醒过多](https://developer.android.com/topic/performance/vitals/wakeup)
</li>
<li>
[频繁使用局部唤醒锁](https://developer.android.google.cn/topic/performance/vitals/wakelock)
</li>
<li>
[后台网络使用量过高](https://developer.android.com/topic/performance/vitals/bg-network-usage)
</li>
<li>
[后台WiFi scans过多](https://developer.android.com/topic/performance/vitals/bg-wifi)
</li>

在使用了一段时间之后，我发现它并不是那么好用。以Alarm wakeup为例，Vitals以每小时超过10次作为规则。由于这个规则无法做修改，很多时候我们可能希望针对不同的系统版本做更加细致的区分。

其次跟Battery Historian一样，我们只能拿到wakeup的标记的组件，拿不到申请的堆栈，也拿不到当时手机是否在充电、剩余电量等信息。

<img src="https://static001.geekbang.org/resource/image/33/1d/33aa19f951d577b759527c717c7d6e1d.png" alt="">

对于网络、WiFi scans以及WakeLock也是如此。虽然Vitals帮助我们缩小了排查的范围，但是依然需要在茫茫的代码中寻找对应的可疑代码。

**2. 耗电监控都监控什么**

Android Vitals并不是那么好用，而且对于国内的应用来说其实也根本无法使用。不管怎样，我们还是需要搭建自己的耗电监控系统。

那我们的耗电监控系统应该监控哪些内容，怎么样才能比Android Vitals做得更好呢？

<li>
**监控信息**。简单来说系统关心什么，我们就监控什么，而且应该**以后台耗电监控为主**。类似Alarm wakeup、WakeLock、WiFi scans、Network都是必须的，其他的可以根据应用的实际情况。如果是地图应用，后台获取GPS是被允许的；如果是计步器应用，后台获取Sensor也没有太大问题。
</li>
<li>
**现场信息**。监控系统希望可以获得完整的堆栈信息，比如哪一行代码发起了WiFi scans、哪一行代码申请了WakeLock等。还有当时手机是否在充电、手机的电量水平、应用前台和后台时间、CPU状态等一些信息也可以帮助我们排查某些问题。
</li>
<li>
**提炼规则**。最后我们需要将监控的内容抽象成规则，当然不同应用监控的事项或者参数都不太一样。
</li>

由于每个应用的具体情况都不太一样，下面是一些可以用来参考的简单规则。

<img src="https://static001.geekbang.org/resource/image/d4/be/d48b7e4d3fdceb101fa7716b5892b0be.png" alt="">

在安卓绿色联盟的会议中，华为公开过他们后台资源使用的“红线”，你也可以参考里面的一些规则：

<img src="https://static001.geekbang.org/resource/image/86/ff/86a65ea0d9216a11a341d7224fce93ff.png" alt="">

**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), &quot;mService&quot;, this)；

@Override
public void beforeInvoke(Method method, Object[] args) {
    // 申请Wakelock
    if (method.getName().equals(&quot;acquireWakeLock&quot;)) {
        if (isAppBackground()) {
            // 应用后台逻辑，获取应用堆栈等等     
         } else {
            // 应用前台逻辑，获取应用堆栈等等
         }
    // 释放Wakelock
    } else if (method.getName().equals(&quot;releaseWakeLock&quot;)) {
       // 释放的逻辑    
    }
}

```

- [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), &quot;mService&quot;, this)；

public void beforeInvoke(Method method, Object[] args) {
    // 设置Alarm
    if (method.getName().equals(&quot;set&quot;)) {
        // 不同版本参数类型的适配，获取应用堆栈等等
    // 清除Alarm
    } else if (method.getName().equals(&quot;remove&quot;)) {
        // 清除的逻辑
    }
}

```

- 其他。对于后台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)中。

欢迎你点击“请朋友读”，把今天的内容分享给好友，邀请他一起学习。最后别忘了在评论区提交今天的作业，我也为认真完成作业的同学准备了丰厚的“学习加油礼包”，期待与你一起切磋进步哦。


