一、功耗测试
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 [-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:
...
清除屏幕常亮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。
添加唤醒锁权限:
使用唤醒锁:
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 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:
创建工作计划:
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 和网络活动,从而降低耗电量。应用待机模式会延迟用户近期未与之交互的应用的后台网络活动。
针对低电耗模式和应用待机模式进行优化