详解Android电量优化

目录

写在前面

一、电量优化介绍及方案选择

1.1、如何正确认识电量优化

1.2、耗电量测试方案

二、Battery Historian实战分析

三、电量辅助监控实战

3.1、获取运行时能耗

3.2、运行时获取使用时长

3.3、获取线程使用时长

四、电量优化套路


写在前面

上半年刚刚过去,下半年已经开启,时间过的是真快,骚年们,加油吧!

各位小伙伴们早上好!今天继续Android性能优化专题的学习,上一篇中介绍了关于Android网络优化相关的内容,没看过的小伙伴可以去看看哦——《深入探索Android网络优化》,今天咱们继续学习另外一项优化Android的电量优化,一起开始吧!

一、电量优化介绍及方案选择

1.1、如何正确认识电量优化

  • 电量问题重视度不够:开发中一直连接手机
  • 电量消耗线上难以量化

首先需要说明的是对于一般项目电量的关注度是远远不够的,因为我们开发阶段一般都会将手机和电脑相连接进行调试,这个过程实际上也是在给手机充电的,所以我们根本不知道电量减少的速度和总量。然后是对于线上用户基本上也不可能量化出来究竟消耗了多少电量,因为我们无法收集到用户的设备能耗,耗电统计是一个系统组件,伴随着系统运行的整个过程,是基于软件层面实现的,不同的硬件模块配置了不同的参数,然后使用算法进行估算,具体的参数值根据手机使用不同的硬件都是不一样的,所以电量的消耗在线上是难以量化的。

1.2、耗电量测试方案

①、设置——耗电排行

  • 手机设置界面打开耗电排行,会列出每个app的耗电情况,这种方案比较直观,适合线下简单使用,但是它没有详细数据,对解决问题没有太多帮助
  • 找特定场景专项测试:比如在某个页面操作一段时间,再来观察耗电量增加情况,简单判断某个场景是否耗电

②、注册电量相关广播——ACTION_BATTERY_CHANGED

  • 获取电池电量、充电状态、电池状态等信息

下面我们使用这种方式在项目中简单的实践一下,我们来打印一下当前手机的电量是多少:

IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
Intent intent = registerReceiver(null,filter);
LogUtils.i("battery--->当前电量为:"+intent.getIntExtra(BatteryManager.EXTRA_LEVEL,-1));

通过对比日志中打印出的电量和手机截屏的电量发现是一致的:

 

该方案作总结如下:

  • 价值不大:针对手机整体的耗电量,而非特定App
  • 实时性差,精度较低,只能等待系统通知

③、Battery Historian

  • 谷歌推出的一款Android系统电量分析工具
  • 支持5.0(API21)及以上系统的电量分析

该方案具体的使用方法在下面会具体介绍,现在简单的对这种方案做个总结:

  • 功能强大,推荐使用
  • 可视化展示指标:耗电比例、执行时间、次数,以及查看特定App的耗电组件使用情况
  • 适合线下使用

④、关于测试

上面说到了我们并不能拿到精准的耗电量,这就要求我们的测试方法要能针对具体的耗电场景进行测试:

  • 耗电场景测试:重点测试复杂运算、动效较多、视频播放等模块
  • 传感器相关:比如:使用时长、耗电量以及某些模块运行之后手机是否发热等
  • 后台静默测试:App放在后台一段时间观察耗电量的变化

二、Battery Historian实战分析

这个工具并没有集成在Android Studio中,需要我们自行安装:

 导出电量信息

  • 重置电量信息:adb shell dumpsys batterystats --reset
  • 开始记录电量:adb shell dumpsys batterystats --enable full-wake-history
  • 导出电量信息:adb bugreport bugreport.zip(这个过程可能会很慢,耐心等待)

上传分析

  • http://localhost:9999
  • 上传bugreport文件
  • 备用分析网站(需要翻墙):https://bathist.ef.lc/

下面来使用网站进行分析,能翻墙的小伙伴可以使用这种方式,无需安装Battery Historian,直接上传即可:

上传完成之后会自动进行分析,分析完成之后会生成图形化界面进行显示,左边是列出了相对耗电组件的运行情况,CPU的时间线说明CPU一直处于工作状态耗电几率则比较高,将鼠标移到Top app中这里面可以看到也列出了我们的Demo工程以及电量情况运行时间等信息,如下图所示:

左下角有一个App Selection,这里就是你可以选择特定的App,展示的电量消耗就只有选定的App了,我这里为了演示操作的时间并不长,所以没有明显的数据,这里就随便选择一个支付宝的app,其中Battery Percentage Consumed展示的就是电量消耗情况,Userspace Wakelocks是展示的WakeLock的使用情况:

三、电量辅助监控实战

3.1、获取运行时能耗

Android系统中手机的每个组件:比如CPU、GPS、显示屏、WIFI、蓝牙等等,它们运行时的能耗都保存在power_profile.xml这个文件中,可以通过adb pull的方式将这个文件导出,然后进行反编译,通过读取文件就可以获取各个组件的能耗值:

  • adb pull /system/framework/framework-res.apk
  • 反编译,xml——>>power_profile

下面我们就来实际操作一下:这个操作不需要额外的权限,通过这个命令就可以直接将这个apk导出到你的命令当前文件夹:

接着来反编译这个apk:关于反编译的方法我这里就不详细说了,毕竟不是本文的重点,不知道的可以去网上百度一下,执行下面的命令:

然后它就开始进行反编译了,耐心等待反编译完成即可:

然后找到反编译完成的文件夹的res/xml文件夹下的power_profile.xml文件:

注意:线上的这个文件我们是获取不到的,这些值我们也就获取不到,并且每个手机厂商所使用的硬件不一样,所以每个厂商设定的值都是不一样的,这也是我们没有办法在线上获取精准电量统计的原因之一。下面来看一下刚刚反编译出的这个文件:

这里面就列出了每个组件的能耗,了解这个过程虽然不能帮助我们精准的统计线上的电量消耗值,但是在平常App耗电量的测试以及开发过程中还是很有帮助的,因为获取到了这些值就可以清楚的知道在这个手机上哪些模块比较耗电,哪些模块在哪种状态下耗电最高,在测试的时候则重点关注调用了这些模块的地方。

3.2、运行时获取使用时长

  • AOP辅助统计:次数、时间(获取耗电组件的调用位置、调用堆栈、执行时间等信息)

下面以WakeLock的使用为例在项目中演示一下具体的统计方法:wakelock是Android中的一种锁机制,主要是相对系统的休眠而言的,如果应用程序给CPU加了这个锁那么系统就不会休眠,这样做可以更好的配合程序的运行。

首先新建一个WakeLock的使用工具类,后续使用wakelock服务就直接调用该类即可:

public class WakeLockUtils {

    private static PowerManager.WakeLock sWakeLock;

    public static void acquire(Context context){
        if(sWakeLock == null){
            sWakeLock = createWakeLock(context);
        }
        if(sWakeLock != null && !sWakeLock.isHeld()){
            sWakeLock.acquire();
            sWakeLock.acquire(1000);
        }
    }

    public static void release(){
        // 一些逻辑
        try{

        }catch (Exception e){

        }finally {
            // 为了演示正确的使用方式
            if(sWakeLock != null && sWakeLock.isHeld()){
                sWakeLock.release();
                sWakeLock = null;
            }
        }
    }

    @SuppressLint("InvalidWakeLockTag")
    private static PowerManager.WakeLock createWakeLock(Context context){
        PowerManager pm = (PowerManager) context.getApplicationContext().getSystemService(Context.POWER_SERVICE);
        if(pm != null){
            return pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,"");
        }
        return null;
    }

}

然后找个地方进行调用:

WakeLockUtils.acquire(MainActivity.this);
new Handler().postDelayed(() -> WakeLockUtils.release(),200);

OK,写完了这些,我们就可以来写统计的代码了,这里采用AOP的方式进行统计,AOP框架采用的是Lancet,这是一个轻量级的AOP框架并且不会对包体积有任何的影响,关于Lancet的用法我在Android卡顿优化分析中实现界面秒开率统计的时候已经介绍过了,不了解的可以先去看一下它的用法或者直接去Github官网上看也行:

《Android卡顿优化分析及解决方案之界面秒开率统计》

GitHub地址:https://github.com/eleme/lancet

这里我们直接实现acquire和release方法即可,并且在release方法中统计耗时及堆栈信息:

    public static long sStartTime = 0;
    public static String trace;
    public static Handler mHandler = new Handler();
    public static Runnable releaseRun = WakeLockUtils::release;

    @Insert(value = "acquire") //需要统计的方法
    @TargetClass(value = "com.jarchie.performance.wakelock.WakeLockUtils",scope = Scope.SELF) //需要监控的是哪个类
    public static void acquire(Context context){
        //获取是哪个业务方调用的
        trace = Log.getStackTraceString(new Throwable());
        sStartTime = System.currentTimeMillis();
        Origin.callVoid();
        mHandler.postDelayed(releaseRun,1000);
    }

    @Insert(value = "release")
    @TargetClass(value = "com.jarchie.performance.wakelock.WakeLockUtils",scope = Scope.SELF)
    public static void release(){
        //打印时间和调用堆栈
        LogUtils.i("PowerManager "+(System.currentTimeMillis() - sStartTime)+"/n"+trace);
        Origin.callVoid();
    }

我们运行程序之后就能够打印出来时间和具体的调用堆栈了:

3.3、获取线程使用时长

  • 超过阈值预警

如果某个线程的使用时长过长,那它可能就是处于死循环的状态,然后就可以将这种信息记录下来进行上报分析确认,这里还是使用AOP的方式去获取时间及堆栈:

    public static long runTime = 0;

    @Insert(value = "run")
    @ImplementedInterface(value = "java.lang.Runnable",scope = Scope.ALL)
    public void run(){
        trace = Log.getStackTraceString(new Throwable());
        runTime = System.currentTimeMillis();
        Origin.callVoid();
        LogUtils.i("runTime "+(System.currentTimeMillis() - runTime)+"/n"+trace);
    }

这样我们就统计了出了每个线程的执行时间,我这里截取了其中一条的日志信息:

四、电量优化套路

①、CPU时间片

如果由于我们的App导致CPU一直处于工作状态,这样肯定是非常消耗电量的,之前介绍过获取CPU信息的方式:

  • 通过traceview或cpu profiler工具来获取app运行过程中的cpu信息,定位cpu占有率异常方法
  • 减少后台应用的主动运行

②、网络相关

  • 控制发起网络请求的时机及次数:在发送请求的时候可以区分这个请求是否紧急,是否可以延迟,可以延迟的网络请求可以针对性的绑定到一起来发送,这样可以减少网络被激活的时间和次数
  • 数据压缩,减少时间:可以在网络数据传输之前进行压缩,降低网络传输的时间,同时也可以节约流量
  • 禁止使用轮询功能:轮询会导致网络请求一直处于被激活的状态

③、定位相关

  • 定位相关逻辑应根据场景谨慎选择定位模式:在一些对精准度要求没那么高的场景可以使用低精度模式
  • 考虑网络定位代替GPS
  • 使用GPS之后务必及时关闭,减少更新频率:这里可以模仿上面对WakeLock的处理,通过AOP的方式拿到使用GPS的地方,并且加上兜底策略,超过阈值之后强制注销GPS的使用,因为GPS是非常消耗电量的,在使用时要尤为注意

④、界面相关

  • 离开界面后停止对应的耗电活动:比如app被用户切到后台之后应该及时关闭页面动画
  • 耗电操作判断前后台:如果程序处于后台,就不执行相关操作

简单的举个栗子吧:

    private AlphaAnimation alphaAnimation;

    @Override
    protected void onResume() {
        super.onResume();
        alphaAnimation.start();
    }

    @Override
    protected void onPause() {
        super.onPause();
        alphaAnimation.cancel();
    }

⑤、WakeLock相关

  • 注意成对出现:acquire与release:一定要及时释放,不然会导致CPU一直运行,从而使设备处于高功耗的状态
  • 使用带参数的acquire:设置一个超时时间,防止App因为某些异常情况没有及时释放
  • finally确保一定会被释放
  • 常亮场景使用KeepScreenOn即可,无需使用WakeLock

⑥、JobScheduler

  • 在符合某些条件时创建执行在后台的任务
  • 把不紧急的任务放到更合适的时机批量处理

这里也简单的实践一把:

@SuppressLint("NewApi")
public class JobSchedulerService extends JobService {

    @Override
    public boolean onStartJob(JobParameters params) {
        // 此处执行在主线程
        // 模拟一些处理:批量网络请求,APM日志上报
        return false;
    }

    @Override
    public boolean onStopJob(JobParameters params) {
        return false;
    }
}

在AndroidManifest文件中注册:

<service android:name=".net.JobSchedulerService"
            android:permission="android.permission.BIND_JOB_SERVICE"/>

在调用处执行:这里设置了任务需要在用户充电并且处于WIFI的情况下执行

private void startJobScheduler(){
        //需在Android5.0以上执行
        if (Build.VERSION.SDK_INT>Build.VERSION_CODES.LOLLIPOP){
            JobScheduler scheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
            JobInfo.Builder builder = new JobInfo.Builder(1,new ComponentName(getPackageName(), JobSchedulerService.class.getName()));
            builder.setRequiresCharging(true) //任务执行时需要连接电源
                    .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED); //WIFI状态下
            scheduler.schedule(builder.build()); //开启任务
        }
    }

好了,关于电量优化相关的内容就先说到这里了,下半年了,大家加油啊!

祝各位:工作顺利,不负韶华!

猜你喜欢

转载自blog.csdn.net/JArchie520/article/details/106909824