钉钉 Android 端功耗优化最佳实践(中)

简介: 钉钉 Android 端功耗优化最佳实践(中)

更多精彩内容,欢迎观看:

钉钉 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. 1 流量:包含移动网络收/发流量、收/发数据包数量;WiFi 网络收/发流量、收/发数据包数量。
  2. 2 网络收发事件:统计钉钉长连接协议上下行事件;以及 Http 请求事件。
  3. 3 网络变化数:单位时间内有一次或多次网络收发事件记录为一次网络变化;一定时间间隔内的两次连续网络变化记录为一次连续网络变化。

技术实现

流量的统计,Android 10 以上主要是利用 TrafficStats 的 getUidRxBytes 和 getUidTxBytes 获取接收和发送的字节,利用 getUidRxPackets 和 getUidTxPackets 获取接收和发送的数据包数量,结合 App 当前前后台状态、WiFi /移动网络连接状态,计算出流量的消耗。Android 10 以下则利用 /proc/net/xt_qtaguid/stats 获取不同类型网络数据,结合 App 当前前后台状态,计算流量消耗。

再通过钉钉统一网络服务统计上下行网络请求记录事件,以及计算网络变化数和最大连续网络变化数。通过网络变化数和最大连续网络变化数,可有助于分析网络的使用频率;网络收发事件则有助于定位原因。

系统服务调用监控

功耗相关的系统服务调用包含:WakeLock、Alarm、蓝牙扫描、WiFi 扫描、Location、Senser 等的使用。根据系统电量统计原理,监控这些服务与功耗相关的事件的调用,输出事件日志,详细的堆栈信息等。

  1. WakeLock:监控部分唤醒锁和亮屏唤醒锁的使用情况。监控指标包含:持锁时长、持锁个数;
  2. Alarm:监控唤醒闹钟的使用情况。监控指标包含:设置次数、唤醒次数;
  3. WiFi 扫描:WiFi 模块的耗电包含 WiFi 网络数据通信部分(已经在网络部分监控),以及 WiFi 的扫描使用情况。这里的监控指标包含:WiFi 扫描次数;
  4. 蓝牙扫描:监控蓝牙扫描的使用情况。监控指标包含:蓝牙的扫描次数;
  5. Location:监控定位的使用情况。监控指标包含:定位次数、定位时长。

技术实现

系统服务调用的监控,主要采用 Java Hook 的方式来实现。但是,Hook 系统服务调用在不同 Android 版本上会存在一些的兼容性问题,需要做好适配工作。另外,参考系统相关原理,为了让功耗监控更准确,有些需要注意的细节:

  1. 1. WakeLock:根据系统电量统计的实现,WakeLock 耗电只监控 FULL_WAKE_LOCK / SCREEN_BRIGHT_WAKE_LOCK / SCREEN_DIM_WAKE_LOCK / PARTIAL_WAKE_LOCK / PROXIMITY_SCREEN_OFF_WAKE_LOCK 这几类锁。
  2. 2. Alarm:根据系统电量统计的实现,Alarm 耗电只针对唤醒类型 Alarm,即 ELAPSED_REALTIME_WAKEUP / RTC_WAKEUP 。
  3. 3. WiFi 扫描:为降低耗电量,系统在 Android 8.0 (API 级别 26)及更高版本对后台 WiFi 扫描频率有节流限制,所以高版本上,在监控调用 WifiManager.startScan() 扫描次数的基础上,可根据 WifiManager.startScan() 的调用结果判断是否进行了完整的 WiFi 扫描。
  4. 4. Location 定位:
    Location 的监控要注意判断定位类型,GPS 或者 Network。两种方式在电量消耗上有所区分,功耗异常检测上会区别两种类型,所以需要在监控的时候要考虑定位类型。

  5. 为降低耗电量,系统在 Android 8.0(API 级别 26)及更高版本会对应用后台获取当前位置信息的频率进行限制。所以高版本上,在监控定位调用次数的基础上,同时还可以根据位置变更回调来判断实际的位置获取调用情况。

CPU 使用监控

CPU 使用监控主要是针对 CPU 长期高负荷、过于繁忙的场景,需要监控 CPU 使用率这个重要指标,主要包含:

  1. 进程 CPU 开销:包含进程开销、线程数量等;
  2. 线程 CPU 开销:包含普通线程、线程池任务、HandlerThread 任务开销监控;
  3. 线程死循环检测:包含死循环任务检测、异常线程堆栈。

技术实现

  1. 1. 进程开销:
    利用 Linux proc/[pid]/stat该文件包含了某一进程所有的活动的信息,该文件中的所有值都是从系统启动开始累计。该文件中的 pid 字段表示进程号。

    这里比较关键的数据是第 0 位的进程 id、第 1 位的进程名、第 2 位的进程状态,以及第 13-16 位的 utime、stime、cutime 和 cstime(分别是该任务在用户态运行的时间;该任务在核心态运行的时间;所有已死线程在用户态运行的时间;所有已死线程在核心态运行的时间。单位都为 jiffies。)
    进程的总 CPU 开销:utime + stime + cutime + cstime,该值包括其所有线程的 CPU 开销。
  2. 2.线程开销:
  3. 通过遍历proc/[pid]/task/目录内的子目录,proc/[pid]/task/[tid]/stat 该文件包含了进程下所有的活动的信息,该文件中的所有值都是从系统启动开始累计。该文件中的 tid 字段表示线程号。

  4. 这里比较关键的数据是第 1 位的线程名、第 2 位的线程状态,以及第 13-14 位的 utime、stime(分别是该任务在用户态运行的时间;该任务在核心态运行的时间。单位都为 jiffies )。
    线程 CPU 开销:utime + stime。
  5. 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
  6. 4. 死循环检测
    死循环是造成线程 CPU 使用率过高、引起耗电的一类常见问题。当我们发现某一些线程长时间 CPU 使用率过高,会做一次死循环检测,找出其中疑似死循环的线程。死循环检测能力基于死循环线程有三个主要特征:
    ①长时间占用 CPU;
    ②线程不会进入 WAITING 状态;
    ③线程堆栈相似:出现一个循环点时,线程堆栈的底部是永远相同的。
    我们针对长时间 CPU 使用率过高的线程,去做连续 3 帧的堆栈比较,就能比较准确地找出死循环线程,并输出线程堆栈和完整的线程名。
  7. 线程池场景如果是线程池(通过池化技术重复利用已创建的线程)或者 HandlerThread (通过消息队列重复使用当前线程) ,当线程 CPU 使用率高的时候,只分析当前正在执行的任务不一定能找到真正的原因。通过proc/[pid]/task/ 统计的线程开销则还需要进一步拆分定位,每一个任务的执行开销是多少。
  8. 线程池 (Executors) 任务线程池任务的监控,主要是在自身的线程池里 Callable 执行开始和执行结束时监听。
  9. HandlerThread 任务:例如主线程,或者其他自定义 HandlerThread 。HandlerThread 任务主要监控两类:Handler 消息 和 IdleHandler 任务。

  10. Handler 消息:通过替换主线程 Looper 的 Printer,解析 Message 和 Callable 两种格式的消息,则可监控到每个消息的执行开始和执行结束。

  11. IdleHandler:通过反射修改 MessageQueue 的 mIdleHandlers (ArrayList),替换为自定义的 ArrayList ,  在添加和删除 IdleHandler 时,创建 IdleHandlerProxy 代理类并设置。则可监听到 IdleHandler 的 queueIdle() 方法的执行开始和执行结束。

  12. 这样,可在任务的开始和结束,计算该线程的 CPU 开销差值,进一步明确该任务是否有功耗异常。

自启动监控

部分手机的耗电详情上统计了应用的自启动次数,鉴于此,主要监控项为:①自启动次数;②自启动原因;③进程近期退出原因。

  1. 应用自启动次数:两次用户点击启动之间,应用自启动的次数;
  2. 应用自启动原因:每一次应用自启动的原因;
  3. 应用退出原因:应用近期进程退出原因。

技术实现

  1. 1. 自启动原因监控
    四大组件( Activity / Service / ContentProvider / Broadcast )这四大组件在启动的过程中,当其所在的进程不存在时都会调用 startProcessLocked() 创建进程。所以,
    在进程执行 attachBaseContext() 过后,Hook 主线程消息队列里的 message ,结合 startService / bindService /广播/ Activity 的启动流程,可根据 message 内容来判断进程本次的启动原因。
    使用切面方案监控主进程未存活时、应用内子进程通过 ContentResolver 访问主进程 ContentProvider 从而启动主进程的调用,可感知由于 ContentProvider 被调用拉起进程的启动。
  2. 2. 应用退出原因监控
    另外,在Android 11上,还可利用 ActivityManager.getHistoricalProcessExitReasons 获取进程退出原因, 可进一步分析是否有异常的应用频繁退出。

应用&设备状态

功耗消耗是一个过程,是一段时间累积的结果。在一段时间当中,应用可能会在前台/后台等多种状态之间切换,设备可能在充电不充电之间切换、亮灭屏之间切换,而异常耗电更多的是关注在后台、并且是不充电的情况下,忽略状态信息可能会导致许多误报的异常功耗问题。所以,在功耗部件使用监控的基础上,还要记录每一次的状态变化事件;将统计窗口内的状态变化,转变为这段时间内每一种状态的时长占比。在分析功耗问题的时候,将上述功耗模块的使用情况结合这一段时间内应用/设备状态的占比信息,就能更准确地定位功耗问题。

这部分监控包含:应用状态;设备状态;电池信息等。

  1. 1. 应用状态:前台/后台/前台Service/后台悬浮窗;
  2. 2. 设备状态:充电/断电&亮屏/断电&灭屏;
  3. 3. 电池信息:电池电量/电池温度。

感知能力 - 异常耗电监控

在对功耗部件使用情况具备监控情况下,接下来就需要对超过阈值的使用情况认定为异常耗电,异常耗电的监控对于主动感知异常耗电问题至关重要。

主要策略

我们参考 Android Vitals 的功耗性能指标和手机系统的异常耗电提醒类型制定钉钉异常耗电规则以及实时诊断感知:

  1. 制定异常功耗规则:实现了一套异常功耗诊断方案,检测频繁网络使用、CPU 负载过高、WakeLock 长时间持锁、Alarm 频繁唤醒、蓝牙/ WiFi /定位频繁扫描、频繁自启动等高耗电问题;
  2. 实时诊断感知:基于异常功耗规则,实时诊断后台异常功耗问题;并计算头部耗电归因和采集电量报告,快速定位问题。

定异常功耗规则

耗电类型

监控部件

耗电原因

后台网络使用量过高

网络流量

退后台网络流量高

后台网络使用频繁

网络事件

退后台频繁唤醒网络

后台持锁时间过长

WakeLock

退后台长期持有锁不释放

后台频繁唤醒

Alarm

退后台频繁唤醒

后台蓝牙持续扫描

蓝牙扫描

退后台频繁扫描蓝牙

后台 WiFi 频繁扫描

WiFi 扫描

退后台频繁扫描 WiFi

后台频繁自启动

自启动

退后台应用频繁自启动

后台频繁定位

Location

退后台灭屏长时间使用 GPS /网络定位

后台 CPU 负载过高

CPU

退后台有长耗时线程,线程死循环

...

...

...

监控效果

基于这套异常耗电诊断模型,我们能有效感知线上异常高耗电问题。监控上线后,帮我们监控到钉钉潜在的功耗问题。

功耗部件异常监控占比分布,便于洞察功耗头部问题;

单个功耗部件异常功耗的主要归因。例如,下图展示后台长时间持锁的主要归因分布。

快速定位能力 - 电量报告

基于感知能力的功耗部件监控以及使用统计日志,最重要的功耗数据产物之一就是:电量报告

电量报告会显示一段时间各个功耗部件的使用情况。根据电量报告,就可快速定位这个时间窗口内最主要的电量消耗,再结合电量事件日志,就能准确定位问题了。



更多精彩内容,欢迎观看:

钉钉 Android 端功耗优化最佳实践(下):https://developer.aliyun.com/article/1262696?groupCode=alibabaf2e

相关文章
|
14天前
|
算法 Java API
Android性能优化面试题经典之ANR的分析和优化
Android ANR发生于应用无法在限定时间内响应用户输入或完成操作。主要条件包括:输入超时(5秒)、广播超时(前台10秒/后台60秒)、服务超时及ContentProvider超时。常见原因有网络、数据库、文件操作、计算任务、UI渲染、锁等待、ContentProvider和BroadcastReceiver的不当使用。分析ANR可借助logcat和traces.txt。主线程执行生命周期回调、Service、BroadcastReceiver等,避免主线程耗时操作
22 3
|
14天前
|
Java Android开发
Android面试题经典之Glide取消加载以及线程池优化
Glide通过生命周期管理在`onStop`时暂停请求,`onDestroy`时取消请求,减少资源浪费。在`EngineJob`和`DecodeJob`中使用`cancel`方法标记任务并中断数据获取。当网络请求被取消时,`HttpUrlFetcher`的`cancel`方法设置标志,之后的数据获取会返回`null`,中断加载流程。Glide还使用定制的线程池,如AnimationExecutor、diskCacheExecutor、sourceExecutor和newUnlimitedSourceExecutor,其中某些禁止网络访问,并根据CPU核心数动态调整线程数。
29 2
|
15天前
|
安全 Java 数据处理
Android多线程编程实践与优化技巧
Android多线程编程实践与优化技巧
|
18天前
|
安全 Java 数据处理
Android多线程编程实践与优化技巧
Android多线程编程实践与优化技巧
|
26天前
|
缓存 JSON 网络协议
Android面试题:App性能优化之电量优化和网络优化
这篇文章讨论了Android应用的电量和网络优化。电量优化涉及Doze和Standby模式,其中应用可能需要通过用户白名单或电池广播来适应限制。Battery Historian和Android Studio的Energy Profile是电量分析工具。建议减少不必要的操作,延迟非关键任务,合并网络请求。网络优化包括HTTPDNS减少DNS解析延迟,Keep-Alive复用连接,HTTP/2实现多路复用,以及使用protobuf和gzip压缩数据。其他策略如使用WebP图像格式,按网络质量提供不同分辨率的图片,以及启用HTTP缓存也是有效手段。
44 9
|
27天前
|
XML 监控 安全
Android App性能优化之卡顿监控和卡顿优化
本文探讨了Android应用的卡顿优化,重点在于布局优化。建议包括将耗时操作移到后台、使用ViewPager2实现懒加载、减少布局嵌套并利用merge标签、使用ViewStub减少资源消耗,以及通过Layout Inspector和GPU过度绘制检测来优化。推荐使用AsyncLayoutInflater异步加载布局,但需注意线程安全和不支持特性。卡顿监控方面,提到了通过Looper、ChoreographerHelper、adb命令及第三方工具如systrace和BlockCanary。总结了Choreographer基于掉帧计算和BlockCanary基于Looper监控的原理。
27 3
|
27天前
|
设计模式 安全 前端开发
探索Android应用开发的最佳实践
【6月更文挑战第19天】在这篇文章中,我们将深入探讨Android应用开发的最佳实践。从设计模式的选择到性能优化的技巧,我们将一一解析如何构建高效、可维护且用户友好的Android应用。无论你是新手还是经验丰富的开发者,这篇文章都将为你提供有价值的见解和实用的建议。让我们一起探索Android应用开发的奥秘吧!
26 4
|
28天前
|
ARouter IDE 开发工具
Android面试题之App的启动流程和启动速度优化
App启动流程概括: 当用户点击App图标,Launcher通过Binder IPC请求system_server启动Activity。system_server指示Zygote fork新进程,接着App进程向system_server申请启动Activity。经过Binder通信,Activity创建并回调生命周期方法。启动状态分为冷启动、温启动和热启动,其中冷启动耗时最长。优化技巧包括异步初始化、避免主线程I/O、类加载优化和简化布局。
36 3
Android面试题之App的启动流程和启动速度优化
|
13小时前
|
监控 开发工具 Android开发
探索安卓与iOS开发的差异:平台特性、工具和市场趋势
在移动应用开发的广阔舞台上,安卓与iOS两大操作系统扮演着主角。它们各自拥有独特的平台特性、开发工具和市场定位,这些差异深刻影响着开发者的决策和产品的最终形态。本文将深入分析这两大平台的关键技术差异,探讨各自的开发环境和工具集,以及它们在市场上的表现和未来的趋势,为开发者提供一个全面的视角,帮助他们在这两个平台上做出更明智的开发选择。
|
3天前
|
Android开发 Kotlin
kotlin开发安卓app,如何让布局自适应系统传统导航和全面屏导航
使用`navigationBarsPadding()`修饰符实现界面自适应,自动处理底部导航栏的内边距,再加上`.padding(bottom = 10.dp)`设定内容与屏幕底部的距离,以完成全面的布局适配。示例代码采用Kotlin。
32 15

热门文章

最新文章