1. 程式人生 > >從setContentView方法原始碼出發,弄懂Activity的檢視是怎麼附屬在Window上的

從setContentView方法原始碼出發,弄懂Activity的檢視是怎麼附屬在Window上的

前言

最近正在看Android開發藝術探索,看到了Window的建立過程中的Activity的建立過程,講到Activity的檢視是怎麼附屬在Window上的時候,書中分析了setContentView方法,但是說得比較簡略,一些方法也跟以前有了些改變,所以我就邊看書、邊自己找原始碼來啃。本文主要記錄了我在檢視原始碼的時候的一些理解以及書中的一些內容,因為是初學者,所以裡面可能會有一些理解錯誤,歡迎大家指出。

setContentView

由於Activity的檢視由setContentView方法提供,所以我們只需檢視該方法即可。

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

其中getWindow方法返回的是一個PhoneWindow例項

public Window getWindow() {
    return mWindow;
}
mWindow = new PhoneWindow(this, window, activityConfigCallback);

所以現在變成看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) { installDecor
(); //生成DecorView和mContentParent,主要分析該方法 } 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); //將佈局檔案轉化為View,mContentParent作為其父佈局 //也就是將Activity的檢視新增到DecorView的mContentParent中 } mContentParent.requestApplyInsets(); final Callback cb = getCallback(); //獲得當前Window的Callback介面例項 if (cb != null && !isDestroyed()) { cb.onContentChanged(); //回撥Activity的onContentChanged()方法,通知Activity檢視已經改變 } mContentParentExplicitlySet = true; }

因為是要明白Activity的檢視是怎麼附屬在Window上的,所以上面的程式碼主要分析installDecor方法mLayoutInflater.inflate(layoutResID, mContentParent);以及最後的onContentChanged回撥。

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.
    ViewGroup mContentParent;

翻譯過來就是:這是放置視窗內容的檢視。它既可以是mDecor本身,也可以是包含內容的mDecor的子項。 這裡又涉及到了mDecor,那麼我們繼續看看mDecor是什麼

    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;

可以看到mDecor是一個DecorView(DecorView是一個FrameLayout,它是Activity中的頂級View。一般來說,它包含標題欄和內容欄,內容欄就是setContentView方法傳入的佈局)

如果mContentParent為空的時候,呼叫installDecor方法,這個方法比較長,我們看一些關鍵程式碼

private void installDecor() {
	  if (mDecor == null) {
            mDecor = generateDecor(-1);		//建立DecorView,之後會分析該方法
			
			//省略一些mDecor的設定
        } else {
            mDecor.setWindow(this);		//如果已經有了DecorView,則直接在設定視窗方法中傳入PhoneWindow
        }

	if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);	//生成mContentParent,之後會分析該方法

			//以下省略一大堆程式碼
			//這堆程式碼主要處理mDecor和視窗屬性,跟mContentParent的初始化沒有多大關係
     }   
}

generateDecor

我們先分析generateDecor方法,顧名思義,該方法是用於建立DecorView(注意:呼叫該方法後,雖然建立了DecorView,但是此時的DecorView還是一個空白的FrameLayout)

protected DecorView generateDecor(int featureId) {
		// 注意:系統程序沒有應用程式上下文,在這種情況下,我們需要直接使用我們擁有的上下文。否則我們需要應用程式上下文,所以我們不依賴於活動。
        Context context;
		//先判斷是否使用decor context(只有主活動視窗使用decor context,所有其他視窗使用給予它們的context)
        if (mUseDecorContext) {
            Context applicationContext = getContext().getApplicationContext();	//獲取應用程式上下文
            if (applicationContext == null) {
                context = getContext();		//沒有應用程式上下文的情況下,我們需要直接使用我們擁有的上下文。
            } else {
                context = new DecorContext(applicationContext, getContext().getResources());
				//獲得decor view的context
                if (mTheme != -1) {
                    context.setTheme(mTheme);
                }
            }
        } else {
            context = getContext();		//其他視窗使用它們自己的context
        }
		//總的來說,上面的程式碼就是為了獲取context。只是根據是否使用decor context,獲取的方法有所不同
		
        return new DecorView(context, featureId, this, getAttributes());
		//四個引數分別是Context context, int featureId, PhoneWindow window,WindowManager.LayoutParams params
		//作用是建立DecorView並返回
    }

generateLayout

為了初始化DecorView的結構,PhoneWindow還需要通過generateLayout方法來載入具體的佈局檔案到DecorView中。 所以現在來分析generateLayout方法,由於該方法有大量程式碼,這裡只選取一些相關的程式碼

protected ViewGroup generateLayout(DecorView decor) {
	TypedArray a = getWindowStyle();	//getWindowStyle()是Window的方法,返回該視窗的style屬性
	
	//這裡有一大段程式碼,都是根據上面獲取到的window的style屬性(a變數)進行相關設定
	//例如判斷是否是dialog樣式、擴充套件螢幕功能、設定TypedValue的屬性、設定相關顏色等等

	// Inflate the window decor.
	int layoutResource;
	int features = getLocalFeatures();	
	//getLocalFeatures()是Window中的方法,用於獲取當前Window正在實現的功能

	//這裡一大段程式碼是通過判斷features,來決定layoutResource的值
	
	mDecor.startChanging();  	//開始改變DecorView
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);	//在這裡載入佈局,下面會重點看下這方法

	ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
	//這裡的id就是R.id.content

	//一堆其他操作

	mDecor.finishChanging();	//停止改變DecorView,停止後呼叫drawableChanged方法更新DeoorView

	return contentParent;
}

onResourcesLoaded

再看一下generateLayout方法中的onResourcesLoaded方法,裡面有DecorView載入佈局的具體過程

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
		
		//省略部分其他程式碼

        mDecorCaptionView = createDecorCaptionView(inflater);	//建立DecorCaptionView(裝飾標題檢視)
        final View root = inflater.inflate(layoutResource, null);	//載入傳入的layoutResource,成為根檢視
		
		//判斷DecorCaptionView是否為空
        if (mDecorCaptionView != null) {
            if (mDecorCaptionView.getParent() == null) {
                addView(mDecorCaptionView,
                        new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));	
				//如果DecorCaptionView沒有父佈局,就新增DecorCaptionView到DecorView的最後一項
            }
            mDecorCaptionView.addView(root,
                    new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
			//新增root到DecorCaptionView的最後一項
        } else {
            // Put it below the color views.
            addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
			//新增root到DecorView的第一項
        }
        mContentRoot = (ViewGroup) root;	//將root檢視作為DecorView的mContentRoot(一個ViewGroup)

    }

上面又有一個新的檢視DecorCaptionView,我們看看這又是什麼。 下面是DecorView中對於mDecorCaptionView的解釋

    // This is the caption view for the window, containing the caption and window control
    // buttons. The visibility of this decor depends on the workspace and the window type.
    // If the window type does not require such a view, this member might be null.
    DecorCaptionView mDecorCaptionView;

翻譯過來就是:這是視窗的標題檢視,包含標題和視窗控制按鈕。 這種裝飾的可見性取決於工作空間和視窗型別。如果視窗型別不需要這樣的檢視,則此成員可能為null。

也就是說當不需要這種檢視的時候,mDecorCaptionView為null,也就知道為什麼上面會有對mDecorCaptionView的判空操作了。

添加布局檔案到mContentParent

上面一層接著一層地分析原始碼的方法,其實就是為了探究初始化mContentParent,而從上面可以知道,mContentParent是在generateLayout方法中被初始化的,所以其他的就先不再分析了。

接著我們看看setContentView方法中的mLayoutInflater.inflate(layoutResID, mContentParent);

這句話比較重要,通過這條語句,Activity中的檢視就與DecorView關聯起來,因為Acitvity的佈局檔案已經新增到DecorView中了。 由此可以理解Activity的setContentView這個方法的來歷了。這個方法為什麼不叫setView呢?明明是給Activity設定檢視的啊!但是從這裡可以看出,它確實不適合叫setView,因為Activity的佈局檔案只是新增到了DecorView的mContentParent(內容部分)中,所以叫setContentView更為準確。

onContentChanged

這個過程比較簡單,就是獲得當前Window的Callback介面例項後,執行該介面的onContentChanged()方法。因為Activity實現了Window的Callback介面,所以當Activity的佈局檔案新增到了DecorView的mContentParent後,就會回撥該介面。

檢視Activity的原始碼發現,Acitvity的onContentChanged方法是一個空實現。

    public void onContentChanged() {
    }

我們可以在子Activity中重寫這個回撥方法。在Activity的佈局檔案新增到了DecorView的mContentParent後做相應的處理。

DecorView正式新增到Window中

setContentView方法完成後,DecorView已經被建立並初始化完畢,Activity的佈局檔案也已經新增到了DecorView的mContentParent中,但是此時DecorView還沒有被WindowManager正式新增到Window中。

雖然說早在Acitvity的attach方法中Window已經被建立,但這個時候由於DecorView並沒有被WindowManager識別,所以這時候Window無法提供具體功能,因為它還無法接受外界的輸入資訊。

handleResumeActivity

那麼何時DecorView才正式新增到Window中呢? 先看下ActivityThread的handleResumeActivity方法中,這裡只列出部分相關程式碼

    final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
        
		ActivityClientRecord r = mActivities.get(token);

		//此處省略程式碼

        // TODO Push resumeArgs into the activity for consideration
        r = performResumeActivity(token, clearHide, reason);	
        //這個方法主要負責呼叫Activity的onResume()方法

        if (r != null) {
            
            //此處省略程式碼

            // The window is now visible if it has been added, we are not
            // simply finishing, and we are not starting another activity.
            if (!r.activity.mFinished && willBeVisible
                    && r.activity.mDecor != null && !r.hideForNow) {
				
				//此處省略程式碼
				
				//從客戶端可見時
                if (r.activity.mVisibleFromClient) {
                    r.activity.makeVisible();	//呼叫Activity的makeVisible方法	
                }
            }
		}
    }

在該方法中,首先會呼叫Activity的onResume()方法,接著在後面會呼叫Activity的makeVisible方法。正是在makeVisible方法中,DecorView才真正地完成了新增和顯示這兩個過程。

makeVisible

    void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());	
            //新增DecorView到Window
            
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);		//顯示DecorView
    }

呼叫該方法後,Activity的檢視才能被使用者看到

總結

分析了這麼多原始碼,這裡回到主題,總結一下Activity的檢視是怎麼附屬在Window上的:

  1. 在PhoneWindow的setContentView方法初始化DecorView和mContentParent
  2. 將Activity的檢視(Activity的佈局檔案)新增到DecorView的mContentParent中
  3. 回撥Activity的onContentChanged方法,通知Activity檢視已經發生變化
  4. DecorView正式新增到Window中(通過DecorView將Activity的檢視和window聯絡起來)
  5. 設定DecorView可見後,Activity的檢視就可以被使用者所看到

參考資料: 《Android開發藝術探索》