case分析
资源释放异常——在进入业务场景时使用了系统资源,退出业务场景后没来得及释放导致额外的资源消耗。
sensor
Sensor不使用时没有及时反注册,导致在后台一直接收数据耗电
Activity切到后台后依然会调用传感器,一般来说onPause之后就已经是非业务场景,在destroy之前都会持续占用Sensor,造成不必要的耗电。
注册传感器时,可以选择传感器精度,精度越高耗电也就越多,根据业务实际需求选择合适的精度,不要盲目追求高精度。
按照谷歌官网给的指导建议,建议在onPause中执行反注册
定位服务
从获取方式可以分为两种,主动获取 或 被动定位。主动获取又可以分为 GPS 和 网络定位
GPS_PROVIDER:
1. GPS定位,精准度高耗电量大;
2. 室内GPS定位基本没用。
3. 绝大部分用户默认不开启GPS模块;
4. 从GPS模块启动到获取第一次定位数据,可能需要比较长的时间;
NETWORK_PROVIDER:
网络定位,利用基站和WIFI节点的地址来定位,取决于将基站或WIF节点信息翻译成位置信息的服务器能力;定位快耗电低。
PASSIVE_PROVIDER:
被动定位,使用系统中其他应用的定位信息。
定位服务使用注意事项:
● 根据使用场景选择定位模式,优先考虑使用网络定位(比如定位城市)
● 前台定位时,界面onPause则停止位置更新;后台定位时,根据页务需求控制位置更新时间间隔。
● 应用多模块之间尽量复用位置信息,不要同时定位
●
Broadcast
Android里面的广播按类型分为两种:标准广播和有序广播。从应用软件安全角度又分为:系统广播和本地广播
如果在同一个进程内使用广播通信,请使用本地广播 LocalBroadcastManager,相比普通广播,它更
安全——消息只会在进程内传递
高效——没有跨进程通信。
动态广播需要注册和反注册,非业务场景记得反注册。
例:Activity 场景使用广播,在onResume时注册,onPause 时反注册,很多在onDestroy中反注册,Activity不可见后依然能收到广播。
优化思路
建议同一个业务模块复用一个广播接收器,通过注册通知的方式传递到各个业务,不要自己单独注册,因为接收广播时,会切换到主线程派发,当广播接收器过多时,可能导致主线成繁忙从而造成卡顿。
对于非常频繁的广播,接收侧频繁接收不停处理任务导致CPU高占用,当业务不需要响应那么多次时,建议限制次数或者做别的规避。
Wakelock
Android 运行在很多移动设备上,考虑到功耗原因,引入了 autoSleep的休眠方式,当检测到没有唤醒源时就会进入休眠
wakelock 是阻止系统休眠的接口,代码中一般是保持屏幕常亮。 保持屏幕长亮的WakeLock被建议弃用,系统推荐如下方法(当Activity或view可见时,屏幕才保持常亮)
在Activity.onCreate()中: getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
在xml布局中: android:keepScreenOn="true"
对View设置: view.setKeepScreenOn(true);
FLAG_KEEP_SCREEN_ON实际就是一个SCREEN_BRIGHT_WAKE_LOCK级别的WakeLock,创建和释放锁都由系统自动管理,更加方便和安全,如果非要使用wakeLock,请一定要设置超时释放的时间。
注意:
如果wakelock类型为partial wakelock,那么即使用户按Power键,系统也不会进入休眠。如果申请其它wakelocks,用户按Power键,系统还是会Sleep,异常场景建议使用try catch 的结构注册反注册。
错误 示例
public static void keepScreenOn(Context context, boolean on) {
if (on) {
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
wakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, TAG);
wakeLock.acquire();//没有设置时间,如果逻辑出了错误导致没有释放,就会导致系统无法休眠
//wakeLock.acquire(DELAY_TIME);//超过DELAY_TIME 后会自动释放锁
AEQLog.i(TAG, "keepScreenOn!");
} else {
if (wakeLock != null) {
wakeLock.release();
wakeLock = null;
}
AEQLog.i(TAG, "keepScreenOff!");
}
}
Alarm
AlarmManager利用系统层级的闹钟服务(持有WakeLock),可以指定时间执行任务:
1. 需要精确的定时任务,如闹钟。
2. 非精准确定时任务,可以推迟任务使多个任务同时执行而避免频繁唤醒系统
3. 网络请求相关的业务不使用AlarmManager
JobScheduler
Job Scheduler作为系统服务运行在系统层面,可以指定运行条件(充电状态、Wifi状态、设备空闲),将收到的任务在合适的时间、状态一起执行。
厂家设备对于 “灭屏 + WIFI + 充电” 的场景管控最为松散,建议可延时的任务、数据埋点上报 放到这里执行。
● 网络请求相关业务放到Job Scheduler执行
● 一些与特定场景(JobInfo)绑定的任务
无限使用CPU造成的耗电
While、for(;;)、递归逻辑不严谨,进入无限消耗CPU的循环。
BUG导致的死循环
因为代码逻辑错误,导致循环无法退出,占用CPU耗电
案例
UI绘制
View绘制耗电优化
● 移除不必要的background,比如window默认或嵌套的background
● onDraw多次重复绘制图案,使用clipRect与drawRect
● onDraw方法内不要new对象,避免频繁的GC
● 使用等优化UI布局
● ConstraintLayout替代RelativeLayout、LinearLayout,减少界面测量和布局的次数,优化layout开销
● 减少不必要的infalte,使用变量缓存减少资源加载
● Listview复用convertView,减少资源加载
● 快速滑动列表时,对于图片加载或者网络请求类,在停止滑动才加载数据
● clipPath可能导致CPU、GPU占用过大的问题
View重新绘制导致的
invalidate()
View 的 appearance发生改变,会调用onDraw重新绘制。
requestLayout()
View的宽高发生变化,不适合现在的区域,需要对View重新布局。会调用本身及父类的onMeasure/onLayout,还可能调用其他View的 onMeasure/onLayout
可以通过GPU绘制视图查看
badcase可参考大众点评案例 tech.meituan.com/2018/03/11/…
动画耗电优化
● 动画执行需要和Activity的生命周期关联,如果Activity退出前台则需要暂停动画的执行:
● onPause之后暂停动画执行,减少CPU耗电;onResume重新开始动画绘制
● 当界面的绘制和动画比较复杂或者频繁,优先使用SurfaceView实现,SurfaceView使用单独的绘制线程,避免主线程卡顿
onDraw中创建对象,onDraw频繁调用会导致频繁GC
案例
onDraw创建对象
public class CustomTextView extends View {
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
updateRect();
if (mOldText.equals(mNewText)) {
paint.setAlpha(255);
canvas.drawText(mOldText, PATCH / 2.0f, getHeight() / 2.0f + maxHeight / 2.0f, paint);
} else {
paint.setAlpha((int) (255 * (1 - mCurrentAlphaValue)));
canvas.drawText(mOldText, PATCH / 2.0f, mOuterMoveHeight + getHeight() / 2.0f + maxHeight / 2.0f, paint);
paint.setAlpha((int) (255 * mCurrentAlphaValue));
canvas.drawText(mNewText, PATCH / 2.0f, mCurrentMoveHeight + getHeight() / 2.0f + maxHeight / 2.0f, paint);
}
}
private void updateRect() {
Rect rect1 = new Rect();
Rect rect2 = new Rect();
paint.getTextBounds(mOldText, 0, mOldText.length(), rect1);
paint.getTextBounds(mNewText, 0, mNewText.length(), rect2);
maxWidth = Math.max(mOldText.length(), mNewText.length()) * (getMaxCharWidth() + 2)
+ PATCH;// Math.max(rect1.width(), rect2.width()) + patch;
maxHeight = Math.max(rect1.height(), rect2.height());
}