Android View原始碼解讀:淺談DecorView與ViewRootImpl

yangxi_001發表於2017-01-06

前言

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

從setContentView說起

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

setContentView(R.layout.main);
  • 1
  • 1

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

public void setContentView(@LayoutRes int layoutResID) {
     getWindow().setContentView(layoutResID);  //呼叫getWindow方法,返回mWindow
     initWindowDecorActionBar();
}
...
public Window getWindow() {   
     return mWindow;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

從上面看出,裡面呼叫了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);
        ...
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

我們只看關鍵部分,這裡例項化了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();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

首先判斷了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;
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

它是一個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
        ...
        } 
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

首先,會執行①號程式碼,呼叫PhoneWindow#generateDecor方法:

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

可以看出,這裡例項化了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;
    }
  • 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
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 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
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

由以上程式碼可以看出,該方法還是做了相當多的工作的,首先根據設定的主題樣式來設定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方法
            }
            //...
        }
    }
}
  • 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
  • 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

在該方法內部,獲取該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);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

接著呼叫了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;
        }
    }
  • 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
  • 30
  • 31
  • 32
  • 33
  • 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
  • 30
  • 31
  • 32
  • 33

先看①號程式碼處,例項化了ViewRootImpl類,接著,在②號程式碼處,呼叫ViewRootImpl#setView方法,並把DecorView作為引數傳遞進去,在這個方法內部,會通過跨程式的方式向WMS(WindowManagerService)發起一個呼叫,從而將DecorView最終新增到Window上,在這個過程中,ViewRootImpl、DecorView和WMS會彼此關聯,至於詳細過程這裡不展開來說了。 

最後通過WMS呼叫ViewRootImpl#performTraverals方法開始View的測量、佈局、繪製流程,這三大流程在下文會詳細講述,謝謝大家的閱讀。

轉自:http://blog.csdn.net/a553181867/article/details/51477040

相關文章