Android View源碼解讀:淺談DecorView與ViewRootImpl

前言

對於Android開發者來說,View無疑是開發中經常接觸的,包括它的事件分發機制、測量、佈局、繪製流程等,如果要自定義一個View,那麼應該對以上流程有所瞭解、研究。本系列文章將會爲大家帶來View的工作流程詳細解析。在深入接觸View的測量、佈局、繪製這三個流程之前,我們從Activity入手,看看從Activity創建後到View的正式工作之前,所要經歷的步驟。以下源碼均取自Android API 21。

從setContentView說起

一般地,我們在Activity中,會在onCreate()方法中寫下這樣一句:

setContentView(R.layout.main);

顯然,這是爲activity設置一個我們定義好的main.xml佈局,我們跟蹤一下源碼,看看這個方法是怎樣做的,Activity#setContentView:

public void setContentView(@LayoutRes int layoutResID) {
     getWindow().setContentView(layoutResID);  //調用getWindow方法,返回mWindow
     initWindowDecorActionBar();
}
...
public Window getWindow() {   
     return mWindow;
}

從上面看出,裏面調用了mWindow的setContentView方法,那麼這個“mWindow”是何方神聖呢?嘗試追蹤一下源碼,發現mWindow是Window類型的,但是它是一個抽象類,setContentView也是抽象方法,所以我們要找到Window類的實現類才行。我們在Activity中查找一下mWindow在哪裏被賦值了,可以發現它在Activity#attach方法中有如下實現:

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) {
        ...
        mWindow = new PhoneWindow(this);
        ...
    }

我們只看關鍵部分,這裏實例化了PhoneWindow類,由此得知,PhoneWindow是Window的實現類,那麼我們在PhoneWindow類裏面找到它的setContentView方法,看看它又實現了什麼,PhoneWindow#setContentView:

@Override
public void setContentView(int layoutResID) {
    // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
    // decor, when theme attributes and the like are crystalized. Do not check the feature
    // before this happens.
    if (mContentParent == null) { // 1
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        mLayoutInflater.inflate(layoutResID, mContentParent); // 2
    }
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
}

首先判斷了mContentParent是否爲null,如果爲空則執行installDecor()方法,那麼這個mContentParent又是什麼呢?我們看一下它的註釋:

// This is the view in which the window contents are placed. It is either
// mDecor itself, or a child of mDecor where the contents go.
private ViewGroup mContentParent;

它是一個ViewGroup類型,結合②號代碼處,可以得知,這個mContentParent是我們設置的佈局(即main.xml)的父佈局。註釋還提到了,這個mContentParent是mDecor本身或者是mDecor的一個子元素,這句話什麼意思呢?這裏先留一個疑問,下面會解釋。

這裏先梳理一下以上的內容:Activity通過PhoneWindow的setContentView方法來設置佈局,而設置佈局之前,會先判斷是否存在mContentParent,而我們設置的佈局文件則是mContentParent的子元素。

創建DecorView

接着上面提到的installDecor()方法,我們看看它的源碼,PhoneWindow#installDecor:

private void installDecor() {
    if (mDecor == null) {
        mDecor = generateDecor(); // 1
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor); // 2
        ...
        } 
    }
}

首先,會執行①號代碼,調用PhoneWindow#generateDecor方法:

protected DecorView generateDecor() {
    return new DecorView(getContext(), -1);
}

可以看出,這裏實例化了DecorView,而DecorView則是PhoneWindow類的一個內部類,繼承於FrameLayout,由此可知它也是一個ViewGroup。
那麼,DecroView到底充當了什麼樣的角色呢?
其實,DecorView是整個ViewTree的最頂層View,它是一個FrameLayout佈局,代表了整個應用的界面。在該佈局下面,有標題view和內容view這兩個子元素,而內容view則是上面提到的mContentParent。我們接着看②號代碼,PhoneWindow#generateLayout方法

protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.
        // 從主題文件中獲取樣式信息
        TypedArray a = getWindowStyle();

        ...

        if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
            requestFeature(FEATURE_NO_TITLE);
        } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
            // Don't allow an action bar if there is no title.
            requestFeature(FEATURE_ACTION_BAR);
        }

        if(...){
            ...
        }

        // Inflate the window decor.
        // 加載窗口布局
        int layoutResource;
        int features = getLocalFeatures();
        // System.out.println("Features: 0x" + Integer.toHexString(features));
        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            layoutResource = R.layout.screen_swipe_dismiss;
        } else if(...){
            ...
        }

        View in = mLayoutInflater.inflate(layoutResource, null);    //加載layoutResource
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); //往DecorView中添加子View,即mContentParent
        mContentRoot = (ViewGroup) in;

        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); // 這裏獲取的就是mContentParent
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }

        if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) {
            ProgressBar progress = getCircularProgressBar(false);
            if (progress != null) {
                progress.setIndeterminate(true);
            }
        }

        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            registerSwipeCallbacks();
        }

        // Remaining setup -- of background and title -- that only applies
        // to top-level windows.
        ...

        return contentParent;
    }

由以上代碼可以看出,該方法還是做了相當多的工作的,首先根據設置的主題樣式來設置DecorView的風格,比如說有沒有titlebar之類的,接着爲DecorView添加子View,而這裏的子View則是上面提到的mContentParent,如果上面設置了FEATURE_NO_ACTIONBAR,那麼DecorView就只有mContentParent一個子View,這也解釋了上面的疑問:mContentParent是DecorView本身或者是DecorView的一個子元素
用一幅圖來表示DecorView的結構如下:

DecorView結構

小結:DecorView是頂級View,內部有titlebar和contentParent兩個子元素,contentParent的id是content,而我們設置的main.xml佈局則是contentParent裏面的一個子元素。

在DecorView創建完畢後,讓我們回到PhoneWindow#setContentView方法,直接看②號代碼: mLayoutInflater.inflate(layoutResID, mContentParent);這裏加載了我們設置的main.xml佈局文件,並且設置mContentParent爲main.xml的父佈局,至於它怎麼加載的,這裏就不展開來說了。

到目前爲止,通過setContentView方法,創建了DecorView和加載了我們提供的佈局,但是這時,我們的View還是不可見的,因爲我們僅僅是加載了佈局,並沒有對View進行任何的測量、佈局、繪製工作。在View進行測量流程之前,還要進行一個步驟,那就是把DecorView添加至window中,然後經過一系列過程觸發ViewRootImpl#performTraversals方法,在該方法內部會正式開始測量、佈局、繪製這三大流程。至於該一系列過程是怎樣的,因爲涉及到了很多機制,這裏簡單說明一下:

將DecorView添加至Window

每一個Activity組件都有一個關聯的Window對象,用來描述一個應用程序窗口。每一個應用程序窗口內部又包含有一個View對象,用來描述應用程序窗口的視圖。上文分析了創建DecorView的過程,現在則要把DecorView添加到Window對象中。而要了解這個過程,我們首先要簡單先了解一下Activity的創建過程:
首先,在ActivityThread#handleLaunchActivity中啓動Activity,在這裏面會調用到Activity#onCreate方法,從而完成上面所述的DecorView創建動作,當onCreate()方法執行完畢,在handleLaunchActivity方法會繼續調用到ActivityThread#handleResumeActivity方法,我們看看這個方法的源碼:

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward) { 
    //...
    ActivityClientRecord r = performResumeActivity(token, clearHide); // 這裏會調用到onResume()方法

    if (r != null) {
        final Activity a = r.activity;

        //...
        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow(); // 獲得window對象
            View decor = r.window.getDecorView(); // 獲得DecorView對象
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager(); // 獲得windowManager對象
            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); // 調用addView方法
            }
            //...
        }
    }
}

在該方法內部,獲取該activity所關聯的window對象,DecorView對象,以及windowManager對象,而WindowManager是抽象類,它的實現類是WindowManagerImpl,所以後面調用的是WindowManagerImpl#addView方法,我們看看源碼:

public final class WindowManagerImpl implements WindowManager {    
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    ...
    @Override
    public void addView(View view, ViewGroup.LayoutParams params) {
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }
}

接着調用了mGlobal的成員函數,而mGlobal則是WindowManagerGlobal的一個實例,那麼我們接着看WindowManagerGlobal#addView方法:

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ...

        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
            ...

            root = new ViewRootImpl(view.getContext(), display); // 1

            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); // 2
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            synchronized (mLock) {
                final int index = findViewLocked(view, false);
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
            }
            throw e;
        }
    }

先看①號代碼處,實例化了ViewRootImpl類,接着,在②號代碼處,調用ViewRootImpl#setView方法,並把DecorView作爲參數傳遞進去,在這個方法內部,會通過跨進程的方式向WMS(WindowManagerService)發起一個調用,從而將DecorView最終添加到Window上,在這個過程中,ViewRootImpl、DecorView和WMS會彼此關聯,至於詳細過程這裏不展開來說了。
最後通過WMS調用ViewRootImpl#performTraverals方法開始View的測量、佈局、繪製流程,這三大流程在下文會詳細講述,謝謝大家的閱讀。

發佈了61 篇原創文章 · 獲贊 283 · 訪問量 31萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章