由setContentView()方法引起的思考

19snow93發表於2019-03-23

Android的setContentView()方法我們平時用很多,但是有多少人會點進setContentView()方法裡面看看它的原始碼究竟是何方神聖呢,今天我就來看看從這個方法裡面究竟涉及到多少未知的知識。

public class ViewActivity extends Activity {


    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_view);
    }
}
複製程式碼

懷著好奇心我點下了setContentView()這個方法去尋根索源:

public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }
複製程式碼

getWindow().setContentView(layoutResID)這是什麼鬼,然後點進去看看:

由setContentView()方法引起的思考
我的天,竟然看到setContentView()是一個叫Window類的抽象方法,Window相信每個人都聽過,但是對於Android的Window相信不是所有人都瞭解,我也是一樣,然後我帶著問題翻閱了書本。

Window

摘自來自《Android開發藝術探索》的解釋:

Window表示一個視窗的概念,在日常開發中直接接觸Window的機會並不多,但是在某些特殊時候我們需要在桌面上顯示一個類似懸浮窗的東西,那麼這種效果就需要用到Window來實現。Window是一個抽象類,他的具體實現是PhoneWindow。建立一個Window是很簡單的事,只需要通過WindowManager即可完成。WindowManager是外界訪問Window的入口,Window的具體實現位於WindowManagerService中,WindowManager和WindowManagerService的互動是一個IPC過程。Android所有的檢視都是通過Window來呈現的,不管是Activity、Dialog還是Toast,他們的檢視實際上都是附加在Window上的,因此Window實際上是View的直接管理者。

IPC:Inter-Process Communication的縮寫,含義為程式間通訊或者跨程式通訊,是指兩個程式之間進行資料交換的過程。

看了一大輪的文字概念,我就想睡覺了。但是看了那麼久,總算幾個關鍵詞PhoneWindowWindowManagerWindowManagerService。上面講到PhoneWindow是Window的實現類,那麼我們先去看看PhoneWindow吧。

@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) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        ...
    }
複製程式碼

在PhoneWindow確實找到了setContentView()方法的具體實現。 mContentParent是什麼?

ViewGroup mContentParent;
複製程式碼

暫時還不知道它是什麼,那我們當它是null吧,進入installDecor()方法看看:

 private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            ...
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);

            // Set up decor part of UI to ignore fitsSystemWindows if appropriate.
            mDecor.makeOptionalFitsSystemWindows();

            final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
                    R.id.decor_content_parent);

            if (decorContentParent != null) {
                mDecorContentParent = decorContentParent;
                mDecorContentParent.setWindowCallback(getCallback());
                if (mDecorContentParent.getTitle() == null) {
                    mDecorContentParent.setWindowTitle(mTitle);
                }

                ...
                下面省略了一大堆UiOptions,setIcon,Transition的方法
            } else {
                ...
            }

            
    }
複製程式碼

mDecor是什麼?

private DecorView mDecor;
複製程式碼

DecorView是什麼? 書本是這樣寫的: ViewRoot對應於ViewRootImpl類,它是連線WindowManagerDecorView的紐帶,View的三大流程(onMeasure(),onLayout(),onDraw())均是通過ViewRoot來完成的。在ActivityThread中,當Activity物件被建立完畢後,會將DecorView新增到Window中,同時會建立ViewRootImpl物件,並將ViewRootImpl物件和DecorView建立關聯

我真的醉了,越翻越多自己不懂的概念出來:ViewRoot,ViewRootImpl,現在暫且做個筆記吧,先不管了。先看看我們找到的線索:

當Activity物件被建立完畢後,會將DecorView新增到Window中.

這就是我們要找的東西。DecorView原來是這樣用的。

回到installDecor()中,當mDecor為空時,呼叫generateDecor(-1)方法:

protected DecorView generateDecor(int featureId) {
        ...
        return new DecorView(context, featureId, this, getAttributes());
    }
複製程式碼

這裡建立了一個DecorView了,我們發現DecorView其實是一個FrameLayout,再回到installDecor(),這時候我們知道mContentParent仍然為null,那麼進入generateLayout(mDecor)方法:

protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.
        //根據當前設定的主題來載入預設佈局
        TypedArray a = getWindowStyle();
        ...
        設定各種各樣的屬性
        ...
        
        //如果你在theme中設定了window_windowNoTitle,則這裡會呼叫到,其他方法同理,
        //這裡是根據你在theme中的設定去設定的
        if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
            requestFeature(FEATURE_NO_TITLE);
        } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
            
            requestFeature(FEATURE_ACTION_BAR);
        }
       
        //是否有設定全屏
        if (a.getBoolean(R.styleable.Window_windowFullscreen, false)) {
            setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags()));
        }

        ...

        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 {
            // Embedded, so no decoration is needed.
            layoutResource = R.layout.screen_simple;
            // System.out.println("Simple!");
        }

        mDecor.startChanging();
        //選擇對應佈局建立新增到DecorView中
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        ...
        return contentParent;
    }
複製程式碼

首先generateLayout會根據當前使用者設定的主題去設定對應的Feature,接著,根據對應的Feature來選擇載入對應的佈局檔案,(Window.FEATURE_NO_TITLE)接下來通過getLocalFeatures來獲取你設定的feature,進而選擇載入對應的佈局,這也就是為什麼我們要在setContentView之前呼叫requesetFeature的原因。

public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
複製程式碼

我們還能看到contentParent其實是一個叫com.android.internal.R.id.content的佈局,最後新增到DecorView上。generateLayout()方法最後返回的就是contentParent。好了,installDecor()方法走完了,建立了DecorView和在上面設定了一大堆屬性,並建立帶來了一個mContentParent,再回到PhoneWindow的setContentView()中

@Override
    public void setContentView(int layoutResID) {
        
        if (mContentParent == null) {
            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 {
            //把mContentParent載入到mLayoutInflater中,
            //而mLayoutInflater在上面generateLayout(DecorView decor)方法中
            //早已載入到DecorView中
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            //回撥通知表示完成介面改變
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }
複製程式碼

此時已經建立完DecorView並且獲取到mContentParent,接著就是將你setContentView的內容新增到mContentParent中,也就是

 mLayoutInflater.inflate(layoutResID, mContentParent);
 或者
 mContentParent.addView(view, params);
複製程式碼

來到這裡該總結一下了:

由setContentView()方法引起的思考
真是千言萬語都在這圖中了,網上盜的圖真是萬能的,我的總結都在這個圖中了。 我以前一直不懂為什麼setContentView()叫setContentView而不叫setView呢,那是因為我們所建立的佈局其實是Activity裡面的PhoneWindow建立出來的DecorView裡面的ContentView來的而已。一開始以為你是老大,現在才發現你是個小弟大概就是這種感覺吧。

等等,雖然知道setContentView()方法是怎麼來的,但是在看它的原始碼中,我們還發現了好幾個疑問:WindowManagerViewRootViewRootImplPhoneWindowWindowManagerService。他們幾個的關係又是怎麼個錯綜複雜呢?拿著這些線索,我們下一篇文章再來探個究竟吧。

我的掘金: juejin.im/user/594e8e…

我的簡書: www.jianshu.com/u/b538ca57f…

相關文章