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的各个生命周期函数 在特定的时间内无法处理完成。

超时的原因一般有两种

  1. 当前的事件没有机会得到处理(UI 线程正在处理前一个事件没有及时完成或者 looper 被某种原因阻塞住)
  2. 当前的事件正在处理,但没有及时完成UI。(解决方案:线程尽量只做跟 UI 相关的工作,耗时的工作放在子线程处理。)
    (耗时任务包括:数据库操作,I/O,连接网络 或者其他可能阻碍 UI 线程的操作)

典型的ANR问题场景

  1. 耗时的网络访问
  2. 大量的数据读写
  3. 数据库操作
  4. 硬件操作(比如 camera)
  5. 调用 thread 的 join()方法、sleep()方法、wait()方法或者等待线程锁的时候
  6. service binder 的数量达到上限
  7. service 忙导致超时无响应
  8. 其他线程持有锁,导致主线程等待超时
  9. 其它线程终止或崩溃导致主线程一直等待

ANR的定位和分析

  1. 导出/data/data/anr/traces.txt,找出函数和调用过程,分析代码。
  2. 通过性能 LOG 人肉查找;可以找一些工具,比如使用 Bugly、Matrix等APM工具。

卡顿

卡顿问题分析

  1. 用户对卡顿的感知, 主要来源于界面的刷新. 而界面的性能主要是依赖于设备的UI 渲染性能。
  2. 如果我们的 UI 设计过于复杂, 或是实现不够友好,计算绘制算法不够优化, 设备又不给力, 界面就会像卡住了一样, 给用户卡顿的感觉。
  3. 如果应用界面出现卡顿不流畅的情况,很大原因是没有在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出当前主线程的执行堆栈,通过堆栈分析找到卡顿原因。
    从监控主线程的实现原理上,主要分为两种:

    1. 依赖主线程 Looper,监控每次 dispatchMessage 的执行耗时。
    2. 依赖 Choreographer 模块,监控相邻两次 Vsync 事件通知的时间差。
  • 存在的问题:
    这两种方案,可以较方便的捕捉到卡顿的堆栈,但其最大的不足在于,无法获取到各个函数的执行耗时,对于稍微复杂一点的堆栈,很难找出可能耗时的函数,也就很难找到卡顿的原因。
    另外,通过其他线程循环获取主线程的堆栈,如果稍微处理不及时,很容易导致获取的堆栈有所偏移,不够准确,加上没有耗时信息,卡顿也就不好定位。

  • 计算函数的执行耗时:
    可以在线上准确地捕捉卡顿堆栈,又能计算出各个函数执行耗时的方案。 而要计算函数的执行耗时,最关键的点在于如何对执行过程中的函数进行打点监控。有两种方式:

    1. 在应用启动时,默认打开 Trace 功能(Debug.startMethodTracing),应用内所有函数在执行前后将会经过该函数(dalvik 上 dvmMethodTraceAdd 函数 或 art 上 Trace::LogMethodTraceEvent 函数), 通过 hook 手段代理该函数,在每个执行方法前后进行打点记录。

    2. 修改字节码的方式,在编译期修改所有 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就已成功集成到项目中,并且开始收集和分析性能相关异常数据。

参考资料:

以上是关于ANR及卡顿体验优化的主要内容,如果未能解决你的问题,请参考以下文章

ANR及卡顿体验优化

Android程序性能优化——ANR卡顿优化内存优化耗电优化APK大小优化以及启动速度和实战项目

Android性能优化高阶:卡顿ANR死锁,线上如何监控?

吹爆系列:深入实战Android卡顿优化

卡顿ANR死锁,线上如何监控?

卡顿ANR死锁,线上如何监控?