在上篇文章中,笔者带领你们学习了卡顿优化分析方法与工具、自动化卡顿检测方案及优化这两块内容。若是对这块内容还不了解的同窗建议先看看《深刻探索Android卡顿优化(上)》。本篇,为深刻探索Android卡顿优化的下篇。这篇文章包含的主要内容以下所示:html
卡顿时间过长,必定会形成应用发生ANR。下面,咱们就来从应用的ANR分析与实战来开始今天的探索之旅。java
首先,咱们再来回顾一下ANR的几种常见的类型,以下所示:linux
具体的时间定义咱们能够在AMS(ActivityManagerService)中找到:android
// How long we allow a receiver to run before giving up on it.
static final int BROADCAST_FG_TIMEOUT = 10*1000;
static final int BROADCAST_BG_TIMEOUT = 60*1000;
// How long we wait until we timeout on key dispatching.
static final int KEY_DISPATCHING_TIMEOUT = 5*1000;
复制代码
接下来,咱们来看一下ANR的执行流程。git
分析完ANR的执行流程以后,咱们来分析下怎样去解决ANR,究竟哪里能够做为咱们的一个突破点。github
在上面咱们说过,当应用发生ANR时,会写入当时发生ANR的场景信息到文件中,那么,咱们可不能够经过这个文件来判断是否发生了ANR呢?算法
关于根据ANR log进行ANR问题的排查与解决的方式笔者已经在深刻探索Android稳定性优化的第三节ANR优化中讲解过了,这里就很少赘述了。shell
在深刻探索Android稳定性优化的第三节ANR优化中我说到了使用FileObserver能够监听 /data/anr/traces.txt的变化,利用它能够实现线上ANR的监控,可是它有一个致命的缺点,就是高版本ROM须要root权限,解决方案是只能经过海外Google Play服务、国内Hardcoder的方式去规避。可是,这在国内显然是不现实的,那么,有没有更好的实现方式呢?json
那就是ANR-WatchDog,下面我就来详细地介绍一下它。c#
ANR-WatchDog是一种非侵入式的ANR监控组件,能够用于线上ANR的监控,接下来,咱们就使用ANR-WatchDog来监控ANR。
首先,在咱们项目的app/build.gradle中添加以下依赖:
implementation 'com.github.anrwatchdog:anrwatchdog:1.4.0'
复制代码
而后,在应用的Application的onCreate方法中添加以下代码启动ANR-WatchDog:
new ANRWatchDog().start();
复制代码
能够看到,它的初始化方式很是地简单,同时,它内部的实现也很是简单,整个库只有两个类,一个是ANRWatchDog,另外一个是ANRError。
接下来咱们来看一下ANRWatchDog的实现方式。
/**
* A watchdog timer thread that detects when the UI thread has frozen.
*/
public class ANRWatchDog extends Thread {
复制代码
能够看到,ANRWatchDog其实是继承了Thread类,也就是它是一个线程,对于线程来讲,最重要的就是其run方法,以下所示:
private static final int DEFAULT_ANR_TIMEOUT = 5000;
private volatile long _tick = 0;
private volatile boolean _reported = false;
private final Runnable _ticker = new Runnable() {
@Override public void run() {
_tick = 0;
_reported = false;
}
};
@Override
public void run() {
// 一、首先,将线程命名为|ANR-WatchDog|。
setName("|ANR-WatchDog|");
// 二、接着,声明了一个默认的超时间隔时间,默认的值为5000ms。
long interval = _timeoutInterval;
// 三、而后,在while循环中经过_uiHandler去post一个_ticker Runnable。
while (!isInterrupted()) {
// 3.1 这里的_tick默认是0,因此needPost即为true。
boolean needPost = _tick == 0;
// 这里的_tick加上了默认的5000ms
_tick += interval;
if (needPost) {
_uiHandler.post(_ticker);
}
// 接下来,线程会sleep一段时间,默认值为5000ms。
try {
Thread.sleep(interval);
} catch (InterruptedException e) {
_interruptionListener.onInterrupted(e);
return ;
}
// 四、若是主线程没有处理Runnable,即_tick的值没有被赋值为0,则说明发生了ANR,第二个_reported标志位是为了不重复报道已经处理过的ANR。
if (_tick != 0 && !_reported) {
//noinspection ConstantConditions
if (!_ignoreDebugger && (Debug.isDebuggerConnected() || Debug.waitingForDebugger())) {
Log.w("ANRWatchdog", "An ANR was detected but ignored because the debugger is connected (you can prevent this with setIgnoreDebugger(true))");
_reported = true;
continue ;
}
interval = _anrInterceptor.intercept(_tick);
if (interval > 0) {
continue;
}
final ANRError error;
if (_namePrefix != null) {
error = ANRError.New(_tick, _namePrefix, _logThreadsWithoutStackTrace);
} else {
// 五、若是没有主动给ANR_Watchdog设置线程名,则会默认会使用ANRError的NewMainOnly方法去处理ANR。
error = ANRError.NewMainOnly(_tick);
}
// 六、最后会经过ANRListener调用它的onAppNotResponding方法,其默认的处理会直接抛出当前的ANRError,致使程序崩溃。 _anrListener.onAppNotResponding(error);
interval = _timeoutInterval;
_reported = true;
}
}
}
复制代码
首先,在注释1处,咱们将线程命名为了|ANR-WatchDog|。接着,在注释2处,声明了一个默认的超时间隔时间,默认的值为5000ms。而后,注释3处,在while循环中经过_uiHandler去post一个_ticker Runnable。注意这里的_tick默认是0,因此needPost即为true。接下来,线程会sleep一段时间,默认值为5000ms。在注释4处,若是主线程没有处理Runnable,即_tick的值没有被赋值为0,则说明发生了ANR,第二个_reported标志位是为了不重复报道已经处理过的ANR。若是发生了ANR,就会调用接下来的代码,开始会处理debug的状况,而后,咱们看到注释5处,若是没有主动给ANR_Watchdog设置线程名,则会默认会使用ANRError的NewMainOnly方法去处理ANR。ANRError的NewMainOnly方法以下所示:
/**
* The minimum duration, in ms, for which the main thread has been blocked. May be more.
*/
public final long duration;
static ANRError NewMainOnly(long duration) {
// 一、获取主线程的堆栈信息
final Thread mainThread = Looper.getMainLooper().getThread();
final StackTraceElement[] mainStackTrace = mainThread.getStackTrace();
// 二、返回一个包含主线程名、主线程堆栈信息以及发生ANR的最小时间值的实例。
return new ANRError(new $(getThreadTitle(mainThread), mainStackTrace).new _Thread(null), duration);
}
复制代码
能够看到,在注释1处,首先获了主线程的堆栈信息,而后返回了一个包含主线程名、主线程堆栈信息以及发生ANR的最小时间值的实例。(咱们能够改造其源码在此时添加更多的卡顿现场信息,如CPU 使用率和调度信息、内存相关信息、I/O 和网络相关的信息等等)
接下来,咱们再回到ANRWatchDog的run方法中的注释6处,最后这里会经过ANRListener调用它的onAppNotResponding方法,其默认的处理会直接抛出当前的ANRError,致使程序崩溃。对应的代码以下所示:
private static final ANRListener DEFAULT_ANR_LISTENER = new ANRListener() {
@Override public void onAppNotResponding(ANRError error) {
throw error;
}
};
复制代码
了解了ANRWatchDog的实现原理以后,咱们试一试它的效果如何。首先,咱们给MainActivity中的悬浮按钮添加主线程休眠10s的代码,以下所示:
@OnClick({R.id.main_floating_action_btn})
void onClick(View view) {
switch (view.getId()) {
case R.id.main_floating_action_btn:
try {
// 对应项目中的第170行
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
jumpToTheTop();
break;
default:
break;
}
}
复制代码
而后,咱们从新安装运行项目,点击悬浮按钮,发如今10s内都不能触发屏幕点击和触摸事件,而且在10s以后,应用直接发生了崩溃。接着,咱们在Logcat过滤栏中输入fatal关键字,找出致命的错误,log以下所示:
2020-01-18 09:55:53.459 29924-29969/? E/AndroidRuntime: FATAL EXCEPTION: |ANR-WatchDog|
Process: json.chao.com.wanandroid, PID: 29924
com.github.anrwatchdog.ANRError: Application Not Responding for at least 5000 ms.
Caused by: com.github.anrwatchdog.ANRError$$$_Thread: main (state = TIMED_WAITING)
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:373)
at java.lang.Thread.sleep(Thread.java:314)
// 1
at json.chao.com.wanandroid.ui.main.activity.MainActivity.onClick(MainActivity.java:170)
at json.chao.com.wanandroid.ui.main.activity.MainActivity_ViewBinding$1.doClick(MainActivity_ViewBinding.java:45)
at butterknife.internal.DebouncingOnClickListener.onClick(DebouncingOnClickListener.java:22)
at android.view.View.performClick(View.java:6311)
at android.view.View$PerformClick.run(View.java:24833)
at android.os.Handler.handleCallback(Handler.java:794)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:173)
at android.app.ActivityThread.main(ActivityThread.java:6653)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:821)
Caused by: com.github.anrwatchdog.ANRError$$$_Thread: AndroidFileLogger./storage/emulated/0/Android/data/json.chao.com.wanandroid/log/ (state = RUNNABLE)
复制代码
能够看到,发生崩溃的线程正是|ANR-WatchDog|。咱们重点关注注释1,这里发生崩溃的位置是在MainActivity的onClick方法,对应的行数为170行,从前可知,这里正是线程休眠的地方。
接下来,咱们来分析一下ANR-WatchDog的实现原理。
最后,ANR-WatchDog的工做流程简图以下所示:
上面咱们最后说到,若是检测到主线程发生了卡顿,则会抛出一个ANR异常,这将会致使应用崩溃,显然不能将这种方案带到线上,那么,有什么方式可以自定义最后发生卡顿时的处理过程吗?
其实ANR-WatchDog自身就实现了一个咱们自身也能够去实现的ANRListener,经过它,咱们就能够对ANR事件去作一个自定义的处理,好比将堆栈信息压缩后保存到本地,并在适当的时间上传到APM后台。
ANR-WatchDog是一种非侵入式的ANR监控方案,它可以弥补咱们在高版本中没有权限去读取traces.txt文件的问题,须要注意的是,在线上这两种方案咱们须要结合使用。
在以前,咱们还讲到了AndroidPerformanceMonitor,那么它和ANR-WatchDog有什么区别呢?
对于AndroidPerformanceMonitor来讲,它是监控咱们主线程中每个message的执行,它会在主线程的每个message的先后打印一个时间戳,而后,咱们就能够据此计算每个message的具体执行时间,可是咱们须要注意的是一个message的执行时间一般是很是短暂的,也就是很难达到ANR这个级别。而后咱们来看看ANR-WatchDog的原理,它是无论应用是如何执行的,它只会看最终的结果,即sleep 5s以后,我就看主线程的这个值有没有被更改。若是说被改过,就说明没有发生ANR,不然,就代表发生了ANR。
根据这两个库的原理,咱们即可以判断出它们分别的适用场景,对于AndroidPerformanceMonitor来讲,它适合监控卡顿,由于每个message它执行的时间并不长。对于ANR-WatchDog来讲,它更加适合于ANR监控的补充。
此外,虽然ANR-WatchDog解决了在高版本系统没有权限读取 /data/anr/traces.txt 文件的问题,可是在Java层去获取全部线程堆栈以及各类信息很是耗时,对于卡顿场景不必定合适,它可能会进一步加重用户的卡顿。若是是对性能要求比较高的应用,能够经过Hook Native层的方式去得到全部线程的堆栈信息,具体为以下两个步骤:
经过这种方式就大体模拟了系统打印 ANR 日志的流程,可是因为采用的是Hook方式,因此可能会产生一些异常甚至崩溃的状况,这个时候就须要经过 fork 子进程方式去避免这种问题,并且使用 子进程去获取堆栈信息的方式能够作到彻底不卡住咱们主进程。
可是须要注意的是,fork 进程会致使进程号发生改变,此时须要经过指定 /proc/[父进程 id]的方式从新获取应用主进程的堆栈信息。
经过 Native Hook 的 方式咱们实现了一套“无损”获取全部 Java 线程堆栈与详细信息的卡顿监控体系。为了下降上报数据量,建议只有主线程的 Java 线程状态是 WAITING、TIME_WAITING 或者 BLOCKED 的时候,才去使用这套方案。
除了自动化的卡顿与ANR监控以外,咱们还须要进行卡顿单点问题的检测,由于上述两种检测方案的并不能知足全部场景的检测要求,这里我举一个小栗子:
好比我有不少的message要执行,可是每个message的执行时间
都不到卡顿的阈值,那自动化卡顿检测方案也就不可以检测出卡
顿,可是对用户来讲,用户就以为你的App就是有些卡顿。
复制代码
除此以外,为了创建体系化的监控解决方案,咱们就必须在上线以前将问题尽量地暴露出来。
常见的单点问题有主线程IPC、DB操做等等,这里我就拿主线程IPC来讲,由于IPC实际上是一个很耗时的操做,可是在实际开发过程当中,咱们可能对IPC操做没有足够的重视,因此,咱们常常在主程序中去作频繁IPC操做,因此说,这种耗时它可能并不到你设定卡顿的一个阈值,接下来,咱们看一下,对于IPC问题,咱们应该去监测哪些指标。
常规方案就是在IPC的先后加上埋点。可是,这种方式不够优雅,并且,在日常开发过程当中咱们常常忘记某个埋点的真正用处,同时它的维护成本也很是大。
接下来,咱们讲解一下IPC问题监测的技巧。
在线下,咱们能够经过adb命令的方式来进行监测,以下所示:
// 一、首先,对IPC操做开始进行监控
adb shell am trace-ipc start
// 二、而后,结束IPC操做的监控,同时,将监控到的信息存放到指定的文件当中
adb shell am trace-ipc stop -dump-file /data/local/tmp/ipc-trace.txt
// 三、最后,将监控到的ipc-trace导出到电脑查看
adb pull /data/local/tmp/ipc-trace.txt
复制代码
而后,这里咱们介绍一种优雅的实现方案,看过深刻探索Android布局优化(上)的同窗可能知道这里的实现方案无非就是ARTHook或AspectJ这两种方案,这里咱们须要去监控IPC操做,那么,咱们应该选用哪一种方式会更好一些呢?(利用epic实现ARTHook)
要回答这个问题,就须要咱们对ARTHook和AspectJ这二者的思想有足够的认识,对应ARTHook来讲,其实咱们能够用它来去Hook系统的一些方法,由于对于系统代码来讲,咱们没法对它进行更改,可是咱们能够Hook住它的一个方法,在它的方法体里面去加上本身的一些代码。可是,对于AspectJ来讲,它只能针对于那些非系统方法,也就是咱们App本身的源码,或者是咱们所引用到的一些jar、aar包。由于AspectJ其实是往咱们的具体方法里面插入相对应的代码,因此说,他不可以针对于咱们的系统方法去作操做,在这里,咱们就须要采用ARTHook的方式去进行IPC操做的监控。
在使用ARTHook去监控IPC操做以前,咱们首先思考一下,哪些操做是IPC操做呢?
好比说,咱们经过PackageManager去拿到咱们应用的一些信息,或者去拿到设备的DeviceId这样的信息以及AMS相关的信息等等,这些其实都涉及到了IPC的操做,而这些操做都会经过固定的方式进行IPC,并最终会调用到android.os.BinderProxy,接下来,咱们来看看它的transact方法,以下所示:
public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
复制代码
这里咱们仅仅关注transact方法的参数便可,第一个参数是一个行动编码,为int类型,它是在FIRST_CALL_TRANSACTION与LAST_CALL_TRANSACTION之间的某个值,第2、三个参数都是Parcel类型的参数,用于获取和回复相应的数据,第四个参数为一个int类型的标记值,为0表示一个正常的IPC调用,不然代表是一个单向的IPC调用。而后,咱们在项目中的Application的onCreate方法中使用ARTHook对android.os.BinderProxy类的transact方法进行Hook,代码以下所示:
try {
DexposedBridge.findAndHookMethod(Class.forName("android.os.BinderProxy"), "transact",
int.class, Parcel.class, Parcel.class, int.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
LogHelper.i( "BinderProxy beforeHookedMethod " + param.thisObject.getClass().getSimpleName()
+ "\n" + Log.getStackTraceString(new Throwable()));
super.beforeHookedMethod(param);
}
});
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
复制代码
从新安装应用,便可看到以下的Log信息:
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ WanAndroidApp$1.beforeHookedMethod (WanAndroidApp.java:160)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ LogHelper.i (LogHelper.java:37)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ [WanAndroidApp.java | 160 | beforeHookedMethod] BinderProxy beforeHookedMethod BinderProxy
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ java.lang.Throwable
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at json.chao.com.wanandroid.app.WanAndroidApp$1.beforeHookedMethod(WanAndroidApp.java:160)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at com.taobao.android.dexposed.DexposedBridge.handleHookedArtMethod(DexposedBridge.java:237)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at me.weishu.epic.art.entry.Entry64.onHookBoolean(Entry64.java:72)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at me.weishu.epic.art.entry.Entry64.referenceBridge(Entry64.java:237)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at me.weishu.epic.art.entry.Entry64.booleanBridge(Entry64.java:86)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.os.ServiceManagerProxy.getService(ServiceManagerNative.java:123)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.os.ServiceManager.getService(ServiceManager.java:56)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.os.ServiceManager.getServiceOrThrow(ServiceManager.java:71)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.app.UiModeManager.<init>(UiModeManager.java:127)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.app.SystemServiceRegistry$42.createService(SystemServiceRegistry.java:511)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.app.SystemServiceRegistry$42.createService(SystemServiceRegistry.java:509)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.app.SystemServiceRegistry$CachedServiceFetcher.getService(SystemServiceRegistry.java:970)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.app.SystemServiceRegistry.getSystemService(SystemServiceRegistry.java:920)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.app.ContextImpl.getSystemService(ContextImpl.java:1677)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.view.ContextThemeWrapper.getSystemService(ContextThemeWrapper.java:171)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.app.Activity.getSystemService(Activity.java:6003)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.support.v7.app.AppCompatDelegateImplV23.<init>(AppCompatDelegateImplV23.java:33)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.support.v7.app.AppCompatDelegateImplN.<init>(AppCompatDelegateImplN.java:31)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.support.v7.app.AppCompatDelegate.create(AppCompatDelegate.java:198)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.support.v7.app.AppCompatDelegate.create(AppCompatDelegate.java:183)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.support.v7.app.AppCompatActivity.getDelegate(AppCompatActivity.java:519)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.support.v7.app.AppCompatActivity.onCreate(AppCompatActivity.java:70)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at me.yokeyword.fragmentation.SupportActivity.onCreate(SupportActivity.java:38)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at json.chao.com.wanandroid.base.activity.AbstractSimpleActivity.onCreate(AbstractSimpleActivity.java:29)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at json.chao.com.wanandroid.base.activity.BaseActivity.onCreate(BaseActivity.java:37)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.app.Activity.performCreate(Activity.java:7098)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.app.Activity.performCreate(Activity.java:7089)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1215)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2770)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2895)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.app.ActivityThread.-wrap11(Unknown Source:0)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1616)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.os.Handler.dispatchMessage(Handler.java:106)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.os.Looper.loop(Looper.java:173)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at android.app.ActivityThread.main(ActivityThread.java:6653)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at java.lang.reflect.Method.invoke(Native Method)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:821)
复制代码
能够看出,这里弹出了应用中某一个IPC调用的全部堆栈信息。在这里,具体是在AbstractSimpleActivity的onCreate方法中调用了ServiceManager的getService方法,它是一个IPC调用的方法。这样,应用的IPC调用咱们就能很方便地捕获到了。
你们能够看到,经过这种方式咱们能够很方便地拿到应用中全部的IPC操做,并能够得到到IPC调用的类型、调用耗时、发生次数、调用的堆栈等等一系列信息。固然,除了IPC调用的问题以外,还有IO、DB、View绘制等一系列单点问题须要去创建与之对应的检测方案。
对于卡顿问题检测方案的建设,主要是利用ARTHook去完善线下的检测工具,尽量地去Hook相对应的操做,以暴露、分析问题。这样,才能更好地实现卡顿的体系化解决方案。
界面的打开速度对用户体验来讲是相当重要的,那么如何实现界面秒开呢?
其实界面秒开就是一个小的启动优化,其优化的思想能够借鉴启动速度优化与布局优化的一些实现思路。
首先,咱们能够经过Systrace来观察CPU的运行情况,好比有没有跑满CPU;而后,咱们在启动优化中学习到的优雅异步以及优雅延迟初始化等等一些方案;其次,针对于咱们的界面布局,咱们可使用异步Inflate、X2C、其它的绘制优化措施等等;最后,咱们可使用预加载的方式去提早获取页面的数据,以免网络或磁盘IO速度的影响,或者也能够将获取数据的方法放到onCreate方法的第一行。
一般,咱们是经过界面秒开率去统计页面的打开速度的,具体就是计算onCreate到onWindowFocusChanged的时间。固然,在某些特定的场景下,把onWindowFocusChanged做为页面打开的结束点并非特别的精确,那咱们能够去实现一个特定的接口来适配咱们的Activity或Fragment,咱们能够把那个接口方法做为页面打开的结束点。
那么,除了以上说到的一些界面秒开的实现方式以外,尚未更好的方式呢?
那就是Lancet。
Lancet是一个轻量级的Android AOP框架,它具备以下优点:
而后,我来简单地讲解下Lancet的用法。Lancet自身提供了一些注解用于Hook,以下所示:
接下来,咱们就是使用Lancet来进行一下实战演练。
首先,咱们须要在项目根目录的 build.gradle 添加以下依赖:
dependencies{
classpath 'me.ele:lancet-plugin:1.0.5'
}
复制代码
而后,在 app 目录的'build.gradle' 添加:
apply plugin: 'me.ele.lancet'
dependencies {
compileOnly 'me.ele:lancet-base:1.0.5'
}
复制代码
接下来,咱们就可使用Lancet了,这里咱们须要先新建一个类去进行专门的Hook操做,以下所示:
public class ActivityHooker {
@Proxy("i")
@TargetClass("android.util.Log")
public static int i(String tag, String msg) {
msg = msg + "JsonChao";
return (int) Origin.call();
}
}
复制代码
上述的方法就是对android.util.Log的i方法进行Hook,并在全部的msg后面加上"JsonChao"字符串,注意这里的i方法咱们须要从android.util.Log里面将它的i方法复制过来,确保方法名和对应的参数信息一致;而后,方法上面的@TargetClass与@Proxy分别是指定对应的全路径类名与方法名;最后,咱们须要经过Lancet提供的Origin类去调用它的call方法来实现返回原来的调用信息。完成以后,咱们从新运行项目,会出现以下log信息:
2020-01-23 13:13:34.124 7277-7277/json.chao.com.wanandroid I/MultiDex: VM with version 2.1.0 has multidex supportJsonChao
2020-01-23 13:13:34.124 7277-7277/json.chao.com.wanandroid I/MultiDex: Installing applicationJsonChao
复制代码
能够看到,log后面都加上了咱们预先添加的字符串,说明Hook成功了。下面,咱们就能够用Lancet来统计一下项目界面的秒开率了,代码以下所示:
public static ActivityRecord sActivityRecord;
static {
sActivityRecord = new ActivityRecord();
}
@Insert(value = "onCreate",mayCreateSuper = true)
@TargetClass(value = "android.support.v7.app.AppCompatActivity",scope = Scope.ALL)
protected void onCreate(Bundle savedInstanceState) {
sActivityRecord.mOnCreateTime = System.currentTimeMillis();
// 调用当前Hook类方法中原先的逻辑
Origin.callVoid();
}
@Insert(value = "onWindowFocusChanged",mayCreateSuper = true)
@TargetClass(value = "android.support.v7.app.AppCompatActivity",scope = Scope.ALL)
public void onWindowFocusChanged(boolean hasFocus) {
sActivityRecord.mOnWindowsFocusChangedTime = System.currentTimeMillis();
LogHelper.i(getClass().getCanonicalName() + " onWindowFocusChanged cost "+(sActivityRecord.mOnWindowsFocusChangedTime - sActivityRecord.mOnCreateTime));
Origin.callVoid();
}
复制代码
上面,咱们经过@TargetClass和@Insert两个注解实现Hook了android.support.v7.app.AppCompatActivity的onCreate与onWindowFocusChanged方法。咱们注意到,这里@Insert注解能够指定两个参数,其源码以下所示:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Insert {
String value();
boolean mayCreateSuper() default false;
}
复制代码
第二个参数mayCreateSuper设定为true则代表若是没有重写父类的方法,则会默认去重写这个方法。对应到咱们ActivityHooker里面实现的@Insert注解方法就是若是当前的Activity没有重写父类的onCreate和 onWindowFocusChanged方法,则此时默认会去重写父类的这个方法,以避免因某些Activity不存在该方法而Hook失败的状况。
而后,咱们注意到@TargetClass也能够指定两个参数,其源码以下所示:
@Retention(RetentionPolicy.RUNTIME)
@java.lang.annotation.Target({ElementType.TYPE, ElementType.METHOD})
public @interface TargetClass {
String value();
Scope scope() default Scope.SELF;
}
复制代码
第二个参数scope指定的值是一个枚举,可选的值以下所示:
public enum Scope {
SELF,
DIRECT,
ALL,
LEAF
}
复制代码
对于Scope.SELF,它表明仅匹配目标value所指定的一个匹配类;对于DIRECT,它表明匹配value所指定的类的一个直接子类;若是是Scope.ALL,它就代表会去匹配value所指定的类的全部子类,而咱们上面指定的value值为android.support.v7.app.AppCompatActivity,由于scope指定为了Scope.ALL,则说明会去匹配AppCompatActivity的全部子类。而最后的Scope.LEAF 表明匹配 value 指定类的最终子类,由于java是单继承,因此继承关系是树形结构,因此这里表明了指定类为顶点的继承树的全部叶子节点。
最后,咱们设定了一个ActivityRecord类去记录onCreate与onWindowFocusChanged的时间戳,以下所示:
public class ActivityRecord {
/**
* 避免没有仅执行onResume就去统计界面打开速度的状况,如息屏、亮屏等等
*/
public boolean isNewCreate;
public long mOnCreateTime;
public long mOnWindowsFocusChangedTime;
}
复制代码
经过sActivityRecord.mOnWindowsFocusChangedTime - sActivityRecord.mOnCreateTime获得的时间即为界面的打开速度,最后,从新运行项目,会获得以下log信息:
2020-01-23 14:12:16.406 15098-15098/json.chao.com.wanandroid I/WanAndroid-LOG: │ [null | 57 | json_chao_com_wanandroid_aop_ActivityHooker_onWindowFocusChanged] json.chao.com.wanandroid.ui.main.activity.SplashActivity onWindowFocusChanged cost 257
2020-01-23 14:12:18.930 15098-15098/json.chao.com.wanandroid I/WanAndroid-LOG: │ [null | 57 | json_chao_com_wanandroid_aop_ActivityHooker_onWindowFocusChanged] json.chao.com.wanandroid.ui.main.activity.MainActivity onWindowFocusChanged cost 608
复制代码
从上面的log信息,咱们就能够知道 SplashActivity 和 MainActivity 的界面打开速度分别是257ms和608ms。
最后,咱们来看下界面秒开的监控纬度。
对于界面秒开的监控纬度,主要分为如下三个方面:
首先,咱们会监控界面打开的总体耗时,也就是onCreate到onWindowFocusChanged这个方法的耗时;固然,若是咱们是在一个特殊的界面,咱们须要更精确的知道界面打开的一个时间,这个咱们能够用自定义的接口去实现。其次,咱们也须要去监控生命周期的一个耗时,如onCreate、onStart、onResume等等。最后,咱们也须要去作生命周期间隔的耗时监控,这点常常被咱们所忽略,好比onCreate的结束到onStart开始的这一段时间,也是有时间损耗的,咱们能够监控它是否是在一个合理的范围以内。经过这三个方面的监控纬度,咱们就可以很是细粒度地去检测页面秒开各个方面的状况。
尽管咱们在应用中监控了不少的耗时区间,可是仍是有一些耗时区间咱们尚未捕捉到,如onResume到列表展现的间隔时间,这些时间在咱们的统计过程当中很容易被忽视,这里咱们举一个小栗子:
咱们在Activity的生命周期中post了一个message,那这个message极可能其中
执行了一段耗时操做,那你知道这个message它的具体执行时间吗?这个message其实
颇有可能在列表展现以前就执行了,若是这个message耗时1s,那么列表的展现
时间就会延迟1s,若是是200ms,那么咱们设定的自动化卡顿检测就没法
发现它,那么列表的展现时间就会延迟200ms。
复制代码
其实这种场景很是常见,接下来,咱们就在项目中来进行实战演练。
首先,咱们在MainActivity的onCreate中加上post消息的一段代码,其中模拟了延迟1000ms的耗时操做,代码以下所示:
// 如下代码是为了演示Msg致使的主线程卡顿
new Handler().post(() -> {
LogHelper.i("Msg 执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
复制代码
接着,咱们在RecyclerView对应的Adapter中将列表展现的时间打印出来,以下所示:
if (helper.getLayoutPosition() == 1 && !mHasRecorded) {
mHasRecorded = true;
helper.getView(R.id.item_search_pager_group).getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
helper.getView(R.id.item_search_pager_group).getViewTreeObserver().removeOnPreDrawListener(this);
LogHelper.i("FeedShow");
return true;
}
});
}
复制代码
最后,咱们从新运行下项目,看看二者的执行时间,log信息以下:
2020-01-23 15:21:55.076 19091-19091/json.chao.com.wanandroid I/WanAndroid-LOG: │ [MainActivity.java | 108 | lambda$initEventAndData$1$MainActivity] Msg 执行
2020-01-23 15:21:56.264 19091-19091/json.chao.com.wanandroid I/WanAndroid-LOG: │ [null | 57 | json_chao_com_wanandroid_aop_ActivityHooker_onWindowFocusChanged] json.chao.com.wanandroid.ui.main.activity.MainActivity onWindowFocusChanged cost 1585
2020-01-23 15:21:57.207 19091-19091/json.chao.com.wanandroid I/WanAndroid-LOG: │ ArticleListAdapter$1.onPreDraw (ArticleListAdapter.java:93)
2020-01-23 15:21:57.208 19091-19091/json.chao.com.wanandroid I/WanAndroid-LOG: │ LogHelper.i (LogHelper.java:37)
2020-01-23 15:21:57.208 19091-19091/json.chao.com.wanandroid I/WanAndroid-LOG: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2020-01-23 15:21:57.208 19091-19091/json.chao.com.wanandroid I/WanAndroid-LOG: │ [ArticleListAdapter.java | 93 | onPreDraw] FeedShow
复制代码
从log信息中能够看到,MAinActivity的onWindowFocusChanged方法延迟了1000ms才被调用,与此同时,列表页时延迟了1000ms才展现出来。也就是说,post的这个message消息是执行在界面、列表展现以前的。由于任何一个开发都有可能在某一个生命周期或者是某一个阶段以及一些第三方的SDK里面,回去作一些handler post的相关操做,这样,他的handler post的message的执行,颇有可能在咱们的界面或列表展现以前就被执行,因此说,出现这种耗时的盲区是很是广泛的,并且也很差排查,下面,咱们分析下耗时盲区存在的难点。
首先,咱们能够经过细化监控的方式去获取耗时的一些盲区,可是咱们殊不知道在这个盲区中它执行了什么操做。其次,对于线上的一些耗时盲区,咱们是没法进行排查的。
这里,咱们先来看看如何创建耗时盲区监控的线下方案。
这里咱们直接使用TraceView去检测便可,由于它可以清晰地记录线程在具体的时间内到底作了什么操做,特别适合一段时间内的盲区监控。
而后,咱们来看下如何创建耗时盲区监控的线上方案。
咱们知道主线程的全部方法都是经过message来执行的,还记得在以前咱们学习了一个库:AndroidPerformanceMonitor,咱们是否能够经过这个mLogging来作盲区检测呢?经过这个mLogging确实能够知道咱们主线程发生的message,可是经过mLogging没法获取具体的调用栈信息,由于它所获取的调用栈信息都是系统回调回来的,它并不知道当前的message是被谁抛出来的,因此说,这个方案并不够完美。
那么,咱们是否能够经过AOP的方式去切Handler方法呢?好比sendMessage、sendMessageDeleayd方法等等,这样咱们就能够知道发生message的一个堆栈,可是这种方案也存在着一个问题,就是它不清楚准确的执行时间,咱们切了这个handler的方法,仅仅只知道它具体是在哪一个地方被发的和它所对应的堆栈信息,可是没法获取准确的执行时间。若是咱们想知道在onResume到列表展现之间执行了哪些message,那么经过AOP的方式也没法实现。
那么,最终的耗时盲区监控的一个线上方案就是使用一个统一的Handler,定制了它的两个方法,一个是sendMessageAtTime,另一个是dispatchMessage方法。由于对于发送message,无论调用哪一个方法最终都会调用到一个是sendMessageAtTime这个方法,而处理message呢,它最终会调用dispatchMessage方法。而后,咱们须要定制一个gradle插件,来实现自动化的接入咱们定制好的handler,经过这种方式,咱们就能在编译期间去动态地替换全部使用Handler的父类为咱们定制好的这个handler。这样,在整个项目中,全部的sendMessage和handleMessage都会通过咱们的回调方法。接下来,咱们来进行一下实战演练。
首先,我这里给出定制好的全局Handler类,以下所示:
public class GlobalHandler extends Handler {
private long mStartTime = System.currentTimeMillis();
public GlobalHandler() {
super(Looper.myLooper(), null);
}
public GlobalHandler(Callback callback) {
super(Looper.myLooper(), callback);
}
public GlobalHandler(Looper looper, Callback callback) {
super(looper, callback);
}
public GlobalHandler(Looper looper) {
super(looper);
}
@Override
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
boolean send = super.sendMessageAtTime(msg, uptimeMillis);
// 1
if (send) {
GetDetailHandlerHelper.getMsgDetail().put(msg, Log.getStackTraceString(new Throwable()).replace("java.lang.Throwable", ""));
}
return send;
}
@Override
public void dispatchMessage(Message msg) {
mStartTime = System.currentTimeMillis();
super.dispatchMessage(msg);
if (GetDetailHandlerHelper.getMsgDetail().containsKey(msg)
&& Looper.myLooper() == Looper.getMainLooper()) {
JSONObject jsonObject = new JSONObject();
try {
// 2
jsonObject.put("Msg_Cost", System.currentTimeMillis() - mStartTime);
jsonObject.put("MsgTrace", msg.getTarget() + " " + GetDetailHandlerHelper.getMsgDetail().get(msg));
// 3
LogHelper.i("MsgDetail " + jsonObject.toString());
GetDetailHandlerHelper.getMsgDetail().remove(msg);
} catch (Exception e) {
}
}
}
}
复制代码
上面的GlobalHandler将会是咱们项目中全部Handler的一个父类。在注释1处,咱们在sendMessageAtTime这个方法里面判断若是message发送成功,将会把当前message对象对应的调用栈信息都保存到一个ConcurrentHashMap中,GetDetailHandlerHelper类的代码以下所示:
public class GetDetailHandlerHelper {
private static ConcurrentHashMap<Message, String> sMsgDetail = new ConcurrentHashMap<>();
public static ConcurrentHashMap<Message, String> getMsgDetail() {
return sMsgDetail;
}
}
复制代码
这样,咱们就可以知道这个message它是被谁发送过来的。而后,在dispatchMessage方法里面,咱们能够计算拿到其处理消息的一个耗时,并在注释2处将这个耗时保存到一个jsonObject对象中,同时,咱们也能够经过GetDetailHandlerHelper类的ConcurrentHashMap对象拿到这个message对应的堆栈信息,并在注释3处将它们输出到log控制台上。固然,若是是线上监控,则会把这些信息保存到本地,而后选择合适的时间去上传。最后,咱们还能够在方法体里面作一个判断,咱们设置一个阈值,好比阈值为20ms,超过了20ms就把这些保存好的信息上报到APM后台。
在前面的实战演练中,咱们使用了handler post的方式去发送一个消息,经过gradle插件将全部handler的父类替换为咱们定制好的GlobalHandler以后,咱们就能够优雅地去监控应用中的耗时盲区了。
对于实现全局替换handler的gradle插件,除了使用AspectJ实现以外,这里推荐一个已有的项目:DroidAssist。
而后,从新运行项目,关键的log信息以下所示:
MsgDetail {"Msg_Cost":1001,"MsgTrace":"Handler (com.json.chao.com.wanandroid.performance.handler.GlobalHandler) {b0d4d48} \n\tat
com.json.chao.com.wanandroid.performance.handler.GlobalHandler.sendMessageAtTime(GlobalHandler.java:36)\n\tat
json.chao.com.wanandroid.ui.main.activity.MainActivity.initEventAndData$__twin__(MainActivity.java:107)\n\tat"
复制代码
从以上信息咱们不只能够知道message执行的时间,还能够从对应的堆栈信息中获得发送message的位置,这里的位置是MainActivity的107行,也就是new Handler().post()这一行代码。使用这种方式咱们就能够知道在列表展现以前到底执行了哪些自定义的message,咱们一眼就能够知道哪些message实际上是不符合咱们预期的,好比说message的执行时间过长,或者说这个message其实能够延后执行,这个咱们均可以根据实际的项目和业务需求进行相应地修改。
耗时盲区监控是咱们卡顿监控中不可或缺的一个环节,也是卡顿监控全面性的一个重要保障。而须要注意的是,TraceView仅仅适用于线下的一个场景,同时对于TraceView来讲,它能够用于监控咱们系统的message。而最后介绍的动态替换的方式实际上是适合于线上的,同时,它仅仅监控应用自身的一个message。
若是应用出现了卡顿现象,那么能够考虑如下方式进行优化:
而后,咱们来看看卡顿优化的工具建设。
工具建设这块常常容易被你们所忽视,可是它的收益却很是大,也是卡顿优化的一个重点。首先,对于系统工具而言,咱们要有一个认识,同时必定要学会使用它,这里咱们再回顾一下。
而后,咱们介绍了自动化工具建设以及优化方案。咱们介绍了两个工具,AndroidPerformanceMonitor以及ANR-WatchDog。同时针对于AndroidPerformanceMonitor的问题,咱们采用了高频采集,以找出重复率高的堆栈这样一种方式进行优化,在学习的过程当中,咱们不只须要学会怎样去使用工具,更要去理解它们的实现原理以及各自的使用场景。
同时,咱们对于卡顿优化工具的建设也作了细化,对于单点问题,好比说IPC监控,咱们经过Hook的手段来作到尽早的发现问题。对于耗时盲区的监控,咱们在线上采用的是替换Handler的方式来监控全部子线程message执行的耗时以及调用堆栈。
最后,咱们来看一下卡顿监控的指标。咱们会计算应用总体的卡顿率,ANR率、界面秒开率以及交换时间、生命周期时间等等。在上报ANR信息的同时,咱们也须要上报环境和场景信息,这样不只方便咱们在不一样版本之间进行横向对比,同时,也能够结合咱们的报警平台在第一时间感知到异常。
此时,咱们的应用不只应该控制好核心功能的CPU消耗,也须要尽可能减小非核心需求的CPU消耗。
好比List.removeall方法,它内部会遍历一次须要过滤的消息列表,在已经存在循环列表的状况下会形成CPU资源的冗余使用,此时应该去优化相关的算法,避免使用List.removeall这个方法。
这个时候咱们须要使用神器renderscript来图形处理的相关运算,将CPU转换到GPU。关于renderscript的背景知识能够看看笔者以前写的深刻探索Android布局优化(下)。
此时只能关闭文本TextView的硬件加速,以下所示:
textView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
复制代码
当开启了硬件加速进行长中文字体的渲染时,首先会调用ViewRootImpl.draw()方法,最后会调用GLES20Canvas.nDrawDisplayList()方法开始经过JNI调整到Native层。在这个方法里,会继续调用OpenGLRenderer.drawDisplayList()方法,它经过调用DisplayList的replay方法,以回放前面录制的DisplayList执行绘制操做。
DisplayList的replay方法会遍历DisplayList中保存的每个操做。其中渲染字体的操做名是DrawText,当遍历到一个DrawText操做时,会调用OpenGLRender::drawText方法区渲染字体。最终,会在OpenGLRender::drawText方法里去调用Font::render()方法渲染字体,而在这个方法中有一个很关键的操做,即获取字体缓存。咱们都知道每个中文的编码都是不一样的,所以中文的缓存效果很是不理想,可是对于英文而言,只须要缓存26个字母就能够了。在Android 4.1.2版本以前对文本的Buffer设置太小,因此状况比较严重,若是你的应用在其它版本的渲染性能尚可,就能够仅仅把Android 4.0.x的硬件加速关闭,代码以下所示:
// AndroidManifest中
<Applicaiton
...
android:hardwareAccelerated="@bool/hardware_acceleration">
// value-v1四、value-v15中设置相应的Bool
值便可
<bool name="hardware_acceleration">false</bool>
复制代码
此外,硬件渲染还有一些其它的问题在使用时须要注意,具体为以下所示:
从项目的初期到壮大期,最后再到成熟期,每个阶段都针对卡顿优化作了不一样的处理。各个阶段所作的事情以下所示:
我作卡顿优化也是经历了一些阶段,最初咱们的项目当中的一些模块出现了卡顿以后,我是经过系统工具进行了定位,我使用了Systrace,而后看了卡顿周期内的CPU情况,同时结合代码,对这个模块进行了重构,将部分代码进行了异步和延迟,在项目初期就是这样解决了问题。
可是呢,随着咱们项目的扩大,线下卡顿的问题也愈来愈多,同时,在线上,也有卡顿的反馈,可是线上的反馈卡顿,咱们在线下难以复现,因而咱们开始寻找自动化的卡顿监测方案,其思路是来自于Android的消息处理机制,主线程执行任何代码都会回到Looper.loop方法当中,而这个方法中有一个mLogging对象,它会在每一个message的执行先后都会被调用,咱们就是利用这个先后处理的时机来作到的自动化监测方案的。同时,在这个阶段,咱们也完善了线上ANR的上报,咱们采起的方式就是监控ANR的信息,同时结合了ANR-WatchDog,做为高版本没有文件权限的一个补充方案。
在作完这个卡顿检测方案以后呢,咱们还作了线上监控及线下检测工具的建设,最终实现了一整套完善,多维度的解决方案。
咱们的思路是来自于Android的消息处理机制,主线程执行任何代码它都会走到Looper.loop方法当中,而这个函数当中有一个mLogging对象,它会在每一个message处理先后都会被调用,而主线程发生了卡顿,那就必定会在dispatchMessage方法中执行了耗时的代码,那咱们在这个message执行以前呢,咱们能够在子线程当中去postDelayed一个任务,这个Delayed的时间就是咱们设定的阈值,若是主线程的messaege在这个阈值以内完成了,那就取消掉这个子线程当中的任务,若是主线程的message在阈值以内没有被完成,那子线程当中的任务就会被执行,它会获取到当前主线程执行的一个堆栈,那咱们就能够知道哪里发生了卡顿。
通过实践,咱们发现这种方案获取的堆栈信息它不必定是准确的,由于获取到的堆栈信息它极可能是主线程最终执行的一个位置,而真正耗时的地方其实已经执行完成了,因而呢,咱们就对这个方案作了一些优化,咱们采起了高频采集的方案,也就是在一个周期内咱们会屡次采集主线程的堆栈信息,若是发生了卡顿,那咱们就将这些卡顿信息压缩以后上报给APM后台,而后找出重复的堆栈信息,这些重复发生的堆栈大几率就是卡顿发生的一个位置,这样就提升了获取卡顿信息的一个准确性。
首先,针对卡顿,咱们采用了线上、线下工具相结合的方式,线下工具咱们须要尽量早地去暴露问题,而针对于线上工具呢,咱们侧重于监控的全面性、自动化以及异常感知的灵敏度。
同时呢,卡顿问题还有不少的难题。好比说有的代码呢,它不到你卡顿的一个阈值,可是执行过多,或者它错误地执行了不少次,它也会致使用户感官上的一个卡顿,因此咱们在线下经过AOP的方式对常见的耗时代码进行了Hook,而后对一段时间内获取到的数据进行分析,咱们就能够知道这些耗时的代码发生的时机和次数以及耗时状况。而后,看它是否是知足咱们的一个预期,不知足预期的话,咱们就能够直接到线下进行修改。同时,卡顿监控它还有不少容易被忽略的一个盲区,好比说生命周期的一个间隔,那对于这种特定的问题呢,咱们就采用了编译时注解的方式修改了项目当中全部Handler的父类,对于其中的两个方法进行了监控,咱们就能够知道主线程message的执行时间以及它们的调用堆栈。
对于线上卡顿,咱们除了计算App的卡顿率、ANR率等常规指标以外呢,咱们还计算了页面的秒开率、生命周期的执行时间等等。并且,在卡顿发生的时刻,咱们也尽量多地保存下来了当前的一个场景信息,这为咱们以后解决或者复现这个卡顿留下了依据。
恭喜你,若是你看到了这里,你会发现要作好应用的卡顿优化的确不是一件简单的事,它须要你有成体系的知识构建基底。最后,咱们再来回顾一下面对卡顿优化,咱们已经探索的如下九大主题:
相信看到这里,你必定收获满满,可是要记住,方案再好,也只有本身动手去实践,才能真正地掌握它。只有重视实践,充分运用感性认知潜能,在项目中磨炼本身,才是正确的学习之道。在实践中,在某些关键动做上刻意练习,也会取得事半功倍的效果。
一、国内Top团队大牛带你玩转Android性能分析与优化 第6章 卡顿优化
三、《Android移动性能实战》第四章 CPU
四、《Android移动性能实战》第七章 流畅度
五、Android dumpsys cpuinfo 信息解读
七、nanoscope-An extremely accurate Android method tracing tool
九、lancet-A lightweight and fast AOP framework for Android App and SDK developers
十、MethodTraceMan-用于快速找到高耗时方法,定位解决Android App卡顿问题
十二、使用 ftrace
1三、profilo-A library for performance traces from production
1四、ftrace 简介
1五、atrace源码
1六、AndroidAdvanceWithGeektime / Chapter06
1七、AndroidAdvanceWithGeektime / Chapter06-plus
若是这个库对您有很大帮助,您愿意支持这个项目的进一步开发和这个项目的持续维护。你能够扫描下面的二维码,让我喝一杯咖啡或啤酒。很是感谢您的捐赠。谢谢!
欢迎关注个人微信:
bcce5360
微信群若是不能扫码加入,麻烦你们想进微信群的朋友们,加我微信拉你进群。
2千人QQ群,Awesome-Android学习交流群,QQ群号:959936182, 欢迎你们加入~