Android Activity啟動耗時統計方案
Activity的啟動速度是很多開發者關心的問題,當頁面跳轉耗時過長時,App就會給人一種非常笨重的感覺。在遇到某個頁面啟動過慢的時候,開發的第一直覺一般是onCreate執行速度太慢了,然後在onCreate方法前後記錄下時間戳計算出耗時。不過有時候即使把onCreate方法的耗時優化了,效果仍舊不明顯。實際上影響到Activity啟動速度的原因是多方面的,需要從Activity的啟動流程入手,才能找到真正問題所在。
Activity啟動流程
如果要給Activity的“啟動”做一個定義的話,個人覺得應該是:從調用startActivity到
Activity可被操作為止,代表啟動成功。所謂的可被操作,是指可接受各種輸入事件,比如手勢、鍵盤輸入之類的。換個角度來說,也可以看成是主線程處于空閑狀态,能執行後續進入的各種Message。
Activity的啟動可以分為三個步驟,以ActivityA啟動ActivityB為例,三步驟分别為:
- 以ActivityA調用startActivity,到ActivityA成功pause為止
- ActivityB成功初始化,到執行完resume為止
- ActivityB向WSM注冊視窗,到第一幀繪制完成為止
Activity啟動涉及到App程序與ActivityManagerService(AMS)、WindowManagerService(WMS)的通信,網上關于這個流程的文章很多,這邊就不再具體描述了,隻列一下關鍵方法的調用鍊路。
ActiivtyA Pause流程
當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.onPause
ActivityManagerNative.getDefault().activityPaused
當App請求AMS要啟動一個新頁面的時候,AMS首先會pause掉目前正在顯示的Activity,當然,這個Activity可能與請求要開啟的Activity不在一個程序,比如點選桌面圖示啟動App,目前要暫停的Activity就是桌面程式Launcher。在onPause内執行耗時操作是一種很不推薦的做法,從上述調用鍊路可以看出,如果在onPause内執行了耗時操作,會直接影響到ActivityManagerNative.getDefault().activityPaused()方法的執行,而這個方法的作用就是通知AMS,“目前Activity已經已經成功暫停,可以啟動新Activity了”。
ActivityB Launch流程
在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後就能看到如下日志
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5SMwU2Zpx2LcV2Zpx2Lc12bj5ycj5Wd5lGbh5Sahh2ZuFGaz1ibj1ycz9mLyVmdhp2Lc9CX6MHc0RHaiojIsJye.png)
末尾的+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);
}
通過添加一點小延遲,可以把消息的執行時間延遲到屏障消息之後,這條消息就會被屏障消息阻塞,直到屏障消息被移除時才執行了。不過由于系統函數執行時間不可控,這種方式并不保險。
Frament耗時
這裡單獨說一下Fragment的耗時。Fragment本質上是一個View,隻不過這個View有自己的聲明周期管理。
應用内統計方案
耗時統計是非常适合使用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 )研發助手,已內建耗時統計功能!