钉钉 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

相关文章
|
3天前
|
Java 数据库 Android开发
【专栏】构建高效 Android 应用:探究 Kotlin 多线程优化策略
【4月更文挑战第27天】本文探讨了Kotlin在Android开发中的多线程优化,包括线程池、协程的使用,任务分解、避免阻塞操作以及资源管理。通过案例分析展示了网络请求、图像处理和数据库操作的优化实践。同时,文章指出并发编程的挑战,如性能评估、调试及兼容性问题,并强调了多线程优化对提升应用性能的重要性。开发者应持续学习和探索新的优化策略,以适应移动应用市场的竞争需求。
|
7天前
|
缓存 监控 Android开发
构建高效Android应用:从优化用户体验到提升性能表现
【4月更文挑战第23天】 在竞争激烈的移动市场中,一个高效的Android应用是吸引并保留用户的关键。本文将探讨如何通过一系列技术手段和最佳实践来优化Android应用的用户体验和性能表现。我们将深入分析响应式UI设计、内存管理、多线程处理以及最新的Android框架特性,揭示它们如何共同作用以减少应用延迟,提高响应速度,并最终提升整体用户满意度。
|
1天前
|
移动开发 数据库 Android开发
构建高效Android应用:探究Kotlin协程的优化实践
【4月更文挑战第29天】在移动开发领域,尤其是Android平台上,性能优化一直是开发者关注的重点。近年来,Kotlin语言凭借其简洁性和功能性成为Android开发的热门选择。其中,Kotlin协程作为一种轻量级的并发处理机制,为编写异步代码、网络请求和数据库操作提供了极大的便利。本文将深入探讨Kotlin协程在Android应用中的性能优化技巧,帮助开发者构建更加高效的应用程序。
|
2天前
|
移动开发 API Android开发
Android应用性能优化实战
【4月更文挑战第28天】在移动开发领域,一个流畅的用户体验是至关重要的。对于Android开发者而言,应用的性能优化是一项既挑战性也极其重要的工作。本文将深入探讨Android应用性能优化的多个方面,包括内存管理、UI渲染、多线程处理以及电池效率等,旨在为开发者提供实用的性能提升策略和具体的实施步骤。通过分析常见的性能瓶颈,并结合最新的Android系统特性和工具,我们的目标是帮助读者打造更加高效、响应迅速的Android应用。
|
3天前
|
缓存 监控 Android开发
Android 应用性能优化实战
【4月更文挑战第27天】 在竞争激烈的移动应用市场中,性能优越的应用更能吸引和保留用户。针对Android平台,本文将深入探讨影响应用性能的关键因素,并提供一系列实用的优化策略。我们将从内存管理、UI渲染、多线程处理以及电池使用效率等方面入手,通过具体案例分析如何诊断常见问题,并给出相应的解决方案。文中所提技巧旨在帮助开发者构建更加流畅、高效的Android应用。
16 2
|
6天前
|
移动开发 Java Android开发
构建高效Android应用:采用Kotlin协程优化网络请求
【4月更文挑战第24天】 在移动开发领域,尤其是对于Android平台而言,网络请求是一个不可或缺的功能。然而,随着用户对应用响应速度和稳定性要求的不断提高,传统的异步处理方式如回调地狱和RxJava已逐渐显示出局限性。本文将探讨如何利用Kotlin协程来简化异步代码,提升网络请求的效率和可读性。我们将深入分析协程的原理,并通过一个实际案例展示如何在Android应用中集成和优化网络请求。
|
8天前
|
Java 数据库 Android开发
构建高效Android应用:探究Kotlin协程的优化实践
【4月更文挑战第22天】 随着移动开发技术的不断进步,Android平台上的性能优化已成为开发者们关注的焦点。在众多优化手段中,Kotlin协程以其轻量级线程管理和异步编程的优势,为提高应用性能和响应性提供了新的思路。本文将深入探讨Kotlin协程在Android开发中的具体应用,通过实例演示如何利用协程进行网络请求、数据库操作和UI线程的非阻塞更新,以期达到提升应用性能的目的。
12 2
|
存储 弹性计算 安全
成功案例-钉钉 | 学习笔记
快速学习 成功案例-钉钉
295 0
|
存储 弹性计算 安全
案例分享——钉钉|学习笔记
快速学习 案例分享——钉钉
275 0
|
移动开发 物联网 Go
SAP Business ByDesign 和支付宝与钉钉集成的一个原型开发案例
SAP Business ByDesign 和支付宝与钉钉集成的一个原型开发案例
SAP Business ByDesign 和支付宝与钉钉集成的一个原型开发案例