Android應用啟動優化:一種DelayLoad的實現和原理(下篇)(轉載)
我們使用第三種方法來實現延遲載入。不過上一篇寫的比較簡單,只是講解了如何去實現,這一篇就來講一下為何要這麼做,以及這麼做後面的原理。
其中會涉及到一些 Android 中的比較重要的類,以及 Activity 生命週期中比較重要的幾個函式。
其實這個其中的原理比較簡單,不過要弄清楚其實現的過程,還是一件蠻好玩的事情,其中會用到一些工具,自己加除錯程式碼等,一步一步下來,自己對 Activity 的啟動的理解又深了一層,希望大家讀完之後也會對大家有一定的幫助。
上一篇中我們最終使用的 DelayLoad 的核心方法是在 Activity 的 onCreate 函式中加入下面的方法 :
1 2 |
getWindow().getDecorView().post(new Runnable() { @Override public void run() { myHandler.post(mLoadingRunnable); } }); |
我們一一來看涉及到的類和方法
1. Activity.getWindow 及 PhoneWindow 的初始化時機
Activity 的 getWindow 方法獲取到的是一個 PhoneWindow 物件:
1 2 3 |
public Window getWindow() { return |
這個 mWindow 就是一個 PhoneWindow 物件,其初始化的時機為這個 Activity attach 的時候:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, int ident, Application application, Intent intent, ActivityInfo info, |
這裡需要注意 Activity 的 attach 方法很早就會呼叫的,是要早於 Activity 的 onCreate 方法的。
總結:
- PhoneWindow 與 Activity 是一對一的關係,通過上面的初始化過程你應該更加清楚這個概念
- Android 中對 PhoneWindow 的註釋是 :Android-specific Window ,可見其重要性
- PhoneWindow 中有很多大家比較熟悉的方法,比如 setContentView / addContentView 等 ; 也有幾個重要的內部類,比如:DecorView ;
2. PhoneWindow.getDecorView 及 DecorView 的初始化時機
上面我們說到 DecorView是 PhoneWindow 的一個內部類,其定義如下:
1
|
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker
|
那麼 DecorView 是什麼時候初始化的呢?DecorView 是在 Activity 的父類的 onCreate 方法中被初始化的,比如我例子中的 MainActivity 是繼承自 android.support.v7.app.AppCompatActivity ,當我們呼叫 MainActivity 的 super.onCreate(savedInstanceState); 的時候,就會呼叫下面的
1 2 3 4 5 |
protected void onCreate(@Nullable Bundle savedInstanceState) { getDelegate().installViewFactory(); getDelegate().onCreate(savedInstanceState); super.onCreate(savedInstanceState); } |
由於我們匯入的是 support.v7 包裡面的AppCompatActivity, getDelegate() 得到的就是AppCompatDelegateImplV7 ,其 onCreate 方法如下:
1 2 3 4 5 6 |
public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mWindowDecor = (ViewGroup) mWindow.getDecorView(); ...... } |
就是這裡的 mWindow.getDecorView() ,對 DecorView 進行了例項化:
1 2 3 4 5 6 |
public final View getDecorView() { if (mDecor == null) { installDecor(); } return mDecor; } |
第一次呼叫 getDecorView 的時候,會進入 installDecor 方法,這個方法對 DecorView 進行了一系列的初始化 ,其中比較重要的幾個方法有:generateDecor / generateLayout 等,generateLayout 會從當前的 Activity 的 Theme 提取相關的屬性,設定給 Window,同時還會初始化一個 startingView,新增到 DecorView上,也就是我們所說的 startingWindow。
總結
- Decor 有裝飾的意思,DecorView 官方註釋為 “This is the top-level view of the window, containing the window decor” , 我們可以理解為 DecorView 是我們當前 Activity 的最下面的佈局。所以我們開啟 DDMS 檢視 Tree Overview 的時候,可以發現最根部的那個 View 就是 DecorView:
- 應用從桌面啟動的時候,在主 Activity 還沒有顯示的時候,如果主題沒有設定視窗的背景,那麼我們就會看到白色(這個和手機的Rom也有關係),如果應用啟動很慢,那麼使用者得看好一會白色。如果要避免這個,則可以在 Application 或者 Activity 的 Theme 中設定 WindowBackground , 這樣就可以避免白色(當然現在各種大廠都是SplashActivity+廣告我也是可以理解的)
3. Post
當我們呼叫 DecorView 的 Post 的時候,其實最終會呼叫 View 的 Post ,因為 DecorView 最終是繼承 View 的:
1 2 3 4 5 6 7 8 9 |
public boolean post(Runnable action) { final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { return attachInfo.mHandler.post(action); } // Assume that post will succeed later ViewRootImpl.getRunQueue().post(action); return true; } |
注意這裡的 mAttachInfo ,我們呼叫 post 是在 Activity 的 onCreate 中呼叫的,那麼此時 mAttachInfo 是否為空呢?答案是 mAttachInfo 此時為空。
這裡有一個點就是 Activity 的各個回撥函式都是幹嘛的?是不是平時自己寫應用的時候,貌似在 onCreate 裡面搞定一切就OK了, onResume ? onStart?沒怎麼涉及到嘛,其實不然。
onCreate 顧名思義就是 Create ,我們在前面看到,Activity 的 onCreate 函式做了很多初始化的操作,包括 PhoneWindow/DecorView/StartingView/setContentView等,但是 onCreate 只是初始化了這些物件.
真正要設定為顯示則在 Resume 的時候,不過這些對開發者是透明瞭,具體可以看 ActivityThread 的 handleResumeActivity 函式,handleResumeActivity 中除了呼叫 Activity 的 onResume 回撥之外,還初始化了幾個比較重要的類:ViewRootImpl / ThreadedRenderer。
ActivityThread.handleResumeActivity:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
if (r.window == null && !a.mFinished && willBeVisible) { r.window = r.activity.getWindow(); View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); ViewManager wm = a.getWindowManager(); WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; if (a.mVisibleFromClient) { a.mWindowAdded = true; wm.addView(decor, l); } |
主要是 wm.addView(decor, l); 這句,將 decorView 與 WindowManagerImpl聯絡起來,這句最終會呼叫到 WindowManagerGlobal 的 addView 函式,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { ...... ViewRootImpl root; View panelParentView = null; ...... root = new ViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); mViews.add(view); mRoots.add(root); mParams.add(wparams); } // do this last because it fires off messages to start doing things try { root.setView(view, wparams, panelParentView); } catch (RuntimeException e) { ...... } } |
我們知道 ViewRootImpl 是 View 系統的一個核心類,其定義如下:
1 2 |
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks
|
ViewRootImpl 初始化的時候會對 AttachInfo 進行初始化,這就是為什麼之前的在 onCreate 的時候 attachInfo 為空。ViewRootImpl 裡面有很多我們比較熟悉也非常重要的方法,比如 performTraversals / performLayout / performMeasure / performDraw / draw 等。
我們繼續 addView 中的root.setView(view, wparams, panelParentView); 傳入的 view 為 decorView,root 為 ViewRootImpl ,這個函式中將 ViewRootImpl 的mView 變數 設定為傳入的view,也就是 decorView。
這樣來看,ViewRootImpl 與 DecorView 的關係我們也清楚了。
扯了一圈,我們再回到大標題的 Post 函式上,前面有說這個 Post 走的是 View 的Post 函式,由於 在 onCreate 的時候 attachInfo 為空,所以會走下面的分支:ViewRootImpl.getRunQueue().post(action);
注意這裡的 getRunQueue 得到的並不是 Looper 裡面的那個 MessageQueue,而是由 ViewRootImpl 維持的一個 RunQueue 物件,其核心為一個 ArrayList :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
private final ArrayList<HandlerAction> mActions = new ArrayList<HandlerAction>(); void post(Runnable action) { postDelayed(action, 0); } void postDelayed(Runnable action, long delayMillis) { HandlerAction handlerAction = new HandlerAction(); handlerAction.action = action; handlerAction.delay = delayMillis; synchronized (mActions) { mActions.add(handlerAction); } } void executeActions(Handler handler) { synchronized (mActions) { final ArrayList<HandlerAction> actions = mActions; final int count = actions.size(); for (int i = 0; i < count; i++) { final HandlerAction handlerAction = actions.get(i); handler.postDelayed(handlerAction.action, handlerAction.delay); } actions.clear(); } } |
當我們執行了 Post 之後 ,其實只是把 Runnable 封裝成一個 HandlerAction 物件存入到 ArrayList 中,當執行到 executeActions 方法的時候,將存在這裡的 HandlerAction 再通過 executeActions 方法傳入的 Handler 物件重新進行 Post。
那麼 executeActions 方法是什麼時候執行的呢?傳入的 Handler 又是哪個 Handler 呢?
4. PerformTraversals
我們之前講過,ViewRootImpl 的 performTraversals 方法是一個很核心的方法,每一幀繪製都會走一遍,呼叫各種 measure / layout / draw 等 ,最終將要顯示的資料交給 hwui 去進行繪製。
我們上一節講到的 executeActions ,就是在 performTraversals 中執行的:
1 2 |
// Execute enqueued actions on every traversal in case a detached view enqueued an action getRunQueue().executeActions(mAttachInfo.mHandler); |
可以看到這裡傳入的 Handler 是 mAttachInfo.mHandler ,上一節講到 mAttachInfo 是在 ViewRootImpl 初始化的時候一起初始化的:
1
|
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this);
|
這裡的 mHandler 是一個 ViewRootHandler 物件:
1 2 3 4 5 |
final class ViewRootHandler extends Handler{ ...... } ...... final ViewRootHandler mHandler = new ViewRootHandler(); |
我們注意到 ViewRootHandler 在建立的時候並沒有傳入一個 Looper 物件,這意味著此 ViewRootHandler 的 Looper 就是 mainLooper。
這下我們就清楚了,我們在 onCreate 中 Post 的 runnable 物件,最終還是在第一個 performTraversals 方法執行的時候,加入到了 MainLooper 的 MessageQueue 裡面了。
繞了一圈終於我們終於把文章最前面的那句話解釋清楚了,當然中間還有很多的廢話,不過我估計能耐著性子看到這裡的人會很少,所以如果你看到了這裡,可以在底下的評論裡面將 index ++ ;這裡 index = 0 ;就是看看幾個人是真正認真看了這篇文章的。
5. UpdateText
接著 performTraversals 我們繼續說,話說在第一篇文章 我們有講到,Activity 在啟動時,會在第二次執行 performTraversals 才會去真正的繪製,原因在於第一次執行 performTraversals
的時候,會走到 Egl 初始化的邏輯,然後會重新執行一次 performTraversals 。
所以前一篇文章的評論區有人問為何在 run 方法裡面還要 post 一次,如果在 run 方法裡面直接執行 updateText 方法 ,那麼 updateText 就會在第一個 performTraversals 之後就執行,而不是在第一幀繪製完成後才去執行,所以我們又 Post 了一次 。所以大概的處理步驟如下:
第一步:Activity.onCreate –> Activity.onStart –> Activity.onResume
第二步:ViewRootImpl.performTraversals –>Runnable
第三步:Runnable –> ViewRootImpl.performTraversals
第四步:ViewRootImpl.performTraversals –> UpdateText
第五步:UpdateText
6. 總結
其實一路跟下來發現其實原理很簡單,其實 DelayLoad 其實只是一個很小的點,關鍵是教大家如何去跟蹤一個自己不認識的知識點或者優化,這裡面主要用到了兩個工具:Systrace 和 Method Trace, 以及原始碼編譯和除錯。
關於 Systrace 和 Method Trace 的使用,之後會有詳細的文章去介紹,這兩個工具非常有助於理解原始碼和一些技術的實現。