Android-性能优化-06-电量优化

224 阅读18分钟

耗电量

测试方法

l 通过Android API获取

registerReceiver(receiver ,new IntentFilter(Intent.ACTION_BATTERY_CHANGED))

l 通过读取系统电池传感器设备节点

/sys/class/power_supply/batter y/uevent

l 使用外置电流仪器

Android的硬件特点

image.png

耗电原因

l Android能耗统计文件

frameworks\base\core\res\res\xml\power_profile.xml

l 电量统计原理

• frameworks/base/ser vices/core/java/com/android/ser ver/am/Batter yStatsService.java

• AppUsage:应用程序耗电量计算,是指每一个应用程序使用硬件模块所产生的耗电量,注意是按照Uid统计 shareduid

• MiscUsage:其他杂项耗电量计算,所谓杂项,其实就是用户比较关心的一大类,包括:待机的耗电量、亮屏的耗电量、通话的耗电量、Wifi的耗电量等

 process cpu time

power = CPU每个频率等级下工作的时间比例 / CPU工作总时间 * 应用运行总时间

 process wake lock usage

power = 进程wakelock时间 *power_profiler.xml中type=cpu_awake的数值

 cost of mobile traffic (注:R:receive(接收) T: transmit (传输) 下同 )

power=(mobileRx + mobileTx) * mobilePowerPerByte

 cost of keeping WIFI running

power = wifiRunningTimeMs * averagePower(POWER_WIFI_ON)

 cost of WIFI scans wifi

power = wifiScanTimeMs * averagerPower(POWER_WIFI_SCAN)

 Process Sensor usage 计算uid的每个传感器的耗电

power = averagePower(不同传感器的耗电) * sensorTime

 通话时间

power = phoneOnTimeMs * averagerPower(POWER_RADIO_ACTIVE)

 待机耗电

power = idleTimeMs * averagePower(POWER_CPU_IDLE)

 wifi 开启时的耗电量

power = runningTimeMs * averagePower(POWER_WIFI_ON) + wifi uid 耗电

 蓝牙耗电量

power = btOnTimeMs * averagePower(POWER_BLUETOOTH_ON) + btPingCount *

averagePower(POWER_BLUETOOTH_AT_CMD)+ bluetooth uid 耗电

 手机基站信号耗电量

手机信号有五个信号强度,耗电量是每个信号强度的耗电量的累加

power = strengthTimeMs * averagePower(POWER_RADIO_ON) + scanningTimeMs *averagePower(POWER_RADIO_SCANNING)

 屏幕耗电

power = screenOnTimeMs * averagePower(POWER_SCREEN_ON) + brightnessTime(不同亮度的时间)* screenBinPower(不同亮度的电流强度)

电能统计

① 重置电能统计

a) adb shell dumpsys batter ystats --reset

b) adb shell dumpsys --enable full-wake-histor y

② 文本分析

a) adb shell dumpsys batter ystats > batter ystats.txt

b) adb shell dumpsys batter ystats > com.xxx.xxx > batter ystats.txt

③ Batter y Historian2.0 分析

a) adb bugrepor t xxx_bugrepor t.zip

b) adb bugrepor t xxx_bugrepor t.txt(6.0及以下的)

batterystats.txt分析

① Battery Histor y: 耗电统计的历史记录,每一条记录以Histor yItem的形式存在

② Per-PID Stats: 每个进程唤醒工作的时间

③ Discharge step durations: 每掉一隔电的时间点和设备的状态

④ Daily stats: 以天为单位展示每掉一隔电的时间点和设备状态

⑤ Statistics since last charge: 从上次充电以来的统计详情,包含很多子板块

⑥ Cellular Statistics: 移动数据网络状态和使用情况

⑦ Wifi Statistics: WIFI的网络状态和使用情况

⑧ Bluetooth: 蓝牙在不同工作状态下的使用情况

⑨ Estimated power use (mAh): 近似计算出的各个用户(uid)的耗电量,一个APK通常对应到一个用户,当然,也有多个APK共享一个用户的情况

⑩ All kernel wake locks: 内核锁的使用统计

⑪ All par tial wake locks: 应用锁的使用统计

⑫ All wakeup reasons: 所有的唤醒原因

⑬ Statistics by uid: 每一个uid的耗电细节

Batter y Historian2.0 分析

www.jianshu.com/p/378cf678b…

② 只能定位大概的问题,需要结合代码分析

三大耗电模块

显示

image.png

在应用内调节亮度不太现实,一般由系统控制,在设计页面时可以考虑在不影响美观的情况下 ,优先使用深色,当然还是以应用的实际场景考虑优先

网络

移动网络

• Full power : 能量最高的状态,移动网络连接被激活,允许设备以最大的传输速率进行操作。

• Low power : 一种中间状态,对电量的消耗差不多是Full power状态下的50%。

• Standby: 最低的状态,没有数据连接需要传输,电量消耗最少

image.png

image.png

image.png

Wi-Fi

• 在理想情况下,吞吐量大,延迟低,通常是不计费

• 以一种更加”渴望数据”的方式运行

GPS优化

l 精准度要求不高 不用需要开启GPS,蜂窝无线提供的数据足以

l 避免在信号弱(地下室)开启GPS

通常情况下,使用Wi-Fi连接网络的功耗要低于移动网络

网络优化

n 增量拉取数据

n 界面展示的数据非Wi-Fi下不预取

n 实时的信息上报后台运行时改成非实时上报

n 非Wi-Fi场景减低耗流量的功能的网络通信频率

n 合并网络请求,减少请求次数

n 尽量利用Wi-Fi传输信息

CPU

l CPU频率

• CPU频率表示CPU的计算能力,频率决定CPU周期,互为倒数,CPU主频越快,耗时越短

l CPU时间片

• 在计算机中,每隔N个高电频脉冲,时钟计算器加1,可以把自然时间分成固定的小块,这个就是

• 时间片,CPU时间片10ms 单位是jiffies

l CPU利用率

• CPU分用户态,系统态,空闲态,利用率 = (执行用户态 + 系统态的jiffies)/ 总

jiffies

变频

  • Ondemand:官方内核默认使用这种调速器,规则是有高需求,迅速跳到最大频率,有低需求时,迅速降到最小频率

  • Conser vative:规则是慢升快降,注重省电,有高需求,逐渐提高频率,有低需求时,迅速跳到最小频率

  • Interactive:规则是快升慢降,有高需求,逐渐提高频率,有低需求时,逐渐减低频率

  • Lulzactive:根据负载逐级升高或者降低频率

  • Powersave:把频率锁定在设定范围的最小值,负载再高也不升高频率,很省电

  • Performance:把频率锁定在设定范围的最大值,无论负载如何,CPU都全速运行,很费电

CPU 的利用率高就越耗电 ✖

CPU的频率高才越耗电✔

计算优化

l 尽量避开浮点运算

l 除法变乘法

l 充分利用位移

l 查表法,直接使用映射关系,但会增加内存,视具体场景来定

l 利用arm neon指令集做并行运算,需要ARM V7及以上架构CPU才能支持

避免WakeLock使用不当

  • PARTIAL_WAKE_LOCK:保持CPU 运转,屏幕和键盘灯有可能是关闭的。
  • SCREEN_DIM_WAKE_LOCK:保持CPU 运转,允许保持屏幕显示但有可能是灰的,允许关闭键盘灯
  • SCREEN_BRIGHT_WAKE_LOCK:保持CPU 运转,保持屏幕高亮显示,允许关闭键盘灯
  • FULL_WAKE_LOCK:保持CPU 运转,保持屏幕高亮显示,键盘灯也保持亮度
  • ACQUIRE_CAUSES_WAKEUP:不会唤醒设备,强制屏幕马上高亮显示,键盘灯开启。有一个例外,如果有notification弹出的话,会唤醒设备。
  • ON_AFTER_RELEASE:WakeLock 被释放后,维持屏幕亮度一小段时间,减少WakeLock 循环时的闪烁情况
Wakelock (1/4)

AndroidManifest.xml


<uses-permission android:name="android.permission.WAKE_LOCK" />

PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
"MyWakelockTag");
wakeLock.acquire();
// ...
if (wakeLock.isHeld())
wakeLock.release();

• Wakelock 默认是引用计数(reference counted) 行为,同一 wakelock 呼叫几次 acquire(),就需搭配几次 release() 才能真正释放

• 若 wakelock 已释放,再呼叫 release() 就会丢出 RuntimeException

Wakelock (2/4)
class WakeLock {
    // 设定在多少 ms 后自动放掉 wakelock
    // 建议应用撰写时,只要预期仅在后台运行一段时间,
    // 可以此法避免应用遇到未预期状况没放掉 wakelock
    fun acquire(timeout: Long)

    // 可以用此函数关掉 reference counted 行为
    // release() 一次即真正释放 wakelock
    fun setReferenceCounted(value: Boolean)
}
Wakelock (3/4)

Activity 使用过程中,若需阻止手机灭屏,勿使用FULL_WAKE_LOCK,应使用以下两者之一的方法,好处是框架会自动处理相关 wakelock 的释放与重新获得

• 方法一:

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);
    }
    
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:keepScreenOn="true" >
    ...
</RelativeLayout>
Wakelock (4/4)

image.png

避免Alarm Manager使用不当

  • RTC_WAKEUP 在指定的时刻(设置Alarm的时候),唤醒设备来触发Intent。
  • RTC 在一个显式的时间触发Intent,但不唤醒设备
  • EL APSED_REALTIME 从设备启动后,如果流逝的时间达到总时间,那么触发Intent,但不唤醒设备。流逝的时间包括设备睡眠的任何时间。注意一点的是,时间流逝的计算点是自从它最后一次启动算起。
  • EL APSED_REALTIME_WAKEUP 从设备启动后,达到流逝的总时间后,如果需要将唤醒设备并触发Intent
需使用 alarm 的场合

• 应用进程即使不存在,也需在某段时间在后台启动做事。例如:定时与服务器同步邮件

4 种 alarm

• ELAPSED_REALTIME:在多少 ms 后呼叫指定的 PendingIntent。若当时手机处于休眠,会延迟到手机醒来时(屏幕可能还是关的)才做

• ELAPSED_REALTIME_WAKEUP:在多少 ms 后呼叫指定的 PendingIntent。若当时手机处于休眠,会唤醒手机做事,同时休眠期间被延迟的事也会一起执行。是最常见的耗电凶手

• RTC:在指定的某段时间做事。若当时手机处于休眠,会延迟到手机醒来时 (屏幕可能还是关的)才做

• RTC_WAKEUP:在指定的某段时间做事。若当时手机处于休眠,会唤醒手机做事,同时休眠期间被延迟的事也会一起执行。通常用于闹钟、记事提醒等

alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent: Intent = Intent(context, AlarmReceiver::class.java)
alarmIntent = PendingIntent.getBroadcast(context, 0, intent, 0)
alarmMgr.set(
    AlarmManager.ELAPSED_REALTIMEP,
    SystemClock.elapsedRealtime() +
            60 * 1000, alarmIntent
)
alarmMgr.setInexactRepeating(
    AlarmManager.ELAPSED_REALTIME,
    AlarmManager.INTERVAL_HALF_HOUR,
    AlarmManager.INTERVAL_HALF_HOUR, alarmIntent
)
Alarm 的准时性

• 由于 ELAPSED_TIME 与 RTC 在手机休眠时不会唤醒手机,故一

定不准时

• 下表是针对 WAKEUP alarm 讨论

image.png

使用Job Scheduler(WorkManager)

l 重要不紧急的任务,可以延迟执行,如定期数据库更新和数据上报

l 耗电量较大的任务,比如充电时才希望执行备份数据操作

l 不紧急可以不执行的网络任务,如在Wi-Fi环境预加载数据

l 可以批量执行的任务

Doze模式

初识Doze

  在Android 6.0版本中google提供了两个策略:doze和app standby,能有效的减少电量的消耗,提升待机时间。设备没有连接到电源,设备进入Doze模式时,系统将通过延迟最近用户没有使用的应用程序的后台CPU运作及网络活动,让应用程序处于App Standby状态,以此来减少电池消耗。   Doze功能借助动作检测来确定用户有多长时间没用手机,然后就进入“打盹”模式,从而延长续航时间。而一旦用户重新开始使用手机,Doze模式就会取消恢复到正常状态。

谷歌称,配备Doze的Nexus9将比普通Nexus续航能力提高一倍。德国科技博客ComputerBase分别用一部安装了Android M开发者预览版和一部安装了Android 5.1.1的Nexus 5来做对比。两部手机都安装了同样的应用,电池充满。同时连入相同的无线网络,使用相同的设置,都不装SIM卡,蓝牙,NFC,Android Beam以及LED等全都处于关闭状态。 结果对比是在待机8小时后,安装了Android 5.1.1的Nexus 5消耗了4%的电量,而Android M版则仅消耗了1.5%;在24小时后,Android 5.1.1版的Nexus 5消耗了12%的电量,Android M版则仅消耗了4.5%;48小时后,Android 5.1.1版的Nexus 5消耗了24%的电量,而Android M版则仅消耗了9%。总的来看,在Doze开启下,Nexus 5在Android M下则能待机533小时,在Android 5.1.1下可待机200小时。

理解Doze

  1、设备进入Doze睡眠模式时机:     

–用户不操作设备一段时间 (通过动作监测来判断)     

–屏幕关闭     

–设备未连接电源充电   

2、Doze模式下应用程序有什么变化:     

–系统试图通过限制应用程序访问网络和CPU密集型8服务节省电池     

–防止应用程序访问网络,推延应用程序的工作,同步,和标准的警报     

–系统定期提供一个短暂的时间让应用程序完成延迟的工作活动,在这个时间片里,系统将提供维持性窗口应用程序访问网络,运行在等待的同步,工作,和报警等活动   

Doze模式的五种状态,分别如下:     

–ACTIVE:手机设备处于激活活动状态     

–INACTIVE:屏幕关闭进入非活动状态     

–IDLE_PENDING:每隔30分钟让App进入等待空闲预备状态     

–IDLE:空闲状态     

–IDLE_MAINTENANCE:处理挂起任务   

下面一张图将介绍这几种状态之间的切换关系   

image.png google官方文档说明,在灭屏后至少60分钟才会进入到doze模式。在进入到doze模式后,应用程序的活动和网络链接都会被挂起,每个一段时间会进入一次Maintenance阶段(持续30秒),让应用能处理挂起的任务。

image.png

进入Maintenance的时间间隔会随着doze模式的深入越来越长。   

3、如何退出doze模式     

–用户唤醒装置移动,打开屏幕    

–连接电源  

4、doze模式有哪些限制     

–网络连接会被禁止     

–Wake Lock会被屏蔽     

–AlarmManager定时任务延迟到下一个maintenance window进行处理,除非使用AlarmManager提供的方法:setAndAllowWhileIdle()或者setExactAndAllowWhileIdle()     

–系统将不扫描热点WIFI     

–同步工作将被禁止    

–不允许JobScheduler进行任务调度   

5、如何在开发时适配doze模式     

–Doze影响到AlarmManager闹钟和定时器管理活动,在Android6.0引入了两个新方法:setAndAllowWhileIdle() 和setExactAndAllowWhileIdle(),调用两个方法可以在Doze模式下让系统响应定时任务     

–Doze模式下限制了网络的连接,如果应用程序依赖于实时信息,那么这个将影响App的体验。那么你需要使用Google Cloud Messaging (GCM)谷歌云消息   

6、测试Doze和App Standby模式的方法(Adb命令)

  测试Doze模式

  • 1.首先确保你的硬件或虚拟设备是Android6.0或更高版本系统;

  • 2.连接设备到开发机上并安装你的app;

  • 3.运行app并让其运行活动;

  • 4.关闭设备的屏幕;

  • 5.运行以下adb命令使系统进入Doze模式:

      $ adb shell dumpsys battery unplug      //(断开电源)
      $ adb shell dumpsys deviceidle step     //(获取状态)
    
  • 6.观察你的app表现行为是否有需优化改进的地方。

  测试App Standby模式

  步骤1-3同测试Doze模式

  • 4.运行以下adb命令迫使系统进入App Standby模式:

      $ adb shell dumpsys battery unplug
      $ adb shell am set-inactive <packageName> true
    
  • 5.模拟唤醒你的应用程序使用以下命令:

      $ adb shell am set-inactive <packageName> false
      $ adb shell am get-inactive <packageName>
    
  • 6.观察你的App,确保应用程序恢复正常从待机模式过程中,App的通知及其背部活动能达到预期结果。

理解app standby策略

  当用户不触摸使用应用程序一段时间时,该应用程序处于App Standby状态,系统将把该App标志为空闲状态。除非触发以下任意条件,应用程序将退出App Standby状态:

  • 1.用户主动启动该App;
  • 2.该App当前有一个前台进程(或包含一个活动的前台服务,或被另一个activity或前台service使用);
  • 3.App生成一个用户所能在锁屏或通知托盘看到的Notification,而当用户设备插入电源时,系统将会释放App的待机状态,允许他们自由的连接网络及其执行未完成的工作和同步。如果设备空闲很长一段时间,系统将允许空闲App一天一次访问网络。

  Doze和App Standby的区别:   

Doze模式需要屏幕关闭(通常晚上睡觉或长时间屏幕关闭才会进入),而App Standby不需要屏幕关闭,App进入后台一段时间也会受到连接网络等限制。

设计规范总结

  • 设计与编码需避免的情况
  1. 阻止手机休眠
  2. 时常唤醒手机
  3. 后台频繁运行
  4. 过度绘制
  • 若应用是登入账户、或是某种启用方式后才能使用主要功能,默认应关闭所有注册在 AndroidManifest.xml 的 receiver,并在登入或启用功能后开启:
<receiver android:name=“MyReceiver”

android:enabled=“false” />
ComponentName receiver = new ComponentName(context,
    MyReceiver.class);
PackageManager pm = context.getPackageManager();
pm.setComponentEnabledSetting(receiver,
    PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
    PackageManager.DONT_KILL_APP);
  • 当用户退出或关闭应用功能后,应再以 setComponentEnabledSetting() 关闭 receiver尽可能少用 alarm、尤其是 WAKEUP alarm
  1. 应用在后台联网更新资料的周期应尽可能拉长,并非事事紧急需立即知会用户。周期至少须为 12 小时。若更新及时性没那么重要,周期可设定在一天以上
  2. 若用户进了应用刷新资料,则重设 alarm 周期。举例而言,若周期为 12 小时,且用户在 8 点进入应用刷新资料,则下次在后台更新资料的时间为 20 点
  3. 避免同一应用设定不同周期的 alarm 更新资料
  4. 除了闹钟、记事提醒外,禁止使用 WAKEUP alarm
  5. 避免使用 setExact()、setAlarmClock()、setAndAllowWhileIdle()、setExactAndAllowWhileIdle() 这种对时间高精准要求的接口。使用其他接口,能让 AlarmManager 适度调整时间,节省功耗
  • 避免与服务器建立长连接
  1. 维持长连接需要不断唤醒手机送心跳包。若有许多应用与服务器建立长连接,对手机待机耗电会造成极大影响
  2. 在还没设计出 ROM 里的共用 push 通道之前,请使用 alarm、周期至少 12小时、且不是 WAKEUP alarm 的方式,向服务器更新资料。若用户进了应用刷新资料,则重设 alarm 周期
  • 应用在后台自网络更新资料前,需先检查是否目前为BAT TERY_LOW。若处于 BAT TERY_LOW、且目前不是充电状态,则不进行后台的网路更新,以节省电量

  • 应用内的手动刷新,只有在超过指定的间隔时间,才从网络更新资料

服务器更新资料未必那么频繁,抓到的可能都是同样资料。故即使用户手动刷新资料,也不应每次真的自网络更新。只要判断相邻更新的间隔时间小于某个固定的值,就省去这次更新

  • 使用 par tial wakelock 需设定 timeout,在超过时间后自动释放 wakelock

此规定是为了避免程序逻辑错误,造成 wakelock 永久放不掉的耗电

  • 联不上网的异常处理需合理:
  1. 应先检查目前手机网络连线状态,只有先确定连上 3G/4G 网络或 WiFi 时,才进行连线
  2. 若当下连不上服务器,最多只能重试 3 次。之后就必须结束重试
  3. 避免在后台以监听 ConnectivityManager.CONNECTIVITY_ACTION(android.net.conn.CONNECTIVITY_CHANGE) 广播方式进行联网重试。在网络不稳定的环境,这个 intent 可能会送出非常频繁,若每次都做重新连线的尝试,会造成明显的耗电
  4. 若应用会在后台定期联网,假设此次在后台连不上网,下次重试时间应该是下个周期。避免再写另一个较短的周期进行后台联网重试
  • Activity pause 时尽可能释放耗电的资源,例如传感器、停止要求位置
fun onResume() {
    super.onResume()
    val sManager = mContext
        .getSystemService(Context.SENSOR_SERVICE) as SensorManager
    sManager.registerListener(
        mPositionListener,
        mSensor, SensorManager.SENSOR_DELAY_GAME
    )
// ...
}

fun onPause() {
    super.onPause()
    val sManager = mContext
        .getSystemService(Context.SENSOR_SERVICE) as SensorManager
    sManager.unregisterListener(mPositionListener)
}
  • 查询画面静止时、或应用退到后台后,停掉后台不必要的运行
  1. 按 home 键与 back 键离开皆需检查
  2. 以 top 命令检查 CPU% (如下图滴滴打车、浏览器、今日头条持续不停地在后台运行)

top -m 5 -d 1

image.png

  1. 减少 CPU 资源使用, 滑动或动画
  2. 应用退到后台 (不论是按 home 或 back 键离开) ,尽可能呼叫unregisterReciever() 或 unregisterContentResolver(),以避免在后台收到频繁更新而做事。即使真的不能 unregister,也可先记一个flag 注明有更新,等应用回到前台再更新即可
  3. 避免动画循环不停地播放
  4. 减少 View 的 background 使用,特别是不明显的 background。增加性能、也减少功耗
  • 注册广播时,尽量在代码中动态注册

image.png

  • 要尽量避免使用ACTION_BOOT_COMPLETED之类的有序广播
  1. 并行发送——Parallel——sendBroadcast
  2. 串行发送——Ordered——sendOrderedBroadcast(有序)

image.png

  • 禁止以FLAG_RECEIVER_FOREGROUND标记的方式发广播

image.png

image.png

  • 跨进程广播要尽量减小范围;点对点的广播要指明Component;进程内通信可以采用LocalBroadcastManager
  1. 广播的作用范围要尽量的小:
  2. 跨进程的能通过过滤条件减小范围的就减小 广播接收者可以限定接收的广播的范围 如何做到只监听特定包被安装/卸载了

image.png 4. 只有一个跨进程的收听者应发定向广播

image.png 5. 同一个进程内的应用LocalBroadcastManager

原理:用单例、Handler来模拟广播收发

image.png

  • 跨进程频繁通信要避免使用广播,可以用bindService或ContentResolver.call

image.png