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

相关文章
|
1月前
|
移动开发 监控 Android开发
Android & iOS 使用 ARMS 用户体验监控(RUM)的最佳实践
本文主要介绍了 ARMS 用户体验监控的基本功能特性,并介绍了在几种常见场景下的最佳实践。
|
2月前
|
存储 缓存 编解码
Android经典面试题之图片Bitmap怎么做优化
本文介绍了图片相关的内存优化方法,包括分辨率适配、图片压缩与缓存。文中详细讲解了如何根据不同分辨率放置图片资源,避免图片拉伸变形;并通过示例代码展示了使用`BitmapFactory.Options`进行图片压缩的具体步骤。此外,还介绍了Glide等第三方库如何利用LRU算法实现高效图片缓存。
64 20
Android经典面试题之图片Bitmap怎么做优化
|
1月前
|
调度 Android开发 开发者
构建高效Android应用:探究Kotlin多线程优化策略
【10月更文挑战第11天】本文探讨了如何在Kotlin中实现高效的多线程方案,特别是在Android应用开发中。通过介绍Kotlin协程的基础知识、异步数据加载的实际案例,以及合理使用不同调度器的方法,帮助开发者提升应用性能和用户体验。
46 4
|
13天前
|
安全 Android开发 iOS开发
深入探索iOS与Android系统的差异性及优化策略
在当今数字化时代,移动操作系统的竞争尤为激烈,其中iOS和Android作为市场上的两大巨头,各自拥有庞大的用户基础和独特的技术特点。本文旨在通过对比分析iOS与Android的核心差异,探讨各自的优势与局限,并提出针对性的优化策略,以期为用户提供更优质的使用体验和为开发者提供有价值的参考。
|
2月前
|
Java Android开发 UED
安卓应用开发中的内存管理优化技巧
在安卓开发的广阔天地里,内存管理是一块让开发者既爱又恨的领域。它如同一位严苛的考官,时刻考验着开发者的智慧与耐心。然而,只要我们掌握了正确的优化技巧,就能够驯服这位考官,让我们的应用在性能和用户体验上更上一层楼。本文将带你走进内存管理的迷宫,用通俗易懂的语言解读那些看似复杂的优化策略,让你的开发之路更加顺畅。
59 2
|
2月前
|
Java Android开发 开发者
安卓应用开发中的线程管理优化技巧
【9月更文挑战第10天】在安卓开发的海洋里,线程管理犹如航行的风帆,掌握好它,能让应用乘风破浪,反之则可能遭遇性能的暗礁。本文将通过浅显易懂的语言和生动的比喻,带你探索如何优雅地处理安卓中的线程问题,从基础的线程创建到高级的线程池运用,让你的应用运行更加流畅。
|
2月前
|
监控 算法 数据可视化
深入解析Android应用开发中的高效内存管理策略在移动应用开发领域,Android平台因其开放性和灵活性备受开发者青睐。然而,随之而来的是内存管理的复杂性,这对开发者提出了更高的要求。高效的内存管理不仅能够提升应用的性能,还能有效避免因内存泄漏导致的应用崩溃。本文将探讨Android应用开发中的内存管理问题,并提供一系列实用的优化策略,帮助开发者打造更稳定、更高效的应用。
在Android开发中,内存管理是一个绕不开的话题。良好的内存管理机制不仅可以提高应用的运行效率,还能有效预防内存泄漏和过度消耗,从而延长电池寿命并提升用户体验。本文从Android内存管理的基本原理出发,详细讨论了几种常见的内存管理技巧,包括内存泄漏的检测与修复、内存分配与回收的优化方法,以及如何通过合理的编程习惯减少内存开销。通过对这些内容的阐述,旨在为Android开发者提供一套系统化的内存优化指南,助力开发出更加流畅稳定的应用。
69 0
|
3天前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义视图:打造个性化UI组件
【10月更文挑战第39天】在安卓开发的世界中,自定义视图是实现独特界面设计的关键。本文将引导你理解自定义视图的概念、创建流程,以及如何通过它们增强应用的用户体验。我们将从基础出发,逐步深入,最终让你能够自信地设计和实现专属的UI组件。
|
5天前
|
Android开发 Swift iOS开发
探索安卓与iOS开发的差异和挑战
【10月更文挑战第37天】在移动应用开发的广阔舞台上,安卓和iOS这两大操作系统扮演着主角。它们各自拥有独特的特性、优势以及面临的开发挑战。本文将深入探讨这两个平台在开发过程中的主要差异,从编程语言到用户界面设计,再到市场分布的不同影响,旨在为开发者提供一个全面的视角,帮助他们更好地理解并应对在不同平台上进行应用开发时可能遇到的难题和机遇。
|
7天前
|
XML 存储 Java
探索安卓开发之旅:从新手到专家
【10月更文挑战第35天】在数字化时代,安卓应用的开发成为了一个热门话题。本文旨在通过浅显易懂的语言,带领初学者了解安卓开发的基础知识,同时为有一定经验的开发者提供进阶技巧。我们将一起探讨如何从零开始构建第一个安卓应用,并逐步深入到性能优化和高级功能的实现。无论你是编程新手还是希望提升技能的开发者,这篇文章都将为你提供有价值的指导和灵感。