Activity當中的View是如何呈現在手機上的?含源碼分析

學習任何一門開發語言的經典入門課就是“Hello World”,Android雖然是以java爲基礎,但是也不能僅僅是在控制欄輸出"Hello World"這麼簡單就行了,我們總得在手機上跑起來,讓界面展示"Hello World"纔行,那麼我們要怎樣做呢?很簡單,新建項目這些就不用說了,新建一個佈局,添加一個android:text = "Hello World" 的TextView,通過Activity在onCreate方法中setContentView(R.layout.xxx)即可。雖然想要展示一個界面這麼簡單,但是背後的原理和流程也是需要知道的。

該篇文章源碼基於 android-28

先放一張網上關於Activity視圖層級的圖,看完這篇文章再回過頭來看這張圖:
Activity視圖層級

通過setContentView,我們就給Activity設置了佈局,佈局中寫的任何View都能展示在手機上,那我們就從這個方法開始跟蹤:

//Activity.java
public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

該方法通過getWindow()的返回值進行setContentView:

//Activity.java
public Window getWindow() {
    return mWindow;
}

返回的是一個Window對象:

/**
 * <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */
public abstract class Window {
	//。。。。省略類中的代碼,主要是看註釋
}

Window是一個抽象類,英文中註釋的意思是存在唯一的一個實現類是 android.view.PhoneWindow ,我們接着看PhoneWindow的setContentView方法:

//PhoneWindow.java
//把mContentParent的聲明放過來方便源碼閱讀
ViewGroup mContentParent;

@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();
    }

   	if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
    	 final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, getContext());
         transitionTo(newScene);
     } else {
         mLayoutInflater.inflate(layoutResID, mContentParent);
     }
    //。。。省略部分代碼
}

第一次進來 mContentParent 肯定是null,就會走第一個if判斷執行 installDecor() 方法:

//PhoneWindow.java
//聲明放過來方便源碼閱讀
private DecorView mDecor; //繼承自FrameLayout

private void installDecor() {
    mForceDecorInstall = false;
    if (mDecor == null) {
        mDecor = generateDecor(-1);
        //。。。省略代碼
    } else {
        mDecor.setWindow(this);
    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);
		//。。。省略代碼,省略的代碼是設置圖標、背景、title以及Acitivty動畫的,對我們該篇文章的分析沒有用
    }
}

有兩行代碼關係到我們的流程,一個是 mDecor = generateDecor(-1); ,另一個是 mContentParent = generateLayout(mDecor); ,我們先看 generateDecor(-1) 方法,(返回值通過上面的聲明我們可以知道是一個DecorView,它是一個繼承自FrameLayout的ViewGroup):

// PhoneWindow.java

protected DecorView generateDecor(int featureId) {
    Context context;
    if (mUseDecorContext) {
        Context applicationContext = getContext().getApplicationContext();
        if (applicationContext == null) {
            context = getContext();
        } else {
            context = new DecorContext(applicationContext, getContext());
            if (mTheme != -1) {
                context.setTheme(mTheme);
            }
        }
    } else {
        context = getContext();
    }
    return new DecorView(context, featureId, this, getAttributes());
}

這個方法最主要就是new了一個DecorView返回過去,把這個返回過去的DecorView又傳到了generateLayout(mDecor) 方法:

// PhoneWindow.java

protected ViewGroup generateLayout(DecorView decor) {
    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);
    }
    //。。。省略部分代碼
    
    int layoutResource;//注意這個layoutResource
    int features = getLocalFeatures();

    if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
        layoutResource = R.layout.screen_swipe_dismiss;
        setCloseOnSwipeEnabled(true);
    } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
        if (mIsFloating) {
            TypedValue res = new TypedValue();
            getContext().getTheme().resolveAttribute(
                    R.attr.dialogTitleIconsDecorLayout, res, true);
            layoutResource = res.resourceId;
        } else {
            layoutResource = R.layout.screen_title_icons;
        }
        // XXX Remove this once action bar supports these features.
        removeFeature(FEATURE_ACTION_BAR);
    } //。。。省略大量else if代碼
    else{//直接到最後的else分支
		layoutResource = R.layout.screen_simple;
	}

	mDecor.startChanging();
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

	//注意這個方法
	ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
	//。。。省略部分代碼

	return contentParent;

我們在這段長代碼中可以看到很多熟悉的東西,比如第一行的 TypedArray a = getWindowStyle(); ,是不是想起了我們自定義View的自定義屬性?再比如後面獲取屬性的if else 分支 R.styleable.Window_windowNoTitleR.styleable.Window_windowActionBar ,是否想到了我們在清單文件中給Application、Activity節點添加的style中寫的屬性?最後到了一個關鍵的局部變量 layoutResource,通過各種style屬性的判斷給它賦值了各種layout文件,通過查看這些佈局文件你會發現一個特點,它們都有一個id爲 @android:id/content 的FrameLayout,記住這一點,特別是這個id: @android:id/content
layoutResource 被賦值之後調用了DecorView的 onResourcesLoaded 方法,傳進去了兩個參數:mLayoutInflaterlayoutResource ,第一個參數我們都知道是LayoutInflater,用於inflate佈局的,我們看這個方法:

//DecorView.java

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
    //。。。省略部分代碼
    
    final View root = inflater.inflate(layoutResource, null);
    if (mDecorCaptionView != null) {
        if (mDecorCaptionView.getParent() == null) {
            addView(mDecorCaptionView,
                    new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        }
        mDecorCaptionView.addView(root,
                new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
    } else {

        // Put it below the color views.
        addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }
    mContentRoot = (ViewGroup) root;
    initializeElevation();
}

該方法會inflate一個view,這個view就是我們在PhoneWindow中經過大量判斷(是否有title、actionBar)後拿到的佈局文件id,返回的root被直接addView到DecorView自身了(前面可以知道它是一個ViewGroup)。

看到這裏我們可能會有疑惑了,現在我知道activity下面有PhoneWindow,PhoneWindow下有DecorView了,那我在Activity中setContentView傳入的佈局文件呢?我們回過頭看 installDecor 方法中,mContentParent = generateLayout(mDecor); 的返回值賦值給了 mContentParent (也記住這一點),而這個返回值在 generateLayout 方法中是通過 findViewById得到的:

//將這個常量的聲明放到這裏,方便閱讀
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;

ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

我們可以看到,這個常量就是R.id.content,而那個 layoutResource 的每一個賦值的佈局文件中都有定義了這個id的FrameLayout,最後我們再回到最初的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) {
        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);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}

其中的 mContentParent 就是那個id爲 @android:id/content 的FrameLayout,而 mLayoutInflater.inflate(layoutResID, mContentParent) 這一行代碼就把我們在Activity中setContentView的佈局文件inflate到了這個FrameLayout中了。現在再去看那張Activity層級視圖,是否清晰一些了呢?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章