性能优化 -- 功耗

一、功耗测试

1.环境搭建参考

Battery Historian for windows环境搭建

2.测试方法参考

PC环境与工具:
go语言,python 2.x,python 3.x,Battery Historian

测试步骤:
1.将样机电量充满, 安装定时亮屏APP(实现每隔N分钟亮屏一次)
2.通过命令行执行以下命令以重置电量记录:
  adb shell dumpsys batterystats --enable full-wake-history 
  adb shell dumpsys batterystats --reset 
  adb shell logcat -c
3.置于摇步器上,使其持续摇摆
4.一定时间后,通过命令行执行adb bugreport bugreport.zip
5.将导出的bugreport.zip导入Battery Historian生成电量使用报告

注:
1.多次对比测试时,需尽量保证各种条件一致,如样机型号、OS版本、网络蓝牙状态、权限、账号登陆状态等
2.由于Battery Historian不兼容Q OS, log需通过处理才可导入, 步骤请参考附录

待机测试时长:
方案1:待机24h, 优点是覆盖范围较广,数据比较有代表性,能观察一天的整体耗电情况,数据对比方便,缺点是测试周期较长,且可能会由于存储溢出丢失一小部分数据
方案2:白天黑夜分开待机并抓取数据,优点是测试周期较短,数据不会丢失,且更有针对性,缺点是数据样本数量比方案1多一倍,且数据对比工作量更多

附录:
针对Q OS的bugreport的兼容处理:
1.将生成的bugreport.zip解压
2.在解压出的文件夹中可以发现bugreport-20xx-xx-xx-xx-xx-xx.txt
3.使用battery_historian_fixer.py脚本对bugreport-20xx-xx-xx-xx-xx-xx.txt进行处理
  命令行执行:python3 battery_historian_fixer.py bugreport-20xx-xx-xx-xx-xx-xx.txt的完整路径
4.将生成的bugreport-20xx-xx-xx-xx-xx-xx_fixed.txt导入Battery Historian即可生成报告

3.battery_historian_fixer.py

import re
import sys

verbose = False
usage = '''
usage: python battery_historian_fixer.py <source_file_path> [-v]
______________________________________________________________
-v provides more details as it runs

'''

def removeFraction(content):
    try:
        vals = content.split(",")
        for i in range(len(vals)):
            try:
                vals[i] = str(int(round(float(vals[i]))))
            except Exception:
                pass
        newContent = ",".join(vals)
        if verbose:
            print("%s -> %s" % (content, newContent))
        if not newContent.endswith('\n'):
            newContent += '\n'
        return newContent
    except Exception:
        return content

def getOutputPath(inputPath):
    idxOfDot = inputPath.rfind(".")
    if(idxOfDot != -1 and inputPath[idxOfDot + 1:] in ["txt", "log"]):
        return inputPath[0:idxOfDot] + "_fixed" + inputPath[idxOfDot:]
    else:
        return inputPath + "_fixed"

def main():
    inputPath = None
    global verbose
    if len(sys.argv) < 2:
        print(usage)
        return
    for i in range(1, len(sys.argv)):
        if(sys.argv[i] == '-v'):
            verbose = True
        else:
            inputPath = sys.argv[i]

    try:
        inputFile = open(inputPath, 'r', encoding='utf-8', errors='ignore')
    except Exception:
        print("invalid file path: " + inputPath)
        return

    outputPath = getOutputPath(inputPath)
    outputFile = open(outputPath, 'w', encoding='utf-8')

    isInCheckIn = False

    num = 0

    try:
        for line in inputFile:
            if line == "9,0,l,gmcd,576835,9429304,0.0,0.0,264447,46562,17842,2377,531\n":
                print(line)
            if isInCheckIn:
                if line.find("== Running Application Activities") != -1:
                    isInCheckIn = False
                    print("exit checkin section")
                if line.find(",wfcd,") != -1 or line.find(",gwfcd,") != -1:
                    line = removeFraction(line)
                elif line.find(",mcd,") != -1 or line.find(",gmcd,") != -1:
                    line = removeFraction(line)
                elif line.find(",ble,") != -1 or line.find(",gble,") != -1:
                    line = removeFraction(line)
            else:
                if line.find("CHECKIN BATTERYSTATS") != -1:
                    isInCheckIn = True
                    print("enter checkin section")
            outputFile.write(line)
            num += 1
        print("everything done! Output path is " + outputPath)
    finally:
        outputFile.close()
        inputFile .close()
main()

二、功耗分析

1.CPU

2.WakeLock

3.Alarms & Jobs

4.Network(mobile、WIFI)

5.Location(network、GPS)

6.Bluetooth

7.Sensor

8.Radio

9.Audio

10.Video

日志搜索关键词

  • Battery History
  • Location History
  • wake lock.*com.tomorrow.test(正则表达式)

三、功耗优化

1.点滴积累

屏幕渲染及CPU运行是耗电的主要因素之一。其实当我们在做内存优化、渲染优化、计算优化时,就已经在做耗电优化了。当我们在做耗电优化时,也是在找自己挖的坑,所以在平时的开发中,我们要注意性能优化的点滴积累,有意识地在开发过程中尽量少挖坑。

2.监听手机充电状态

根据自己的业务,把不需要及时和用户交互并且很耗电的操作放到手机充电时去做。

/**
 * This method checks for power by comparing the current battery state against all possible
 * plugged in states. In this case, a device may be considered plugged in either by USB, AC, or
 * wireless charge. (Wireless charge was introduced in API Level 17.)
 */
private boolean checkForPower() {
    // It is very easy to subscribe to changes to the battery state, but you can get the current
    // state by simply passing null in as your receiver.  Nifty, isn't that?
    IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
    Intent batteryStatus = this.registerReceiver(null, filter);

    // There are currently three ways a device can be plugged in. We should check them all.
    int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
    boolean usbCharge = (chargePlug == BatteryManager.BATTERY_PLUGGED_USB);
    boolean acCharge = (chargePlug == BatteryManager.BATTERY_PLUGGED_AC);
    boolean wirelessCharge = false;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
        wirelessCharge = (chargePlug == BatteryManager.BATTERY_PLUGGED_WIRELESS);
    }
    return (usbCharge || acCharge || wirelessCharge);
}

3.屏幕唤醒

保持屏幕常亮最好的方式是在Activity中使用FLAG_KEEP_SCREEN_ON的Flag。这个方法的好处是不像唤醒锁需要特定的权限,并且能正确管理不同应用之间的切换,不用担心无用资源的释放问题。

保持屏幕常亮方式1:
public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    }
}

保持屏幕常亮方式2:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:keepScreenOn="true">
    ...
</RelativeLayout>

清除屏幕常亮flag:
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)

4.WakeLock

wake_lock锁主要是相对系统的休眠而言的,意思是给CPU加了这个锁后系统就不会休眠了,这样做的目的是为了全力配合程序的运行。创建和持有唤醒锁对耗电有较大的影响,在Acitivity中就没必要用了(使用FLAG_KEEP_SCREEN_ON)。只有一种合理的使用场景,就是后台服务在屏幕关闭的情况下hold住CPU完成一些任务,这就需要使用唤醒锁,否则CPU可能在未来的某个时刻休眠导致任务停止,这不是我们想要的。有的人可能认为以前写的后台服务运行得挺好的没有掉过链子,这很可能是任务时间比较短或者CPU被其他的软件唤醒着。

唤醒锁分为四种:

标记值 CPU 屏幕 键盘
PARTIAL_WAKE_LOCK 开启 关闭 关闭
SCREEN_DIM_WAKE_LOCK 开启 变暗 关闭
SCREEN_BRIGHT_WAKE_LOCK 开启 变亮 关闭
FULL_WAKE_LOCK 开启 变亮 变亮

其中FULL_WAKE_LOCK、SCREEN_DIM_WAKE_LOCK、SCREEN_BRIGHT_WAKE_LOCK将被弃用,应用应该使用FLAG_KEEP_SCREEN_ON。

添加唤醒锁权限:
<uses-permission android:name="android.permission.WAKE_LOCK" />

使用唤醒锁:
PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,"Test:WakelockTag");
wakeLock.acquire();

释放唤醒锁:
wakeLock.release();

注意:必须确保acquire和release是成对出现的。不然当我们业务已经不需要唤醒锁时,而CPU仍然处于唤醒状态,这时就会损耗多余的电量。

5.JobScheduler

自Android 5.0发布以来,JobScheduler已成为执行后台工作的首选方式。应用可以在安排作业的同时允许系统基于内存、电源和连接情况进行优化。JobSchedule的宗旨就是把一些不是特别紧急的任务放到更合适的时机批量处理。这样做有两个好处:避免频繁唤醒硬件模块,造成不必要的电量消耗;避免在不合适的时间(例如低电量情况下、弱网络或者移动网络情况下)执行过多的任务消耗电量。

public class JobSchedulerService extends JobService {
    private String TAG = JobSchedulerService.class.getSimpleName();

    @Override
    public boolean onStartJob(JobParameters jobParameters) {
        Log.d(TAG, "onStartJob:" + jobParameters.getJobId());

        if(true) {
            // JobService在主线程运行,如果我们这里需要处理比较耗时的业务逻辑需单独开启一条子线程来处理并返回true,
            // 当给定的任务完成时通过调用jobFinished(JobParameters params, boolean needsRescheduled)告知系统。

            //假设开启一个线程去下载文件
            new DownloadTask().execute(jobParameters);

            return true;

        }else {
            //如果只是在本方法内执行一些简单的逻辑话返回false就可以了
            return false;
        }
    }

    /**
     * 比如我们的服务设定的约束条件为在WIFI状态下运行,结果在任务运行的过程中WIFI断开了系统
     * 就会通过回掉onStopJob()来通知我们停止运行,正常的情况下不会回掉此方法
     *
     * @param jobParameters
     * @return
     */
    @Override
    public boolean onStopJob(JobParameters jobParameters) {
        Log.d(TAG, "onStopJob:" + jobParameters.getJobId());

        //如果需要服务在设定的约定条件再次满足时再次执行服务请返回true,反之false
        return true;
    }

    class DownloadTask extends AsyncTask<JobParameters, Object, Object> {
        JobParameters mJobParameters;

        @Override
        protected Object doInBackground(JobParameters... jobParameterses) {
            mJobParameters = jobParameterses[0];

            //比如说我们这里处理一个下载任务
            //或是处理一些比较复杂的运算逻辑
            //...

            try {
                Thread.sleep(30*1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            return null;
        }

        @Override
        protected void onPostExecute(Object o) {
            super.onPostExecute(o);
            //如果在onStartJob()中返回true的话,处理完成逻辑后一定要执行jobFinished()告知系统已完成,
            //如果需要重新安排服务请true,反之false
            jobFinished(mJobParameters, false);
        }
    }
}

记得在Manifest文件内配置Service:
<service android:name=".JobSchedulerService"
    android:permission="android.permission.BIND_JOB_SERVICE"/>

创建工作计划:
public class MainActivity extends Activity {
    private JobScheduler mJobScheduler;
    private final int JOB_ID = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.mai_layout);

        mJobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE );

        //通过JobInfo.Builder来设定触发服务的约束条件,最少设定一个条件
        JobInfo.Builder jobBuilder = new JobInfo.Builder(JOB_ID, new ComponentName(this, JobSchedulerService.class));

        //循环触发,设置任务每三秒定期运行一次
        jobBuilder.setPeriodic(3000);

        //单次定时触发,设置为三秒以后去触发。这是与setPeriodic(long time)不兼容的,
        // 并且如果同时使用这两个函数将会导致抛出异常。
        jobBuilder.setMinimumLatency(3000);

        //在约定的时间内设置的条件都没有被触发时三秒以后开始触发。类似于setMinimumLatency(long time),
        // 这个函数是与 setPeriodic(long time) 互相排斥的,并且如果同时使用这两个函数,将会导致抛出异常。
        jobBuilder.setOverrideDeadline(3000);

        //在设备重新启动后设置的触发条件是否还有效
        jobBuilder.setPersisted(false);

        // 只有在设备处于一种特定的网络状态时,它才触发。
        // JobInfo.NETWORK_TYPE_NONE,无论是否有网络均可触发,这个是默认值;
        // JobInfo.NETWORK_TYPE_ANY,有网络连接时就触发;
        // JobInfo.NETWORK_TYPE_UNMETERED,非蜂窝网络中触发;
        // JobInfo.NETWORK_TYPE_NOT_ROAMING,非漫游网络时才可触发;
        jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);

        //设置手机充电状态下触发
        jobBuilder.setRequiresCharging(true);

        //设置手机处于空闲状态时触发
        jobBuilder.setRequiresDeviceIdle(true);

        //得到JobInfo对象
        JobInfo jobInfo = jobBuilder.build();

        //设置开始安排任务,它将返回一个状态码
        //JobScheduler.RESULT_SUCCESS,成功
        //JobScheduler.RESULT_FAILURE,失败
        if (mJobScheduler.schedule(jobInfo) == JobScheduler.RESULT_FAILURE) {
            //安排任务失败
        }

        //停止指定JobId的工作服务
        mJobScheduler.cancel(JOB_ID);
        //停止全部的工作服务
        mJobScheduler.cancelAll();
    }
}

6.GPS

  • 选择合适的Location Provider

    GPS_PROVIDER:GPS定位,利用GPS芯片通过卫星获得自己的位置信息。定位精准度高,一般在10米左右,耗电量大,但是在室内GPS定位基本没用。

    NETWORK_PROVIDER:网络定位,利用手机基站和WIFI节点的地址来大致定位位置,这种定位方式取决于服务器,即取决于将基站或WIFI节点信息翻译成位置信息的服务器的能力。

    PASSIVE_PROVIDER:被动定位,就是用现成的,当其他应用使用定位更新了定位信息,系统会保存下来,该应用接收到消息后直接读取就可以了。比如系统中已经安装了百度地图,高德地图(室内可以实现精确定位),只要在它们定位过后再使用这种方法,在你的程序中肯定是可以拿到比较精确的定位信息。

    如果你的应用只是需要一个粗略的定位那么就不需要使用GPS进行定位,既耗费电量,定位的耗时也久。

  • 及时注销定位监听

    在获取到定位之后或者程序处于后台时,注销定位监听,用户不会有感知但是却耗电。

    public void onResume() {
      super.onResume();
      locationManager.requestLocationUpdates(locationManager.getBestProvider(criteria, true),6000,100,locationListener);
    }
    
    public void onPause() {
      super.onPause();
      locationManager.removeListener(locationListener);
    }
    
  • 多模块使用定位尽量复用

    多个模块使用定位,尽量复用上一次的结果,而不是都重新走定位的过程,节省电量损耗。

7.传感器

  • 选择合适的采样率

    SENSOR_DELAY_FASTEST

    SENSOR_DELAY_GAME

    SENSOR_DELAY_UI

    SENSOR_DELAY_NOMAL

  • 在后台时及时注销传感器监听

8.Doze and App Standby

从Android 6.0(API 级别 23)开始,Android 引入了两项省电功能,通过管理应用在设备未连接至电源时的行为方式,帮助用户延长电池寿命。当用户长时间未使用设备时,低电耗模式会延迟应用的后台 CPU 和网络活动,从而降低耗电量。应用待机模式会延迟用户近期未与之交互的应用的后台网络活动。

针对低电耗模式和应用待机模式进行优化

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容