桔妹导读:Activity 的启动速度是很多开发者关心的问题,当页面跳转耗时过长时,App 就会给人一种非常笨重的感觉。在遇到某个页面启动过慢的时候,开发的第一直觉一般是 onCreate 执行速度太慢了,然后在 onCreate 方法前后记录下时间戳计算出耗时。不过有时候即使把 onCreate 方法的耗时优化了,效果仍旧不明显。实际上影响到 Activity 启动速度的原因是多方面的,需要从 Activity 的启动流程入手,才能找到真正问题所在。
1. Activity启动流程
ActiivtyA Pause流程
ActivityB Launch流程
ActivityB Render流程
2. 耗时统计方案
系统耗时统计
三种耗时
Pause耗时
Launch耗时
Render耗时
应用内统计方案
Hook Instrumentation
Hook Looper-Printer
Hook ActivityThread$H
3. 总结
4. 参考
如果要给 Activity 的「启动」做一个定义的话,个人觉得应该是:从调用 startActivity到Activity可被操作为止,代表启动成功。所谓的可被操作,是指可接受各种输入事件,比如手势、键盘输入之类的。换个角度来说,也可以看成是主线程处于空闲状态,能执行后续进入的各种Message。
Activity的启动可以分为三个步骤,以ActivityA启动ActivityB为例,三步骤分别为:
以ActivityA调用startActivity,到ActivityA成功pause为止
ActivityB成功初始化,到执行完resume为止
ActivityB向WSM注册窗口,到第一帧绘制完成为止
Activity启动涉及到App进程与ActivityManagerService(AMS)、WindowManagerService(WMS)的通信,网上关于这个流程的文章很多,这边就不再具体描述了,只列一下关键方法的调用链路。
当ActivityA使用startActivity方法启动ActivityB时,执行函数链路如下:
ActivityA.startActivity->
Instrumentation.execStartActivity->
ActivityManagerNative.getDefault.startActivity->
ActivityManagerService.startActivityAsUser->
ActivityStarter.startActivityMayWait->
ActivityStarter.startActivityLocked->
ActivityStarter.startActivityUnchecked->
ActivityStackSupervisor.resumeFocusedStackTopActivityLocked->
ActivityStack.resumeTopActivityUncheckedLocked->
ActivityStack.resumeTopActivityInnerLocked->
ActivityStack.startPausingLocked->
ActivityThread$$ApplicationThread.schedulePauseActivity->
ActivityThread.handlePauseActivity->
└ActivityA.onPauseActivityManagerNative.getDefault().activityPaused
当 App 请求 AMS 要启动一个新页面的时候,AMS 首先会 pause 掉当前正在显示的 Activity,当然,这个 Activity 可能与请求要开启的 Activity 不在一个进程,比如点击桌面图标启动 App ,当前要暂停的 Activity 就是桌面程序 Launcher。在 onPause 内执行耗时操作是一种很不推荐的做法,从上述调用链路可以看出,如果在 onPause 内执行了耗时操作,会直接影响到 ActivityManagerNative.getDefault().activityPaused() 方法的执行,而这个方法的作用就是通知 AMS,“当前 Activity 已经已经成功暂停,可以启动新 Activity 了”。
在AMS接收到App进程对于activityPaused方法的调用后,执行函数链路如下
ActivityManagerService.activityPaused->
ActivityStack.activityPausedLocked->
ActivityStack.completePauseLocked->
ActivityStackSupervisor.resumeFocusedStackTopActivityLocked->
ActivityStackSupervisor.resumeFocusedStackTopActivityLocked->
ActivityStack.resumeTopActivityUncheckedLocked->
ActivityStack.resumeTopActivityInnerLocked->
ActivityStackSupervisor.startSpecificActivityLocked->
└1.启动新进程:ActivityManagerService.startProcessLocked 暂不展开
└2.当前进程:ActivityStackSupervisor.realStartActivityLocked->
ActivityThread$$ApplicationThread.scheduleLaunchActivity->
Activity.handleLaunchActivity->
└Activity.onCreate
└Activity.onRestoreInstanceState
└handleResumeActivity
└Activity.onStart->
└Activity.onResume->
└WindowManager.addView->
AMS 在经过一系列方法调用后,通知 App 进程正式启动一个 Actviity,注意如果要启动 Activity 所在进程不存在,比如点击桌面图标第一次打开应用,或者 App 本身就是多进程的,要启动的新页面处于另外一个进程,那就需要走到 ActivityManagerService.startProcessLocked 流程,等新进程启动完毕后再通知 AMS,这里不展开。按照正常流程,会依次走过 Activity 生命周期内的 onCreate、onRestoreInstanceState、onStart、onResume 方法,
这一步的耗时基本也可以看成就是这四个方法的耗时,由于这四个方法是同步调用的,所以可以通过以 onCreate 方法为起点, onResume 方法为终点,统计出这一步骤的总耗时。
▍ActivityB Render流程
在ActivityB执行完onResume方法后,就可以显示该Activity了,调用流程如下:
WindowManager.addView->
WindowManagerImpl.addView->
ViewRootImpl.setView->
ViewRootImpl.requestLayout->
└ViewRootImpl.scheduleTraversals->
└Choreographer.postCallback->
WindowManagerSerivce.add
这一步的核心实际上是Choreographer.postCallback,向Choreographer注册了一个回调,当Vsync事件到来时,就会执行下面的回调进行ui的渲染。
ViewRootImpl.doTraversal->
ViewRootImpl.performTraversals->
└ViewRootImpl.relayoutWindow
└ViewRootImpl.performMeasure
└ViewRootImpl.performLayout
└ViewRootImpl.performDraw
ViewRootImpl.reportDrawFinished
这里分别执行了 performMeasure、performLayout、performDraw,实际上就是对应到DecorView 的测量、布局、绘制三个流程。由于 Android 的UI是个树状结构,作为根View的DecorView的测量、布局、绘制,会调用到所有子View相应的方法,因此,这一步的总耗时就是所有子View在测量、布局、绘制中的耗时之和,如果某个子View在这三个方法中如果进行了耗时操作,就会拖慢整个UI的渲染,进而影响Activity第一帧的渲染速度。
知道了Actviity启动流程的三个步骤和对应的方法耗时统计方法,那该如何设计一个统计方案呢?在这之前,可以先看看系统提供的耗时统计方法。
▍系统耗时统计
打开 Android Studio的Logcat ,输入过滤关键字 ActivityManager, 在启动一个 Actviity 后就能看到如下日志:
末尾的+59ms便是启动该Activity的耗时。这个日志是Android系统在AMS端直接输出的,《WMS常见问题一(Activity displayed延迟)》这篇文章分析了系统耗时统计的方法,简单来说,上述日志是通过ActivityRecord.reportLaunchTimeLocked方法打印出来的。
ActivityRecord.java
private void reportLaunchTimeLocked(final long curTime) {
......
final long thisTime = curTime - displayStartTime;
final long totalTime = stack.mLaunchStartTime != 0
? (curTime - stack.mLaunchStartTime) : thisTime;
if (SHOW_ACTIVITY_START_TIME) {
Trace.asyncTraceEnd(TRACE_TAG_ACTIVITY_MANAGER, "launching: " + packageName, 0);
EventLog.writeEvent(AM_ACTIVITY_LAUNCH_TIME,
userId, System.identityHashCode(this), shortComponentName,
thisTime, totalTime);
StringBuilder sb = service.mStringBuilder;
sb.setLength(0);
sb.append("Displayed ");
sb.append(shortComponentName);
sb.append(": ");
TimeUtils.formatDuration(thisTime, sb);
if (thisTime != totalTime) {
sb.append(" (total ");
TimeUtils.formatDuration(totalTime, sb);
sb.append(")");
}
Log.i(TAG, sb.toString());
}
......
}
其中 displayStartTime 是在 ActivityStack.setLaunchTime() 方法中设置的,具体调用链路:
ActivityStackSupervisor.startSpecificActivityLocked->
└ActivityStack.setLaunchTime
ActivityStackSupervisor.realStartActivityLocked->
ActivityThread$$ApplicationThread.scheduleLaunchActivity->
Activity.handleLaunchActivity->
ActivityThread$$ApplicationThread.scheduleLaunchActivity->
Activity.handleLaunchActivity->
在 ActivityStackSupervisor.startSpecificActivityLocked 方法中调用了 ActivityStack.setLaunchTime(),而 startSpecificActivityLocked 方法最终会走到 App 端的 Activity.onCreate 方法,所以统计开始的时间实际上就是 App 启动中的第二步开始的时间。
而 ActivityRecord.reportLaunchTimeLocked 方法自身的调用链如下:
ViewRootImpl.reportDrawFinished->
Session.finishDrawing->
WindowManagerService.finishDrawingWindow->
WindowSurfacePlacer.requestTraversal->
WindowSurfacePlacer.performSurfacePlacement->
WindowSurfacePlacer.performSurfacePlacementLoop->
RootWindowContainer.performSurfacePlacement->
WindowSurfacePlacer.handleAppTransitionReadyLocked->
WindowSurfacePlacer.handleOpeningApps->
AppWindowToken.updateReportedVisibilityLocked->
AppWindowContainerController.reportWindowsDrawn->
ActivityRecord.onWindowsDrawn->
ActivityRecord.reportLaunchTimeLocked
在启动流程第三步UI渲染完成后,App 会通知 WMS,紧接着 WMS 执行一系列和切换动画相关的方法后,调用到 ActivityRecord.reportLaunchTimeLocked,最终打印出启动耗时。
由上述流程可以看到,系统统计并没有把 ActivityA的pause 操作耗时计入 Activity 启动耗时中。不过,如果我们在 ActivityA 的 onPause 中做一个 Thread.sleep(2000) 操作,会很神奇地看到系统打印的耗时也跟着变了。
这次启动耗时变成了 1.571s,明显是把 onPause 的时间算进去了,但是却小于 onPause 内休眠的2秒。其实,这是由于 AMS 对于 pause 操作的超时处理导致的,在 ActivityStack.startPausingLocked 方法中,会执行到 schedulePauseTimeout 方法。
ActivityThread.java
private static final int PAUSE_TIMEOUT = 500;
private void schedulePauseTimeout(ActivityRecord r) {
final Message msg = mHandler.obtainMessage(PAUSE_TIMEOUT_MSG);
msg.obj = r;
r.pauseTime = SystemClock.uptimeMillis();
mHandler.sendMessageDelayed(msg, PAUSE_TIMEOUT);
if (DEBUG_PAUSE) Slog.v(TAG_PAUSE, "Waiting for pause to complete...");
}
...
private class ActivityStackHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case PAUSE_TIMEOUT_MSG: {
ActivityRecord r = (ActivityRecord)msg.obj;
// We don't at this point know if the activity is fullscreen,
// so we need to be conservative and assume it isn't.
Slog.w(TAG, "Activity pause timeout for " + r);
synchronized (mService) {
if (r.app != null) {
mService.logAppTooSlow(r.app, r.pauseTime, "pausing " + r);
}
activityPausedLocked(r.appToken, true);
}
} break;
这个方法的作用在于,如果过了 500ms,上一个要暂停 Activity 的进程还没有回调 activityPausedLocked 方法,AMS 就会自己调用 activityPausedLocked 方法,继续之后的 Launch 流程。所以过了 500ms 之后,AMS 就会通知 App 进程启动 ActivityB 的操作,然而此时 App进程仍旧被 onPause 的 Thread.sleep 阻塞着,所以只能再等待 1.5s 才能继续操作,因此打印出来的时间是 2s-0.5s+ 正常的耗时。
▍三种耗时
说完了系统的统计方案,接下去介绍下应用内的统计方案。根据前面的介绍,若想自己实现 Activity 的启动耗时统计功能,只需要以 startActivity 执行为起始点,以第一帧渲染为结束点,就能得出一个较为准确的耗时。不过,这种统计方式无法帮助我们定位具体的问题,当遇到一个页面启动较慢时,我们可能需要知道它具体慢在哪里。而且,由于启动过程中涉及到大量的系统进程耗时和 App 端 Framework 层的方法耗时,这块耗时又是难以对其进行干涉的,所以接下去会把统计的重点放在通过编码能影响到的耗时上,按照启动流程的三个步骤,划分为三种耗时。
Pause耗时
尽管启动 Activity 的起点是 startActivity 方法,但是从调用这个方法开始,到 onPause 被执行到为止,其实都是 App 端 Framework 层与 AMS 之间的交互,所以这里把第一阶段 Pause 的耗时统计放在 onPause 方法开始时候。这一块的统计也很简单,只需要计算一下 onPause 方法的耗时就足够了。
有些同学可能会疑惑:是否 onStop 也要计入 Pause 耗时。并不需要,onStop 操作其实是在主线程空余时才会执行的,在 Activity.handleResumeActivity 方法中,会执行Looper.myQueue().addIdleHandler(new Idler()) 方法,Idler定义如下:
ActivityThread.java
private class Idler implements MessageQueue.IdleHandler {
@Override
public final boolean queueIdle() {
......
am.activityIdle(a.token, a.createdConfig,
......
return false;
}
}
addIdleHandler 表示会放入一个低优先级的任务,只有在线程空闲的时候才去执行,而 am.activityIdle 方法会通知 AMS 找到处于 stop 状态的 Activity,通过 Binder 回调 ActivityThread.scheduleStopActivity,最终执行到 onStop。而这个时候,UI 第一帧已经渲染完毕。
Launch耗时
Launch 耗时可以通过 onCreate、onRestoreInstanceState、onStart、onResume 四个函数的耗时相加得出。在这四个方法中,onCreate 一般是最重的那个方法,因为很多变量的初始化都会放在这里进行。
另外,onCreate 方法中还有个耗时大户是 LayoutInfalter.infalte 方法,调用 setContentView 会执行到这个方法,对于一些复杂布局的第一次解析,会消耗大量时间。由于这四个方法是同步顺序执行的,单独把某些操作从 onCreate 移到 onResume 之类的并没有什么意义,Launch 耗时只关心这几个方法的总耗时。
Render耗时
从 onResume 执行完成到第一帧渲染完成所花费的时间就是 Render 耗时。Render 耗时可以用三种方式计算出来。
◎ 第一种方法-IdleHandler:
Activity.java
@Override
protected void onResume() {
super.onResume();
final long start = System.currentTimeMillis();
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
Log.d(TAG, "onRender cost:" + (System.currentTimeMillis() - start));
return false;
}
});
}
前面说过IdleHandler只会在线程处于空闲的时候被执行。
◎ 第二种方法-DecorView 的两次 post:
Activity.java
@Override
protected void onResume() {
super.onResume();
final long start = System.currentTimeMillis();
getWindow().getDecorView().post(new Runnable() {
@Override
public void run() {
new Hanlder().post(new Runnable() {
@Override
public void run() {
Log.d(TAG, "onPause cost:" + (System.currentTimeMillis() - start));
}
});
}
});
}
View.java
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
......
// Transfer all pending runnables.
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
......
}
ViewRootImpl.java
private void performTraversals() {
......
// host即DecorView
host.dispatchAttachedToWindow(mAttachInfo, 0);
.......
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
.......
performLayout(lp, mWidth, mHeight);
.......
performDraw();
.......
}
通过 getWindow().getDecorView() 获取到 DecorView 后,调用 post 方法,此时由于 DecorView 的 attachInfo 为空,会将这个 Runnable 放置 runQueue 中。runQueue 内的任务会在 ViewRootImpl.performTraversals 的开始阶段被依次取出执行,我们知道这个方法内会执行到 DecorView 的测量、布局、绘制操作,不过 runQueue 的执行顺序会在这之前,所以需要再进行一次 post 操作。第二次的 post 操作可以继续用DecorView().post 或者其普通 Handler.post() ,并无影响。此时 mAttachInfo 已不为空,DecorView().post 也是调用了 mHandler.post()。
◎ 第三种方法-new Handler 的两次 post:
Activity.java
@Override
protected void onResume() {
super.onResume();
final long start = System.currentTimeMillis();
new Handler.post(new Runnable() {
@Override
public void run() {
getWindow().getDecorView().post(new Runnable() {
@Override
public void run() {
Log.d(TAG, "onPause cost:" + (System.currentTimeMillis() - start));
}
});
}
});
}
乍看一下第三种方法和第二种方法区别不大,实际上原理大不相同。这是因为 ViewRootImpl.scheduleTraversals 方法会往主线程队列插入一个屏障消息,代码如下所示:
ViewRootImpl.java
void scheduleTraversals() {
......
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
......
}
}
屏障消息的作用在于阻塞在它之后的同步消息的执行,当我们在 onResume 方法中执行第一次 new Handler().post 方法,向主线程消息队列放入一条消息时,从前面的内容可以知道 onResume 是在 ViewRootImpl.scheduleTraversals 方法之前执行的,所以这条消息会在屏障消息之前,能被正常执行;而第二次 post 的消息就在屏障消息之后了,必须等待屏障消息被移除掉才能执行。屏障消息的移除操作在 ViewRootImpl.doTraversal 方法。
ViewRootImpl.java void doTraversal() {
.......
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
.......
performTraversals();
.......
}
}
在这之后就将执行performTraversals方法,所以移除屏障消息后,等待performTraversals执行完毕,就能正常执行第二次post操作了。在这个地方,其实有个小技巧可以只进行一次post操作,就是在第一次post的时候进行一次小的延迟:
Activity.java
@Override
protected void onResume() {
super.onResume();
final long start = System.currentTimeMillis();
new Handler.postDelay(new Runnable() {
@Override
public void run() {
Log.d(TAG, "onPause cost:" + (System.currentTimeMillis() - start));
}
},10);
}
通过添加一点小延迟,可以把消息的执行时间延迟到屏障消息之后,这条消息就会被屏障消息阻塞,直到屏障消息被移除时才执行了。不过由于系统函数执行时间不可控,这种方式并不保险。
另外,正是由于这条屏障消息的存在,在第一帧渲染完成以前,用户的操作都会被阻塞。
耗时统计是非常适合使用 AOP 思想来实现的功能。我们当然不希望在每个 Activity 的 onPause、onCreate、onResume 等方法中进行手动方法统计,第一这会增加编码量,第二这对代码有侵入,第三对于第三方 sdk 内的 Activity 代码,无法进行修改。使用 AOP,表示需要找到一个切入点,这个切入点是 Activity 生命周期回调的入口。这里推荐三种方案。
◎ Hook Instrumentation
Hook Instrumentation 是指通过反射将 ActivtyThread 内的 Instrumentation 对象替换成我们自定义的 Instrumentation 对象。在插件化方案中,Hook Instrumentation 是种很常见的方式。由于所有 Activity 生命周期的回调都要经过 Instrumentation 对象,因此通过 Hook Instrumentation 对象,可以很方便地统计出 Actvity 每个生命周期的耗时。以启动流程第一阶段的 Pause 耗时为例,可以这么修改 Instrumentation:
public class TestInstrumentation extends Instrumentation {
private static final String TAG="TestInstrumentation";
private static final Instrumentation mBase;
public TestInstrumentation(Instrumentation base){
mBase = base;
}
.......
@Override
public void callActivityOnPause(Activity activity) {
long startTime = System.currentTimeMillis();
mBase.callActivityOnPause(activity);
Log.d(TAG,"onPause cost:"+(System.currentTimeMillis()-startTime));
}
.......
}
而 Render 耗时,可以在 callActivityOnResume 方法最后,通过 Post Message 的方式进行统计。
Hook Instrumentation 是种很理想的解决方案,唯一的问题是太多人喜欢 Hook 它了。由于很多功能,比如插件化都喜欢 Hook Instrumentation,为了不影响他们的使用,不得不重写大量的方法执行 mBase.xx()。如果 Instrumentation 是个接口,能够使用动态代理就更理想了。
◎ Hook Looper-Printer
Hook Looper是种比较取巧的方案,做法是通过Looper.getMainLooper().setMessageLogging(Printer)方法设置一个日志对象。
public static void loop() {
......
for (;;) {
......
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
......
try {
msg.target.dispatchMessage(msg);
end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
.......
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
.......
}
}
在 Looper 执行消息前后,如果 Printer 对象不为空,就会各输出一段日志,而我们知道Activity 的生命周期回调的起点其实都是 ActviityThread 内的 mH 这个 Handler,通过解析日志,就能知道当前 msg 是否是相应的生命周期任务,解析大致流程如下:
匹配“>>>>> Dispatching to”和“<<<<< Finished to ”,区分msg开始和结束节点
匹配msg.target是否等于“android.app.ActivityThread$H”,确定是否为生命周期调消息
匹配msg.what,确定当前消息码,不同生命周期回调对应不同消息码,比如LAUNCH_ACTIVITY = 100、PAUSE_ACTIVITY = 101
统计开始节点和结束节点之前的耗时,就能得出响应生命周期的耗时。同样的,Render耗时需要在Launch结束时,通过Post Message的方式得出。
这个方案的优点是不需要通过反射等方式,修改系统对象,所以安全性很高。但是通过该方法只能区分 Pause、Launch、Render 三个步骤的相应耗时,无法细分 Launch 方法中各个生命周期的耗时,因为是以每个消息的执行为统计单位,而 Launch 消息实际上同时包含了 onCreate、onStart、onResume 等的回调。更致命的一点是在 Android P 中,系统对生命周期的处理做了一次大的重构,不再细分 Pause、Launch、Stop、Finish 等消息,统一使用 EXECUTE_TRANSACTION=159 来处理,而具体生命周期的处理则是用多态的方式实现。所以该方案无法兼容Android P及以上版本。
Hook ActivityThread$H
每当 ASM 通过 Binder 调用到到 App 端时,会根据不同的调用方法转化成不同的消息放入 ActivityThread$H 这个 Handler 中,因此,只要 Hook 住了 ActivityThread$H,就能得到所有生命周期的起点。
另外,Handler 事实上可以设置一个 mCallback 字段(需要通过反射设置),在执行 dispatchMessage 方法时,如果 mCallback 不为空,则优先执行 mCallback。
Handler.java
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
因此,可以通过反射获取ActivityThread中的H对象,将mCallback修改为自己实现的Handler.Callback对象,实现消息的拦截,而不需要替换Hanlder对象。
class ProxyHandlerCallback implements Handler.Callback {
//设置当前的callback,防止其他sdk也同时设置了callback被覆盖
public final Handler.Callback mOldCallback;
public final Handler mHandler;
ProxyHandlerCallback(Handler.Callback oldCallback, Handler handler) {
mOldCallback = oldCallback;
mHandler = handler;
}
@Override
public boolean handleMessage(Message msg) {
// 处理消息开始,同时返回消息类型,主要为了兼容Android P,把159消息转为101(Pause)和100(Launch)
int msgType = preDispatch(msg);
// 如果旧的callback返回true,表示已经被它拦截,而它内部必定调用了Handler.handleMessage,直接返回
if (mOldCallback != null && mOldCallback.handleMessage(msg)) {
postDispatch(msgType);
return true;
}
// 直接调用handleMessage执行消息处理
mHandler.handleMessage(msg);
// 处理消息结束
postDispatch(msgType);
// 返回true,表示callback会拦截消息,Hanlder不需要再处理消息因为我们上一步已经处理过了
return true;
}
.......
}
为了统计mHandler.handleMessage(msg)方法耗时,Callback的handleMessage方法会返回true。preDispatch和postDispatch的处理和Hook Looper流程差不多,不过增加了Android P下,消息类行为159时的处理,方案可以参考《Android的插件化兼容性》。
和Hook Looper一样,Hook Hanlder也有个缺点是无法分别获取Launch中各个生命周期的耗时。
最后做下总结:
Activity的启动分为Pause、Launch和Render三个步骤,在启动一个新Activity时,会先Pause前一个正在显示的Activity,再加载新Activity,然后开始渲染,直到第一帧渲染成功,Activity才算启动完毕。
可以利用Logcat查看系统输出的Activity启动耗时,系统会统计Activity Launch+Render的时间做为耗时时间,而系统最多允许Pause操作超时500ms,到时见就会自己调用Pause完成方法进行后续流程。
可以使用Hook Instrumentation、Hook Looper、Hook Handler三种方式实现AOP的耗时统计,其中Hook Looper方式无法兼容Android P。
推广下 DoraemonKit ,是一款功能齐全的客户端( iOS 、Android )研发助手,已集成耗时统计功能。
ArgusAPM
(Android 9.0)Activity启动流程源码分析
Android应用程序窗口设计框架介绍
显示窗口动画的原理分析
切换Activity窗口(App Transition)的过程分析
WMS常见问题一(Activity displayed延迟)
《Android的插件化兼容性》

本文作者
▬
林 基 宗
滴滴出行 | 高级软件开发工程师
多年移动端开发经验,专注于安卓移动端性能优化,新技术探索。目前供职于滴滴普惠出行产品技术部,主要负责代驾司机端App与DoKit的日常开发和维护。
推荐阅读
▬
更多推荐
▬
滴滴开源 / Open Source
Mpx | Booster | Chameleon | DDMQ | DroidAssist | Rdebug | Doraemonkit | Kemon | Mand Moblie | virtualApk | 获取更多项目
技术干货 / Recommended article
深度解密 Go 语言:关于 interface 的 10 个问题 | 滴滴宋世君:DS(数据分析师),究竟是做什么的? | 一个 Golang 项目的测试实践全记录 | 阅读更多内容