1. 程式人生 > >Android應用啟動優化:一種DelayLoad的實現和原理(下篇)(轉載)

Android應用啟動優化:一種DelayLoad的實現和原理(下篇)(轉載)

我們使用第三種方法來實現延遲載入。不過上一篇寫的比較簡單,只是講解了如何去實現,這一篇就來講一下為何要這麼做,以及這麼做後面的原理。
其中會涉及到一些 Android 中的比較重要的類,以及 Activity 生命週期中比較重要的幾個函式。
其實這個其中的原理比較簡單,不過要弄清楚其實現的過程,還是一件蠻好玩的事情,其中會用到一些工具,自己加除錯程式碼等,一步一步下來,自己對 Activity 的啟動的理解又深了一層,希望大家讀完之後也會對大家有一定的幫助。

上一篇中我們最終使用的 DelayLoad 的核心方法是在 Activity 的 onCreate 函式中加入下面的方法 :

1
2
3 4 5 6
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;
}

這個 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,
CharSequence title, Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances, Configuration config, String referrer, IVoiceInteractor voiceInteractor)
{
attachBaseContext(context); mFragments.attachActivity(this, mContainer, null); mWindow = PolicyManager.makeNewWindow(this); mWindow.setCallback(this); mWindow.setOnWindowDismissedCallback(this); mWindow.getLayoutInflater().setPrivateFactory(this); ........ // PolicyManager.makeNewWindow(this) 最終會呼叫 Policy 的 makeNewWindow 方法 public Window makeNewWindow(Context context) { return new PhoneWindow(context); } }

這裡需要注意 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 的使用,之後會有詳細的文章去介紹,這兩個工具非常有助於理解原始碼和一些技術的實現。

Systrace

Method Trace

原始碼編譯與除錯

程式碼