Activity的啓動速度是不少開發者關心的問題,當頁面跳轉耗時過長時,App就會給人一種很是笨重的感受。在遇到某個頁面啓動過慢的時候,開發的第一直覺通常是onCreate執行速度太慢了,而後在onCreate方法先後記錄下時間戳計算出耗時。不過有時候即便把onCreate方法的耗時優化了,效果仍舊不明顯。實際上影響到Activity啓動速度的緣由是多方面的,須要從Activity的啓動流程入手,才能找到真正問題所在。html
若是要給Activity的「啓動」作一個定義的話,我的以爲應該是:從調用startActivity到 Activity可被操做爲止,表明啓動成功。所謂的可被操做,是指可接受各類輸入事件,好比手勢、鍵盤輸入之類的。換個角度來講,也能夠當作是主線程處於空閒狀態,能執行後續進入的各類Message。java
Activity的啓動能夠分爲三個步驟,以ActivityA啓動ActivityB爲例,三步驟分別爲:android
Activity啓動涉及到App進程與ActivityManagerService(AMS)、WindowManagerService(WMS)的通訊,網上關於這個流程的文章不少,這邊就再也不具體描述了,只列一下關鍵方法的調用鏈路。git
當ActivityA使用startActivity方法啓動ActivityB時,執行函數鏈路以下github
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了」。安全
在AMS接收到App進程對於activityPaused方法的調用後,執行函數鏈路以下markdown
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方法爲終點,統計出這一步驟的總耗時。app
在ActivityB執行完onResume方法後,就能夠顯示該Activity了,調用流程以下框架
WindowManager.addView->
WindowManagerImpl.addView->
ViewRootImpl.setView->
ViewRootImpl.requestLayout->
└ViewRootImpl.scheduleTraversals->
└Choreographer.postCallback->
WindowManagerSerivce.add
複製代碼
這一步的核心其實是Choreographer.postCallback,向Choreographer註冊了一個回調,當Vsync事件到來時,就會執行下面的回調進行ui的渲染async
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層的方法耗時,這塊耗時又是難以對其進行干涉的,因此接下去會把統計的重點放在經過編碼能影響到的耗時上,按照啓動流程的三個步驟,劃分爲三種耗時。
儘管啓動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耗時能夠經過onCreate、onRestoreInstanceState、onStart、onResume四個函數的耗時相加得出。在這四個方法中,onCreate通常是最重的那個方法,由於不少變量的初始化都會放在這裏進行。另外,onCreate方法中還有個耗時大戶是LayoutInfalter.infalte方法,調用setContentView會執行到這個方法,對於一些複雜佈局的第一次解析,會消耗大量時間。因爲這四個方法是同步順序執行的,單獨把某些操做從onCreate移到onResume之類的並無什麼意義,Launch耗時只關心這幾個方法的總耗時。
從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); } 複製代碼
經過添加一點小延遲,能夠把消息的執行時間延遲到屏障消息以後,這條消息就會被屏障消息阻塞,直到屏障消息被移除時才執行了。不過因爲系統函數執行時間不可控,這種方式並不保險。
這裏單獨說一下Fragment的耗時。Fragment本質上是一個View,只不過這個View有本身的聲明週期管理。
耗時統計是很是適合使用AOP思想來實現的功能。咱們固然不但願在每一個Activity的onPause、onCreate、onResume等方法中進行手動方法統計,第一這會增長編碼量,第二這對代碼有侵入,第三對於第三方sdk內的Activity代碼,沒法進行修改。使用AOP,表示須要找到一個切入點,這個切入點是Activity生命週期回調的入口。這裏推薦兩三方案。
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是種比較取巧的方案,作法是經過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是不是相應的生命週期任務,解析大體流程以下:
這個方案的優勢是不須要經過反射等方式,修改系統對象,因此安全性很高。可是經過該方法只能區分Pause、Launch、Render三個步驟的相應耗時,沒法細分Launch方法中各個生命週期的耗時,由於是以每一個消息的執行爲統計單位,而Launch消息實際上同時包含了onCreate、onStart、onResume等的回調。更致命的一點是在Android P中,系統對生命週期的處理作了一次大的重構,再也不細分Pause、Launch、Stop、Finish等消息,統一使用EXECUTE_TRANSACTION=159來處理,而具體生命週期的處理則是用多態的方式實現。因此該方案沒法兼容Android P及以上版本
每當ASM經過Binder調用到到App端時,會根據不一樣的調用方法轉化成不一樣的消息放入ActivityThreadH,就能獲得全部生命週期的起點。 另外,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中各個生命週期的耗時。
最後作下總結:
推廣下DoraemonKit, 是一款功能齊全的客戶端( iOS 、Android )研發助手,已集成耗時統計功能!