• 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. 权限相关

    
    
    
    
    
    
    
    
    
    

    b. 组件使用 需要在AndroidManifest.xml里添加如下组件声明:

    
    

    三. 一个简单的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,方便后台聚合。)

    热门方案对比

    APM方案对比.png
    截图摘自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就已成功集成到项目中,并且开始收集和分析性能相关异常数据。

    参考资料:

  • 相关阅读:
    Web APIs 第03天上
    nginx代理socket链接集群后,频繁断开重连
    计算机系统结构期末复习
    【linux】——程序地址空间_终
    数字滚动组件(react)
    acwing算法基础之搜索与图论--树与图的遍历
    快速实现本地数据备份与FTP远程数据迁移
    程序员笔记本电脑选 windows 还是 MAC
    【目标检测】30、
    如何应对数字时代的网络安全新挑战?
  • 原文地址:https://blog.csdn.net/wzj_what_why_how/article/details/127118721