App电量优化

1,479 阅读6分钟

耗电的背景知识

电池的关键指标:

  • 电池容量。
  • 充电时间。
  • 寿命。随着使用时间的延长,电池容量会降低。例如苹果官方数据,500 次充电循环 iPhone 电池的剩余容量为原来的 80%。
  • 安全性。三星 Note 7 爆炸。

耗电模块

电能 = 电压 * 电流 * 时间

power_profiler.xml 文件定义了不同模块的电流消耗值以及该模块在一段时间内大概消耗的电流。可以参考官方文档《Android 电源配置文件》。电流的大小和当前模块的状态也有关系。

屏幕

adb shell dumpsys batterystats > battery.txt
// 各个 Uid 的总耗电量,而且是粗略的电量计算估计。
Estimated power use (mAh):
    Capacity: 3450, Computed drain: 501, actual drain: 552-587
    ...
    Idle: 41.8
    Uid 0: 135 ( cpu=103 wake=31.5 wifi=0.346 )
    Uid u0a208: 17.8 ( cpu=17.7 wake=0.00460 wifi=0.0901 )
    Uid u0a65: 17.5 ( cpu=12.7 wake=4.11 wifi=0.436 gps=0.309 )
    ...

// reset 电量统计
adb shell dumpsys batterystats --reset

BatteryStatsService 是对外的电量统计服务,具体实现逻辑在 BatteryStatsImpl 中。BatteryStatsImpl 内部使用的就是 PowerProfile,为每个应用创建一个 UID 实例来监控应用系统资源的使用情况。

优化案例:《大众点评 App 的短视频耗电量优化实战》

反馈耗电问题时,bug reportBattery Historian 是最好的排查方法。

//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

Android 每年都在为了电量优化做努力,在整个 Android 的发展过程中,分几个阶段:

  • 野蛮生长:Pre Android 5.0。
  • 逐步收紧:Android 5.0~8.0。5.0 专门开启一个 Volta 项目,改善电池的续航。
  • 最严限制:Android 9.0。9.0 开始的电源管理,引入了更加严格的限制。

耗电优化

耗电优化的优先级:

  • 优化应用的后台耗电,符合用户心理预期。

  • 符合系统规则,让系统认为你的耗电正常,避免弹出“高耗电警告”。

耗电优化的难点:

  • 缺乏现场,无法复现。
  • 信息不全,难以定位。统计无法定位到堆栈,无法直接定位到具体原因。
  • 无法评估结果。

耗电的主要原因:

  • 需求导向,保活。例如:推送、后台任务。
  • 代码的 Bug。GPS 没有关闭、WakeLock 没有释放。

从主要问题出发,整理优化思路:

Android Vitals 在过年其实根本无法使用。

参考:

Alarm Manager Wakeup 唤醒过多

频繁使用局部唤醒锁

后台网络使用量过高

后台 WiFi scans 过多

华为的耗电红线

监控耗电的方法:

  • Java Hook。主要监控 WeakLockAlarm,以及一些后台时硬件的使用。

// 代理 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")) {
       // 释放的逻辑    
    }
}

// 代理 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")) {
        // 清除的逻辑
    }
}

  • 插桩。解决 Android P 之后,很多 Hook 的手段不支持的问题。参考 Facebook 的耗电监控开源库 Battery-Metrics

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 释放监控逻辑
    }
}

步骤

实现

Hook

实现了IAlarmManagerHook、IPowerManagerHook、ILocationManagerHook这三那个代理,分别实现对AlarmManager、PowerManager、LocationManager的监听。

public class BatteryHookManager {

    /**
     * 初始化
     * @param context
     */
    public void initHook(Context context){
        //初始化日志
        BatteryLogUtil.init(context);

        //初始化Hook
        new IAlarmManagerHook(context).onInstall();
        new IPowerManagerHook(context).onInstall();
        new ILocationManagerHook(context).onInstall();
    }

    private BatteryHookManager(){}
    private static class BatteryHookManagerHolder{
        private static final BatteryHookManager INSTANCE= new BatteryHookManager();
    }

    public static BatteryHookManager getImp(){
        return BatteryHookManagerHolder.INSTANCE;
    }

}

在项目的application里面初始化BatteryHook

public class BatteryApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        BatteryHookManager.getImp().initHook(this);

    }
}

检测错误的UI绘制刷新导致的耗电

排除由于错误的绘制方法,导致CPU占用过高,进而导致耗电量高

检测方式参考大众点评App的短视频耗电量优化实战

首先打开开发者选项,打开GPU视图更新的开关,然后看看应用内部有哪些不必要的UI刷新,

场景一(自定义TextView)

首页快速组队页面,列表中查看更多在一直刷新,直接看代码,

这里有一个自定义View,继续看TextViewWideContent的代码

分析代码发现在onDraw()里面调用了setPadding(),继续看setPadding()的源码,

可以看到这里面调用了页面invalidate(),这就导致了onDraw()方法的循环调用,所以页面会持续刷新。结论就是在自定义View的onDraw()方法里面,调用setPadding(),会导致页面重复绘制 解决办法也很简单把setPadding放到onLayout()里面去。

场景二(CoordinatorLayout+AppBarLayout错误的依赖关系)

在CoordinatorLayout+AppBarLayout页面结构中,错误的将底部依赖于头部控件,导致底部一直在刷新,去掉 app:layout_anchor="@id/user_appbar_layout" 就好了。

Android电量优化的相关建议

①在需要网络连接的程序中,首先检查网络连接是否正常,如果没有网络连接,那么就不需要执行相应的程序;

②判断网络类型,针对特定的数据在特定的网络下请求.例如:大量数据传输的时候在wifi下请求;wifi下下载数据耗电量只有2、3、4G的1/3.

③使用效率高的数据格式和解析方法,推荐使用JSON和Protobuf;

④在进行大数据量下载时,尽量使用GZIP方式下载;

⑤使用推送,代替循环请求

⑥其它:

尽量不要使用浮点运算;

回收java对象,特别是较大的java对像,使用reset方法;

主动回收java对象,特别是较大的,例如bitmap。减少GC的工作频率;

避免内存抖动,内存抖动是因为大量的对象被创建又在短时间内马上被释放;

避免在for循环、onDraw方法中创建对象;无法避免的可以创建对象池,然后在不使用的时候释放;

对定位要求不是太高的话尽量不要使用GPS定位,可以使用wifi和移动网络cell定位即可;

获取屏幕尺寸等信息可以使用缓存技术,不需要进行多次请求;

使用AlarmManager来定时启动服务替代使用sleep方式的定时任务;