ANR及卡顿体验优化
Posted wzj_what_why_how
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ANR及卡顿体验优化相关的知识,希望对你有一定的参考价值。
ANR及卡顿体验优化
背景与意义
据统计,73%的性能问题都是由用户发现的,在这73%的问题中,严重性能问题占到
23%,遇到性能问题的用户有98%的会选择沉默,忍受、或离开,仅有2%的核心用户
才会进行投诉反馈。当应用出现崩溃、错误时会引起关键业务中断、收入减少等情况;又
如由于应用请求响应时间过长,启动较慢导致的终端用户体验下降;以及应用交互性能
慢引发的页面元素加载缓慢,造成ANR、卡顿或是页面元素加载不完整造成的布局错乱,种种
原因都会导致最终的用户流失。
因此,调研并落地ANR及卡顿体验优化是非常有必要并且极大作用于提高用户体验,减少用户流失。
ANR
ANR 全称Application Not Responding,即应用无响应。一般在测试人员进行Monkey测试的时候,ANR出现的概率会比较高。
ANR一般有三种类型
- KeyDispatchTimeout(5 seconds) --主要类型按键或触摸事件在特定时间内无响应。(比较常见)
- BroadcastTimeout(10 seconds)-- BroadcastReceiver 的 onReceive()函数运行在主线程中,在特定时间内无法处理完成。
- ServiceTimeout(20 seconds) --小概率类型 Service的各个生命周期函数 在特定的时间内无法处理完成。
超时的原因一般有两种
- 当前的事件没有机会得到处理(UI 线程正在处理前一个事件没有及时完成或者 looper 被某种原因阻塞住)
- 当前的事件正在处理,但没有及时完成UI。(解决方案:线程尽量只做跟 UI 相关的工作,耗时的工作放在子线程处理。)
(耗时任务包括:数据库操作,I/O,连接网络 或者其他可能阻碍 UI 线程的操作)
典型的ANR问题场景
- 耗时的网络访问
- 大量的数据读写
- 数据库操作
- 硬件操作(比如 camera)
- 调用 thread 的 join()方法、sleep()方法、wait()方法或者等待线程锁的时候
- service binder 的数量达到上限
- service 忙导致超时无响应
- 其他线程持有锁,导致主线程等待超时
- 其它线程终止或崩溃导致主线程一直等待
ANR的定位和分析
- 导出/data/data/anr/traces.txt,找出函数和调用过程,分析代码。
- 通过性能 LOG 人肉查找;可以找一些工具,比如使用 Bugly、Matrix等APM工具。
卡顿
卡顿问题分析
- 用户对卡顿的感知, 主要来源于界面的刷新. 而界面的性能主要是依赖于设备的UI 渲染性能。
- 如果我们的 UI 设计过于复杂, 或是实现不够友好,计算绘制算法不够优化, 设备又不给力, 界面就会像卡住了一样, 给用户卡顿的感觉。
- 如果应用界面出现卡顿不流畅的情况,很大原因是没有在16ms 完成你的工作。
16ms原则
- android 在不同的版本都会优化“UI 的流畅性”问题,但是直到在 android 4.1 版 本中做了有效的优化,这就是 Project Butter。
- Project Butter 加入了三个核心元素: VSYNC、Triple Buffer 和 Choreographer。 其中,VSYNC 是理解 Project Buffer 的核心。
- VSYNC:产生一个中断信号。
- Triple Buffer:当双 Buffer 不够使用时,该系统可分配第三块 Buffer
- Choreographer:这个用来接受一个 VSYNC 信号来统一协调 UI 更新
(关于这三个核心元素具体的原理后续叙述。)
卡顿处理
下面我们就以下几种情况导致卡顿问题进行分析处理。
过于复杂的布局
界面性能取决于 UI 渲染性能. 我们可以理解为 UI 渲染的整个过程是由 CPU 和 GPU 两个部分协同完成的。 其中,
-
CPU 负责 UI 布局元素的 Measure, Layout, Draw 等相关运算执行.
-
GPU负责栅格化, 将 UI 元素绘制到屏幕上。 如果我们的 UI 布局层次太深, 或是自定义控件的 onDraw 中有复杂运算, CPU 的相关运算就可能大于 16ms, 导致卡顿。
解决方案:
- Android Studio 3.1以下的可以借助 Hierarchy Viewer[层级观察器] 这个工具来帮我们分析布局了。
- HierarchyViewer 不仅可以以图形化树状结构的形式展示出 UI 层级, 还对每个节点给出了 三个小圆点, 以指示该元素 Measure, - - Layout, Draw 的耗时及性能。
- 关于HierarchyViewer具体可以看官网的 《使用 Hierarchy Viewer 分析布局》
- Android Studio 3.1 或更高版本的可以借助 Layout Inspector[布局检查器]
- 关于Layout Inspector具体可以看官网的 《使用布局检查器和布局验证工具调试布局》
过度绘制
Overdraw: 用来描述一个像素在屏幕上多少次被重绘在一帧上.
通俗的说: 理想情况下, 每屏每帧上, 每个像素点应该只被绘制一次, 如果有多 次绘制, 就是 Overdraw, 过度绘制了。 常见的就是:绘制了多重背景或者绘制了 不可见的 UI 元素.
解决方案:
Android 系统提供了可视化的方案来让我们很方便的查看 overdraw 的现象:
在”系统设置”–>”开发者选项”–>”调试 GPU 过度绘制”中开启调试,此时界面可能会有五种颜色标识:overdraw indicator
- 原色: 没有 overdraw
- 蓝色: 1 次 overdraw
- 绿色: 2 次 overdraw
- 粉色: 3 次 overdraw
- 红色: 4 次及 4 次以上的 overdraw
一般来说, 蓝色是可接受的, 是性能优的。
分析与处理
方式一:Logcat本地日志信息
在Logcat日志信息可以看到主要包含如下内容:
- 导致 ANR 的类名及所在包X
- 发生 ANR 的进程名称及 ID
- ANR 产生的原因(类型)
- 系统中活跃进程的 CPU 占用率
(对于一些生产包,不做处理的话则开发人员较难看到对应设备的Logcat日志,不过如果项目接入XLog工具之后,将可以拿到用户上传的Logcat日志进行辅助分析)
方式二:traces.txt文件
该文件对应位置在 /data/data/anr/traces.txt中,导出之后,可以找出函数和调用过程,分析代码。可以看到有助于问题定位的信息主要包括如下内容
-
发生ANR的进程名称.ID,以及时间
-
手机的CPU架构
-
堆内存信息
-
主线程基本信息
- 线程名称
- 线程优先级
- 线程锁ID
- 线程状态
-
主线程的详细信息
- 线程组名称
- 线程被挂起的次数
- 线程被调试器挂起的次数
- 线理的Java对象地址
- 线程本身的对象地址
-
线程的调度信息。
- Linux系统中内核线程U)
- 线程调优优先级
- 线程调度组
- 线程调度策略和优先级
- 线程处理函数地址
-
线程的上下文消息
- 线程调度状态
- 线程在CPU中的执行时间、线程等待时间、线程执行的时间片长度
- 线程在用户态中调度时间值
- 线程在内核态中的调度时间值
-
线程的堆栈位息。
- 堆栈地址和大小
- 堆栈信息
方式三:接入APM监控方案
目前有不少行业方案可以从一定程度上,帮助开发者快速定位到卡顿的堆栈,如 [BlockCanary]、[ArgusAPM]、[LogMonitor]、[Matrix]等。
另外, U-APM 和 Dokit 工具也有支持可以分析ANR和卡顿的相关功能,这里关于这两个工具就不多说明。
接下来对各类APM进行分析与对比。
APM
StrictMode
严格模式 StrictMode 是Android SDK提供的一个用来检测代码中是否存在违规操作的工具类,StrictMode 主要检测两大类:
-
线程策略 ThreadPolicy:
- detectCustomSlowCalls:检测自定义耗时操作。
- detectDiskReads:检测是否存在磁盘读取操作。
- detectDiskWrites:检测是否存在磁盘写入操作。
- detectNetwork:检测是否存在网络操作。
-
虚拟机策略VmPolicy
- detectActivityLeaks:检测是否存在Activity泄漏。
- detectLeakedClosableObjects:检测是否存在未关闭的Closable对象泄漏。
- detectLeakedSqlLiteObjects:检测是否存在Sqlite对象泄漏。
- setClassInstanceLimite:检测类实例个数是否超过限制。
可以看到,其中的ThreadPolicy可以用来检测可能存在的主线程耗时操作,解决这些检测到的问题能够减少应用发生ANR的慨率。
需要注意的是,我们只能在Debug版本中使用它. 发布到市场上的版本要关闭掉 StrictMode 的使用很简单,我们只需在应用初始化的地方例如 Application MainActivity 类的onCreate方法中执行如下代码即可。
@Override
protected void onCreate(Bundle savedlnstanceState)
if (BuildConfig.DEBUG)
//开启线程模式
StrictMode.setThreadPolicy(new StrictMode.Threadpolicy.Builder())
.detectAll()
.penaltyLog()
.build();
//开启虚拟机模式
StrictMode.setVmPolicy(new VmPolicy.Builder()).detectAll().penaltyLog().build();
super.onCreate(savedlnstanceState);
上面的初始化代码调用penaltyLog表在Logcat中打印日志.调用 detectAll 方法表示启动 所有的检测策略,我们也可以根据应用的具体需求只开启某些策略.语句如下:
StrictMode.setThreadPolicy(new StrictMode.Threadpolicy.Builder)
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.buildO);
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectActivityLeaks()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.build();
BlockCanary
BlockCanary 是一个非侵入式的性他监控函数库.它的用法和 LeakCanary 类似.只不过后者监控应用的内存泄漏,
而BlockCanary 主要用来监控应用主线程的卡顿,它的基本原理是利用主线程Looper的消息队列处理机制,监控每次 dispatchMessage 的执行耗时,
通过对比消息分发开始和结束的时间点来判断是否超过设定的时间,如果是,则判断为主线程卡顿。
它的集成很简单,首先在Build.gradle中添加依赖。然后在Application类中进行配置和初始化即可:
public void onCreate()
...
//在主线程初始化调用
BlockCannary.install(this, new AppBlockCanaryContext()).start();
public class AppBlockCanaryContext extends BlockCannaryContext
//实现各种上下文,包括应用标志服、用户uid、网络类型、卡慢判断阈值、Log保存位置等。
360的ArgusAPM
ArgusAPM是360移动端产品使用的可视化性能监控平台,为移动端APP提供性能监控与管理,可以迅速发现和定位各类APP性能和使用问题,帮助APP不断的提升用户体验。
其实现原理主要是依赖 Choreographer 模块,监控相邻两次 Vsync 事件通知的时间差。
简介:
ArgusAPM有以下几个特性:
- 非侵入式:无需修改原有工程结构,无侵入接入,接入成本低。
- 无性能损耗:ArgusAPM针对各个性能采集模块,优化了采集时机,在不影响原有性能的基础上进行性能的采集和分析。
- 监控全面:目前支持UI性能、网络性能、内存、进程、文件、卡顿、ANR等各个维度的性能数据分析,后续还会继续增加新的性能维度。
- Debug模式:支持开发和测试阶段、实时采集性能数据,实时本地分析的能力,帮助开发和测试人员在上线前解决性能问题。
- 支持插件化方案:在初始化阶段进行设置,可支持插件接入,目前360手机卫士采用的就是在RePlugin插件中接入ArgusAPM,并且性能方面无影响。
- 支持多进程采集:针对多进程的情况,我们做了相应的数据采集及优化方案,使ArgusAPM即适合单进程APP也适合多进程APP。
ArgusAPM目前支持如下性能指标:
- 交互分析:分析Activity生命周期耗时,帮助提升页面打开速度,优化用户UI体验
- 网络请求分析:监控流量使用情况,发现并定位各种网络问题
- 内存分析:全面监控内存使用情况,降低内存占用
- 进程监控:针对多进程应用,统计进程启动情况,发现启动异常(耗电、存活率等)
- 文件监控:监控APP私有文件大小/变化,避免私有文件过大导致的卡顿、存储空间占用等问题
- 卡顿分析:监控并发现卡顿原因,代码堆栈精准定位问题,解决明显的卡顿体验
- ANR分析:捕获ANR异常,解决APP的“未响应”问题。
接入流程
一. Gradle配置 在 Project 的 build.gradle 文件中添加ArgusAPM的相关配置,示例如下:
在项目根目录的 build.gradle(注意:不是 app/build.gradle) 中添加以下配置:
buildscript
repositories
jcenter()
dependencies
classpath 'com.android.tools.build:gradle:2.2.3'
classpath 'com.qihoo360.argusapm:argus-apm-gradle-asm:3.0.1.1001'
allprojects
repositories
jcenter()
二. AndroidManifest.xml配置
a. 权限相关
<!--需要申请如下权限-->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.BATTERY_STATS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
b. 组件使用 需要在AndroidManifest.xml里添加如下组件声明:
<provider
android:name="com.argusapm.android.core.storage.ApmProvider"
android:authorities="当前应用的applicationId.apm.storage"
android:exported="false" />
三. 一个简单的SDK初始化代码
在项目的Application的attachBaseContext里调用如下代码即可
Config.ConfigBuilder builder = new Config.ConfigBuilder()
.setAppContext(this)
.setAppName("apm_demo")
.setRuleRequest(new RuleSyncRequest())
.setUpload(new CollectDataSyncUpload())
.setAppVersion("0.0.1")
.setApmid("apm_demo");
Client.attach(builder.build());
Client.startWork();
Matrix
对比
-
APM方案思想:
以上的APM方案思想:监控主线程执行耗时,当超过阈值时,dump出当前主线程的执行堆栈,通过堆栈分析找到卡顿原因。
从监控主线程的实现原理上,主要分为两种:- 依赖主线程 Looper,监控每次 dispatchMessage 的执行耗时。
- 依赖 Choreographer 模块,监控相邻两次 Vsync 事件通知的时间差。
-
存在的问题:
这两种方案,可以较方便的捕捉到卡顿的堆栈,但其最大的不足在于,无法获取到各个函数的执行耗时,对于稍微复杂一点的堆栈,很难找出可能耗时的函数,也就很难找到卡顿的原因。
另外,通过其他线程循环获取主线程的堆栈,如果稍微处理不及时,很容易导致获取的堆栈有所偏移,不够准确,加上没有耗时信息,卡顿也就不好定位。 -
计算函数的执行耗时:
可以在线上准确地捕捉卡顿堆栈,又能计算出各个函数执行耗时的方案。 而要计算函数的执行耗时,最关键的点在于如何对执行过程中的函数进行打点监控。有两种方式:-
在应用启动时,默认打开 Trace 功能(Debug.startMethodTracing),应用内所有函数在执行前后将会经过该函数(dalvik 上 dvmMethodTraceAdd 函数 或 art 上 Trace::LogMethodTraceEvent 函数), 通过 hook 手段代理该函数,在每个执行方法前后进行打点记录。
-
修改字节码的方式,在编译期修改所有 class 文件中的函数字节码,对所有函数前后进行打点插桩。
第一种方案,最大的好处是能统计到包括系统函数在内的所有函数出入口,对代码或字节码不用做任何修改,所以对apk包的大小没有影响,但由于方式比较hook,在兼容性和安全性上存在一定的风险。
第二种方案,利用 Java 字节码修改工具(如 BCEL、ASM、Javassis等),在编译期间收集所有生成的 class 文件,扫描文件内的方法指令进行统一的打点插桩,同样也可以高效的记录函数执行过程中的信息。
相比第一种方案,除了无法统计系统内执行的函数,其它应用内实现的函数都可以覆盖到。而往往造成卡顿的函数并不是系统内执行的函数,一般都是我们应用开发实现的函数,所以这里无法统计系统内执行的函数对卡顿的定位影响不大。
-
此方案无需 hook 任何函数,所以在兼容性方面会比第一个方案更可靠。 Matrix-TraceCannary 便是选择了修改字节码的方案来实现,解决其它方案中卡顿堆栈无耗时信息的主要问题,来帮助开发者发现及定位卡顿问题。并且,本次调研后确定选用集成的APM方案也是这个方案。
Trace Canary特点
- 编译期动态修改字节码, 高性能记录执行耗时与调用堆栈
- 准确的定位到发生卡顿的函数,提供执行堆栈、执行耗时、执行次数等信息,帮助快速解决卡顿问题
- 自动涵盖卡顿、启动耗时、页面切换、慢函数检测等多个流畅性指标
- 准确监控ANR,并且能够高兼容性和稳定性地保存系统产生的ANR Trace文件
实现的核心思想:
通过向 Choreographer 注册监听,在每一帧 doframe 回调时判断距离上一帧的时间差是否超出阈值(卡顿),如果超出阈值,则获取数组 index 前的所有数据(即两帧之间的所有函数执行信息)进行分析上报。
同时,在每一帧 doFrame 到来时,重置一个定时器,如果 5s 内没有 cancel,则认为 ANR 发生,这时会主动取出当前记录的 buffer 数据进行独立分析上报,对这种 ANR 事件进行单独监控及定位。
(性能细节优化:(多次获取系统时间):为了减少对性能的影响,通过另一条更新时间的线程每 5ms 去更新一个时间变量,而每个方法执行前后只读取该变量来减少性能损耗。)
(堆栈聚类问题:数据量很大而且后台很难聚类有问题的堆栈,所以在上报之前需要对采集的数据进行简单的整合及裁剪,并分析出一个能代表卡顿堆栈的 key,方便后台聚合。)
热门方案对比
截图摘自Matrix Android TraceCanary
基本使用
-
项目依赖:在项目根目录下的build.gradle文件添加Matrix依赖
classpath ("com.tencent.matrix:matrix-gradle-plugin:2.0.5") changing = true
-
在 app/build.gradle 文件中添加 Matrix 各模块的依赖
apply plugin: 'com.tencent.matrix-plugin' matrix trace enable = true //if you don't want to use trace canary, set false baseMethodMapFile = "$project.buildDir/matrix_output/Debug.methodmap" blackListFile = "$project.projectDir/matrixTrace/blackMethodList.txt" implementation group: "com.tencent.matrix", name: "matrix-android-lib", version: "2.0.5", changing: true implementation group: "com.tencent.matrix", name: "matrix-android-commons", version: "2.0.5", changing: true implementation group: "com.tencent.matrix", name: "matrix-trace-canary", version: "2.0.5", changing: true implementation group: "com.tencent.matrix", name: "matrix-resource-canary-android", version: "2.0.5", changing: true implementation group: "com.tencent.matrix", name: "matrix-resource-canary-common", version: "2.0.5", changing: true implementation group: "com.tencent.matrix", name: "matrix-io-canary", version: "2.0.5", changing: true implementation group: "com.tencent.matrix", name: "matrix-sqlite-lint-android-sdk", version: "2.0.5", changing: true implementation group: "com.tencent.matrix", name: "matrix-battery-canary", version: "2.0.5", changing: true implementation group: "com.tencent.matrix", name: "matrix-hooks", version: "2.0.5", changing: true
-
接收Matrix处理后的数据 (参考 Matrix中sample-android示例代码 TestPluginListener )
/** * 接收 Matrix 处理后的数据 */ public class TestPluginListener extends DefaultPluginListener public static final String TAG = "Matrix.TestPluginListener"; public TestPluginListener(Context context) super(context); @Override public void onReportIssue(Issue issue) super.onReportIssue(issue); MatrixLog.e(TAG, issue.toString()); //add your code to process data
-
实现动态配置接口, 可修改 Matrix 内部参数. 在 sample-android 中 有个简单的动态接口实例DynamicConfigImplDemo.java, 其中参数对应的 key 位于文件 MatrixEnum中, 摘抄部分示例如下:
public class DynamicConfigImplDemo implements IDynamicConfig public DynamicConfigImplDemo() public boolean isFPSEnable() return true; public boolean isTraceEnable() return true; public boolean isMatrixEnable() return true; public boolean isDumphprof() return false; @Override public String get(String key, String defStr) //hook to change default values @Override public int get(String key, int defInt) //hook to change default values @Override public long get(String key, long defLong) //hook to change default values @Override public boolean get(String key, boolean defBool) //hook to change default values @Override public float get(String key, float defFloat) //hook to change default values
-
选择程序启动的位置对 Matrix 进行初始化,如在 Application 的继承类中, Init 核心逻辑如下:
Matrix.Builder builder = new Matrix.Builder(application); // build matrix builder.pluginListener(new TestPluginListener(this)); // add general pluginListener DynamicConfigImplDemo dynamicConfig = new DynamicConfigImplDemo(); // dynamic config // init plugin IOCanaryPlugin ioCanaryPlugin = new IOCanaryPlugin(new IOConfig.Builder() .dynamicConfig(dynamicConfig) .build()); //add to matrix builder.plugin(ioCanaryPlugin); //init matrix Matrix.init(builder.build()); // start plugin ioCanaryPlugin.start();
至此,Matrix就已成功集成到项目中,并且开始收集和分析性能相关异常数据。
参考资料:
- 《Android高阶进阶(顾浩鑫)》第十二章
- 《2021移动应用性能管理白皮书》
- 《使用 Hierarchy Viewer 分析布局》
- 《使用布局检查器和布局验证工具调试布局》
- 360的ArgusAPM
- Matrix Android TraceCanary
以上是关于ANR及卡顿体验优化的主要内容,如果未能解决你的问题,请参考以下文章