更多精彩内容,欢迎观看:
钉钉 Android 端功耗优化最佳实践(上):https://developer.aliyun.com/article/1262699?spm=a2c6h.13148508.setting.14.1fb44f0e1zEMZJ
感知能力 - 功耗部件监控
感知能力的建设对于了解线上功耗健康度非常关键,对于我们甄别头部问题以及防劣化体系意义重大。帮助我们从被动应对转变为主动出击。
系统电量统计原理
首先简单说明下 Android 系统是如何统计耗电量的。物理学中电量计算公式:
电量 = 功率 × 时间 = (电压 × 电流 )× 时间
手机上的电压一般是恒定不变的,所以计算电量可直接使用电流来代替功率;再结合各个硬件模块在不同状态下的使用时间,则可以计算出其消耗的电量。
系统服务 BatteryStatsService 就是用于耗电量计算。负责电池信息收集,以及各个部件、各个应用程序的各类别的耗电量统计。计算电池剩余使用时间,电池充满时间等。
系统统计电量的流程是这样的:Android 系统将各个硬件模块的电流消耗值以及该模块在一段时间内大概消耗的电量以固定值的形式存储在 power_profile.xml(电源配置文件)中。由于硬件之间的差异,电源配置文件需要各个设备制造商进行定制。PowerProfile 负责解析电源配置文件,获取各个功耗部件的功耗值,并将获取的值提供给 BatteryStatsService。BatteryStatsService 则会委托 BatteryStatsImpl 跟踪统计各个硬件模块的状态和使用的时间,通过 BatteryStatsHelper 交给各个硬件模块的 PowerCalculator 计算模块的电量,以此来估算 App 整体耗电量。
主要策略
通过系统的电量统计原理了解到系统的耗电量统计与哪些组件的哪些行为有关,以及统计流程和方法。在无法直接获取耗电量情况下,可以参照系统的统计原理,监控耗电相关的组件的使用情况,以此来统计功耗使用数据、反映功耗消耗情况。
根据系统电量统计原理,结合异常耗电的基线标准,以及钉钉的业务情况,我们主要监控以下模块的使用情况:
接下来将介绍下各个功耗部件的监控方案。
网络使用监控
在前文中已经介绍过,Mobile Radio 和 WiFi 模块的耗电不仅仅与流量大小相关,还与网络状态激活的次数和间隔相关。频繁的连续的网络收发非常影响耗电。所以网络部分主要监控:①流量、②网络收发事件、③网络变化数 3 个指标。
- 1 流量:包含移动网络收/发流量、收/发数据包数量;WiFi 网络收/发流量、收/发数据包数量。
- 2 网络收发事件:统计钉钉长连接协议上下行事件;以及 Http 请求事件。
- 3 网络变化数:单位时间内有一次或多次网络收发事件记录为一次网络变化;一定时间间隔内的两次连续网络变化记录为一次连续网络变化。
技术实现
流量的统计,Android 10 以上主要是利用 TrafficStats 的 getUidRxBytes 和 getUidTxBytes 获取接收和发送的字节,利用 getUidRxPackets 和 getUidTxPackets 获取接收和发送的数据包数量,结合 App 当前前后台状态、WiFi /移动网络连接状态,计算出流量的消耗。Android 10 以下则利用 /proc/net/xt_qtaguid/stats 获取不同类型网络数据,结合 App 当前前后台状态,计算流量消耗。
再通过钉钉统一网络服务统计上下行网络请求记录事件,以及计算网络变化数和最大连续网络变化数。通过网络变化数和最大连续网络变化数,可有助于分析网络的使用频率;网络收发事件则有助于定位原因。
系统服务调用监控
功耗相关的系统服务调用包含:WakeLock、Alarm、蓝牙扫描、WiFi 扫描、Location、Senser 等的使用。根据系统电量统计原理,监控这些服务与功耗相关的事件的调用,输出事件日志,详细的堆栈信息等。
- WakeLock:监控部分唤醒锁和亮屏唤醒锁的使用情况。监控指标包含:持锁时长、持锁个数;
- Alarm:监控唤醒闹钟的使用情况。监控指标包含:设置次数、唤醒次数;
- WiFi 扫描:WiFi 模块的耗电包含 WiFi 网络数据通信部分(已经在网络部分监控),以及 WiFi 的扫描使用情况。这里的监控指标包含:WiFi 扫描次数;
- 蓝牙扫描:监控蓝牙扫描的使用情况。监控指标包含:蓝牙的扫描次数;
- Location:监控定位的使用情况。监控指标包含:定位次数、定位时长。
技术实现
系统服务调用的监控,主要采用 Java Hook 的方式来实现。但是,Hook 系统服务调用在不同 Android 版本上会存在一些的兼容性问题,需要做好适配工作。另外,参考系统相关原理,为了让功耗监控更准确,有些需要注意的细节:
- 1. WakeLock:根据系统电量统计的实现,WakeLock 耗电只监控 FULL_WAKE_LOCK / SCREEN_BRIGHT_WAKE_LOCK / SCREEN_DIM_WAKE_LOCK / PARTIAL_WAKE_LOCK / PROXIMITY_SCREEN_OFF_WAKE_LOCK 这几类锁。
- 2. Alarm:根据系统电量统计的实现,Alarm 耗电只针对唤醒类型 Alarm,即 ELAPSED_REALTIME_WAKEUP / RTC_WAKEUP 。
- 3. WiFi 扫描:为降低耗电量,系统在 Android 8.0 (API 级别 26)及更高版本对后台 WiFi 扫描频率有节流限制,所以高版本上,在监控调用 WifiManager.startScan() 扫描次数的基础上,可根据 WifiManager.startScan() 的调用结果判断是否进行了完整的 WiFi 扫描。
- 4. Location 定位:
Location 的监控要注意判断定位类型,GPS 或者 Network。两种方式在电量消耗上有所区分,功耗异常检测上会区别两种类型,所以需要在监控的时候要考虑定位类型。 - 为降低耗电量,系统在 Android 8.0(API 级别 26)及更高版本会对应用后台获取当前位置信息的频率进行限制。所以高版本上,在监控定位调用次数的基础上,同时还可以根据位置变更回调来判断实际的位置获取调用情况。
CPU 使用监控
CPU 使用监控主要是针对 CPU 长期高负荷、过于繁忙的场景,需要监控 CPU 使用率这个重要指标,主要包含:
- 进程 CPU 开销:包含进程开销、线程数量等;
- 线程 CPU 开销:包含普通线程、线程池任务、HandlerThread 任务开销监控;
- 线程死循环检测:包含死循环任务检测、异常线程堆栈。
技术实现
- 1. 进程开销:
利用 Linux 的proc/[pid]/stat
:该文件包含了某一进程所有的活动的信息,该文件中的所有值都是从系统启动开始累计。该文件中的 pid 字段表示进程号。
这里比较关键的数据是第 0 位的进程 id、第 1 位的进程名、第 2 位的进程状态,以及第 13-16 位的 utime、stime、cutime 和 cstime(分别是该任务在用户态运行的时间;该任务在核心态运行的时间;所有已死线程在用户态运行的时间;所有已死线程在核心态运行的时间。单位都为 jiffies。)
进程的总 CPU 开销:utime + stime + cutime + cstime,该值包括其所有线程的 CPU 开销。 - 2.线程开销:
- 通过遍历proc/[pid]/task/目录内的子目录,
proc/[pid]/task/[tid]/stat
该文件包含了进程下所有的活动的信息,该文件中的所有值都是从系统启动开始累计。该文件中的 tid 字段表示线程号。
这里比较关键的数据是第 1 位的线程名、第 2 位的线程状态,以及第 13-14 位的 utime、stime(分别是该任务在用户态运行的时间;该任务在核心态运行的时间。单位都为 jiffies )。
线程 CPU 开销:utime + stime。- 3. 进程/线程 CPU 使用率计算
基于上面的背景知识,我们可以每隔一段时间 period 秒读取proc/[pid]/stat
,解析其中的 utime / stime / cutime / cstime , 将其和(utime + stime + cutime + cstime) 与上一次采样时的和做差,这就是这一段时间内该进程占用 CPU 的时间,单位为 TICK 。而总的 CPU 时间为 period * HZ。所以,进程的 CPU 使用率可以用如下公式计算:((utime + stime + cutime + cstime)- (lastutime + laststime + lastcutime + lastcstime)) / period * HZ
因为通常 HZ = 100, 当进程/线程的 jiffies 开销约等于每分钟 6000 jiffies 的时候,换算下来进程/线程的 CPU 使用率约为 100%。类似的,线程的 CPU 使用率为:((utime + stime )- (lastutime + laststime)) / period * HZ - 4. 死循环检测
死循环是造成线程 CPU 使用率过高、引起耗电的一类常见问题。当我们发现某一些线程长时间 CPU 使用率过高,会做一次死循环检测,找出其中疑似死循环的线程。死循环检测能力基于死循环线程有三个主要特征:
①长时间占用 CPU;
②线程不会进入 WAITING 状态;
③线程堆栈相似:出现一个循环点时,线程堆栈的底部是永远相同的。
我们针对长时间 CPU 使用率过高的线程,去做连续 3 帧的堆栈比较,就能比较准确地找出死循环线程,并输出线程堆栈和完整的线程名。 - 线程池场景如果是线程池(通过池化技术重复利用已创建的线程)或者 HandlerThread (通过消息队列重复使用当前线程) ,当线程 CPU 使用率高的时候,只分析当前正在执行的任务不一定能找到真正的原因。通过
proc/[pid]/task/
统计的线程开销则还需要进一步拆分定位,每一个任务的执行开销是多少。 - ①线程池 (Executors) 任务线程池任务的监控,主要是在自身的线程池里 Callable 执行开始和执行结束时监听。
- ②HandlerThread 任务:例如主线程,或者其他自定义 HandlerThread 。HandlerThread 任务主要监控两类:Handler 消息 和 IdleHandler 任务。
- Handler 消息:通过替换主线程 Looper 的 Printer,解析 Message 和 Callable 两种格式的消息,则可监控到每个消息的执行开始和执行结束。
- IdleHandler:通过反射修改 MessageQueue 的 mIdleHandlers (ArrayList),替换为自定义的 ArrayList , 在添加和删除 IdleHandler 时,创建 IdleHandlerProxy 代理类并设置。则可监听到 IdleHandler 的 queueIdle() 方法的执行开始和执行结束。
- 这样,可在任务的开始和结束,计算该线程的 CPU 开销差值,进一步明确该任务是否有功耗异常。
自启动监控
部分手机的耗电详情上统计了应用的自启动次数,鉴于此,主要监控项为:①自启动次数;②自启动原因;③进程近期退出原因。
- 应用自启动次数:两次用户点击启动之间,应用自启动的次数;
- 应用自启动原因:每一次应用自启动的原因;
- 应用退出原因:应用近期进程退出原因。
技术实现
- 1. 自启动原因监控
四大组件( Activity / Service / ContentProvider / Broadcast )这四大组件在启动的过程中,当其所在的进程不存在时都会调用 startProcessLocked() 创建进程。所以,
在进程执行 attachBaseContext() 过后,Hook 主线程消息队列里的 message ,结合 startService / bindService /广播/ Activity 的启动流程,可根据 message 内容来判断进程本次的启动原因。
使用切面方案监控主进程未存活时、应用内子进程通过 ContentResolver 访问主进程 ContentProvider 从而启动主进程的调用,可感知由于 ContentProvider 被调用拉起进程的启动。 - 2. 应用退出原因监控
另外,在Android 11上,还可利用 ActivityManager.getHistoricalProcessExitReasons 获取进程退出原因, 可进一步分析是否有异常的应用频繁退出。
应用&设备状态
功耗消耗是一个过程,是一段时间累积的结果。在一段时间当中,应用可能会在前台/后台等多种状态之间切换,设备可能在充电不充电之间切换、亮灭屏之间切换,而异常耗电更多的是关注在后台、并且是不充电的情况下,忽略状态信息可能会导致许多误报的异常功耗问题。所以,在功耗部件使用监控的基础上,还要记录每一次的状态变化事件;将统计窗口内的状态变化,转变为这段时间内每一种状态的时长占比。在分析功耗问题的时候,将上述功耗模块的使用情况结合这一段时间内应用/设备状态的占比信息,就能更准确地定位功耗问题。
这部分监控包含:应用状态;设备状态;电池信息等。
- 1. 应用状态:前台/后台/前台Service/后台悬浮窗;
- 2. 设备状态:充电/断电&亮屏/断电&灭屏;
- 3. 电池信息:电池电量/电池温度。
感知能力 - 异常耗电监控
在对功耗部件使用情况具备监控情况下,接下来就需要对超过阈值的使用情况认定为异常耗电,异常耗电的监控对于主动感知异常耗电问题至关重要。
主要策略
我们参考 Android Vitals 的功耗性能指标和手机系统的异常耗电提醒类型制定钉钉异常耗电规则以及实时诊断感知:
- 制定异常功耗规则:实现了一套异常功耗诊断方案,检测频繁网络使用、CPU 负载过高、WakeLock 长时间持锁、Alarm 频繁唤醒、蓝牙/ WiFi /定位频繁扫描、频繁自启动等高耗电问题;
- 实时诊断感知:基于异常功耗规则,实时诊断后台异常功耗问题;并计算头部耗电归因和采集电量报告,快速定位问题。
制定异常功耗规则
耗电类型 |
监控部件 |
耗电原因 |
后台网络使用量过高 |
网络流量 |
退后台网络流量高 |
后台网络使用频繁 |
网络事件 |
退后台频繁唤醒网络 |
后台持锁时间过长 |
WakeLock |
退后台长期持有锁不释放 |
后台频繁唤醒 |
Alarm |
退后台频繁唤醒 |
后台蓝牙持续扫描 |
蓝牙扫描 |
退后台频繁扫描蓝牙 |
后台 WiFi 频繁扫描 |
WiFi 扫描 |
退后台频繁扫描 WiFi |
后台频繁自启动 |
自启动 |
退后台应用频繁自启动 |
后台频繁定位 |
Location |
退后台灭屏长时间使用 GPS /网络定位 |
后台 CPU 负载过高 |
CPU |
退后台有长耗时线程,线程死循环 |
... |
... |
... |
监控效果
基于这套异常耗电诊断模型,我们能有效感知线上异常高耗电问题。监控上线后,帮我们监控到钉钉潜在的功耗问题。
功耗部件异常监控占比分布,便于洞察功耗头部问题;
单个功耗部件异常功耗的主要归因。例如,下图展示后台长时间持锁的主要归因分布。
快速定位能力 - 电量报告
基于感知能力的功耗部件监控以及使用统计日志,最重要的功耗数据产物之一就是:电量报告。
电量报告会显示一段时间各个功耗部件的使用情况。根据电量报告,就可快速定位这个时间窗口内最主要的电量消耗,再结合电量事件日志,就能准确定位问题了。
更多精彩内容,欢迎观看:
钉钉 Android 端功耗优化最佳实践(下):https://developer.aliyun.com/article/1262696?groupCode=alibabaf2e