据统计,73%的性能问题都是由用户发现的,在这73%的问题中,严重性能问题占到
23%,遇到性能问题的用户有98%的会选择沉默,忍受、或离开,仅有2%的核心用户
才会进行投诉反馈。当应用出现崩溃、错误时会引起关键业务中断、收入减少等情况;又
如由于应用请求响应时间过长,启动较慢导致的终端用户体验下降;以及应用交互性能
慢引发的页面元素加载缓慢,造成ANR、卡顿或是页面元素加载不完整造成的布局错乱,种种
原因都会导致最终的用户流失。
因此,调研并落地ANR及卡顿体验优化是非常有必要并且极大作用于提高用户体验,减少用户流失。
ANR 全称Application Not Responding,即应用无响应。一般在测试人员进行Monkey测试的时候,ANR出现的概率会比较高。
下面我们就以下几种情况导致卡顿问题进行分析处理。
界面性能取决于 UI 渲染性能. 我们可以理解为 UI 渲染的整个过程是由 CPU 和 GPU 两个部分协同完成的。 其中,
CPU 负责 UI 布局元素的 Measure, Layout, Draw 等相关运算执行.
GPU负责栅格化, 将 UI 元素绘制到屏幕上。 如果我们的 UI 布局层次太深, 或是自定义控件的 onDraw 中有复杂运算, CPU 的相关运算就可能大于 16ms, 导致卡顿。
解决方案:
Overdraw: 用来描述一个像素在屏幕上多少次被重绘在一帧上.
通俗的说: 理想情况下, 每屏每帧上, 每个像素点应该只被绘制一次, 如果有多 次绘制, 就是 Overdraw, 过度绘制了。 常见的就是:绘制了多重背景或者绘制了 不可见的 UI 元素.
解决方案:
Android 系统提供了可视化的方案来让我们很方便的查看 overdraw 的现象:
在”系统设置”–>”开发者选项”–>”调试 GPU 过度绘制”中开启调试,此时界面可能会有五种颜色标识:overdraw indicator
在Logcat日志信息可以看到主要包含如下内容:
该文件对应位置在 /data/data/anr/traces.txt中,导出之后,可以找出函数和调用过程,分析代码。可以看到有助于问题定位的信息主要包括如下内容
发生ANR的进程名称.ID,以及时间
手机的CPU架构
堆内存信息
主线程基本信息
主线程的详细信息
线程的调度信息。
线程的上下文消息
线程的堆栈位息。
目前有不少行业方案可以从一定程度上,帮助开发者快速定位到卡顿的堆栈,如 [BlockCanary]、[ArgusAPM]、[LogMonitor]、[Matrix]等。
另外, U-APM 和 Dokit 工具也有支持可以分析ANR和卡顿的相关功能,这里关于这两个工具就不多说明。
接下来对各类APM进行分析与对比。
严格模式 StrictMode 是Android SDK提供的一个用来检测代码中是否存在违规操作的工具类,StrictMode 主要检测两大类:
线程策略 ThreadPolicy:
虚拟机策略VmPolicy
可以看到,其中的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 是一个非侵入式的性他监控函数库.它的用法和 LeakCanary 类似.只不过后者监控应用的内存泄漏,
而BlockCanary 主要用来监控应用主线程的卡顿,它的基本原理是利用主线程Looper的消息队列处理机制,监控每次 dispatchMessage 的执行耗时,
通过对比消息分发开始和结束的时间点来判断是否超过设定的时间,如果是,则判断为主线程卡顿。
它的集成很简单,首先在Build.gradle中添加依赖。然后在Application类中进行配置和初始化即可:
public void onCreate(){
...
//在主线程初始化调用
BlockCannary.install(this, new AppBlockCanaryContext()).start();
}
public class AppBlockCanaryContext extends BlockCannaryContext{
//实现各种上下文,包括应用标志服、用户uid、网络类型、卡慢判断阈值、Log保存位置等。
}
ArgusAPM是360移动端产品使用的可视化性能监控平台,为移动端APP提供性能监控与管理,可以迅速发现和定位各类APP性能和使用问题,帮助APP不断的提升用户体验。
其实现原理主要是依赖 Choreographer 模块,监控相邻两次 Vsync 事件通知的时间差。
ArgusAPM有以下几个特性:
ArgusAPM目前支持如下性能指标:
一. 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();
APM方案思想:
以上的APM方案思想:监控主线程执行耗时,当超过阈值时,dump出当前主线程的执行堆栈,通过堆栈分析找到卡顿原因。
从监控主线程的实现原理上,主要分为两种:
存在的问题:
这两种方案,可以较方便的捕捉到卡顿的堆栈,但其最大的不足在于,无法获取到各个函数的执行耗时,对于稍微复杂一点的堆栈,很难找出可能耗时的函数,也就很难找到卡顿的原因。
另外,通过其他线程循环获取主线程的堆栈,如果稍微处理不及时,很容易导致获取的堆栈有所偏移,不够准确,加上没有耗时信息,卡顿也就不好定位。
计算函数的执行耗时:
可以在线上准确地捕捉卡顿堆栈,又能计算出各个函数执行耗时的方案。 而要计算函数的执行耗时,最关键的点在于如何对执行过程中的函数进行打点监控。有两种方式:
在应用启动时,默认打开 Trace 功能(Debug.startMethodTracing),应用内所有函数在执行前后将会经过该函数(dalvik 上 dvmMethodTraceAdd 函数 或 art 上 Trace::LogMethodTraceEvent 函数), 通过 hook 手段代理该函数,在每个执行方法前后进行打点记录。
修改字节码的方式,在编译期修改所有 class 文件中的函数字节码,对所有函数前后进行打点插桩。
第一种方案,最大的好处是能统计到包括系统函数在内的所有函数出入口,对代码或字节码不用做任何修改,所以对apk包的大小没有影响,但由于方式比较hook,在兼容性和安全性上存在一定的风险。
第二种方案,利用 Java 字节码修改工具(如 BCEL、ASM、Javassis等),在编译期间收集所有生成的 class 文件,扫描文件内的方法指令进行统一的打点插桩,同样也可以高效的记录函数执行过程中的信息。
相比第一种方案,除了无法统计系统内执行的函数,其它应用内实现的函数都可以覆盖到。而往往造成卡顿的函数并不是系统内执行的函数,一般都是我们应用开发实现的函数,所以这里无法统计系统内执行的函数对卡顿的定位影响不大。
此方案无需 hook 任何函数,所以在兼容性方面会比第一个方案更可靠。 Matrix-TraceCannary 便是选择了修改字节码的方案来实现,解决其它方案中卡顿堆栈无耗时信息的主要问题,来帮助开发者发现及定位卡顿问题。并且,本次调研后确定选用集成的APM方案也是这个方案。
通过向 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就已成功集成到项目中,并且开始收集和分析性能相关异常数据。
参考资料: