1. 程式人生 > >Android 啟動優化之啟動頁白屏的真正原理

Android 啟動優化之啟動頁白屏的真正原理

網上有許多解釋

大牛A說:應用啟動的時候有一個空白的Window,View繪製到另一個Window上,在繪製完畢之後,後一個Window去替換掉前一個Window。

B說:setContentView耗時太久,所以白屏

大牛A之所以會這樣認為,是因為他看過原始碼或者開發經驗豐富,知道下一個Activity完全被繪製出來之後,才會去覆蓋原來的Activity的介面,可是這對於從Launcher冷啟動進入Activity的情形並不適用。

B則是認為setContentView就是在繪製了,不過還是給了我一些啟發。

我配合之前看過的原始碼以及對某個app的觀察,得出了結論如下:

從Launcher啟動Activity的時候,我們就建立了一個Window,這個Window自帶了DecorView(這裡我不太明白Window是在建立、attachApplication前建立的還是後建立的,應該是後,這個需要檢視Framework原始碼才能驗證,不過這個不重要,先不深究)。然後會根據你入口活動的主題的window_background屬性去繪製DecorView,預設是白色(或者無色,我列印過,是ColorDrawable)。所以你啟動一個app的時候會有白屏。然後必須在setContentView前把主題修改回來,因為在setContentView裡已經進行了主題、Window_DecorView,所以在setContentView後setTheme應該會被攔截,或者設定了,但已晚了。在onResume後會開始根View的繪製,在繪製完畢後,會被新增到DecorView中。

而在已啟動活動後,開啟頁面內的活動時,會在新的Activity的onResume後繪製View,然後在繪製完成後把新的View新增到DecorView中,當然等同於新增到Window中了,最後用新的Window替換掉之前Activity的Window。

下面需要查閱onResume後的原始碼進行證實

// If the window hasn't yet been added to the window manager,
// and this guy didn't finish itself or start another activity,
// then go ahead and add the window.

直接看見了這個註釋,如果這個Activity的Window還沒有被新增到WindowManger中,並且這個Window沒有停止他自身(也就是使用者打開了這個Activity建立到一半又突然按了back鍵想關了)或者啟動了一個新的活動,然後繼續後面的流程並且把這個Window新增到WindowManager中。

註釋的太詳細了,一下子就驗證了我的猜測,所以原始碼都不用看了。從Launcher進入肯定是在一開始就把Window新增到WindowManager中去了,比較特殊。

但是有一個細節我說錯了

wm.addView(decor, l);

WindowManager新增的是DecorView,而非Window,Window是DecorView的管理者。所以上面講的什麼替換Window,其實都是替換DecorView。

之所以要探究其原理全是為了啟動優化啊!不明白啟動頁白屏原理的啟動優化都是耍流氓!!!

我們給啟動頁添加了window_background後,會在onCreate的setContentView中通過XmlParser、反射獲取根View,然後把根View新增到DecorView中去,在onResume後開始繪製,繪製完後把DecorView新增到WindowManager中去。

這裡再糾正一下前面的一個點,冷啟動活動DecorView在一開始就被新增到WindowManager中去了,在onResume後開始繪製,你是看不見的,是inVisable的

decor.setVisibility(View.INVISIBLE);

再糾正一下,又有一點說錯了

WindowManager.add DecorView後,先把他置為不可見,然後decor.makeVisible,這個時候就開始正式繪製了。

現在終於知道了原理

冷啟動一開始會繪製一波decor的window_background,這個時候decor已被新增到WindowManager

decor.makeVisible開始繪製,原來的背景圖消失了,主介面出來了

所以從一開始,到onResume後,decor.makeVisible,到decor繪製完畢,這一段時間是window_background的顯示時間。這一段時間基本都在進行系統的啟動,所以這段時間是我們不可以佔用的,一旦在主執行緒寫了些什麼,所耗費的時間,都會使得閃屏頁延遲對應的時間顯示。

那麼開啟子執行緒行不行,其實我糾結於Java子執行緒和OS子執行緒的區別了。因為HotShot虛擬機器無疑用的是時間片輪轉排程演算法,所以我認為即使開了子執行緒,也會搶佔主執行緒的時間片。此外就是執行緒的實現方式有核心態和使用者態以及前兩者結合的混合態。無疑HotShot是混合態。

所以得出結論:適當子執行緒能夠充分地利用CPU(因為多核CPU相當於多個CPU),但是不要太多,太多子執行緒之間上下文切換的開銷極大。

綜上所述,可以開啟一定數量的子執行緒,並且設定好優先順序。具體做法不再展開了,本文主要是對啟動原理的分析,已經是淋漓盡致了,強大的啟動優化的實現的文章會在不久後給出,圍繞Rocket框架出發,全方位分析啟動優化過程中的每一個細節。

///////////////////////////////////////////////////

最後寫了個demo,發現我說的是錯的。一開始像大牛A說的一樣,確實有一個Window,這個Window的顏色和我們給activity指定的style的background的顏色一致。

@Override
protected void onCreate(Bundle savedInstanceState) {

    setTheme(R.style.AppTheme2);

    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    Log.i(TAG, getWindow().getDecorView().getBackground() + "");

然後我們是可以在onCreate中改變這個主題的,decor view的background drawable也因此而改變。這也意味著,一開始預顯示的,並非是decor view,而確確實實是另一個Window。

可以看我寫的另一個demo

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "xbh";

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        getWindow().getDecorView().postDelayed(new Runnable() {
            @Override
            public void run() {

                try {
                    test();
                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }

            }
        }, 2000);
    }

    private void test() throws NoSuchFieldException, IllegalAccessException {
        Field mGlobalField = getWindowManager().getClass().getDeclaredField("mGlobal");
        mGlobalField.setAccessible(true);

        Object mGlobal = mGlobalField.get(getWindowManager());

        Field mViewsField = mGlobal.getClass().getDeclaredField("mViews");
        mViewsField.setAccessible(true);

        Object mViews = mViewsField.get(mGlobal);

        List viewList = (List) mViews;

        for (int i = 0, size = viewList.size(); i < size; i ++) {
            Log.i(TAG, viewList.get(i) + "");
        }
    }
}

如果沒有2000延遲,log為空;有了,就有了我們的decor view。這也直接證明了:一開始預顯示的,並非是decor view,而確確實實是另一個Window。而我們的decor view,直到onResume後才會被真正的新增到螢幕中去。而之前那個window根本就沒有被新增到WindowManagerGlobal中。