Android端耗电问题记录整理

228 阅读6分钟

image.png

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());
    }