Android-setContentView與findViewById原始碼解析
原創-轉載請註明出處。
當我們給Activity設定佈局時,都是直接呼叫setContentView來完成的,但具體Android是怎麼把佈局載入到window,又是怎麼通過findViewById獲取view物件的,我們可能並沒有太關心,下面就結合原始碼來分析下這個過程。
Android setContentView
開啟Activity的原始碼發現,setContentView有三個過載方法,
- public void setContentView(int layoutResID);
- public void setContentView(View view);
- public void setContentView(View view, ViewGroup.LayoutParams params)
我們就來看下最常用的第一個方法:
public void setContentView(int layoutResID) { getWindow().setContentView(layoutResID); initWindowDecorActionBar(); }
這個方法呼叫了,Window類中的setContentView()方法,其他方法也是呼叫了Window類中的setContentView(),但是Window是一個抽象類,在Activity的attach方法中被初始化,其實是一個PhoneWindow例項,所以這個setContentView方法在PhoneWindow中實現。
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); } final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } }
首先判斷mContentParent是否為空,如果為空的話則呼叫installDecor()方法,其次判斷是否設定了FEATURE_CONTENT_TRANSITIONS屬性,如果沒有的話則移除所有view(從這裡我們可以得出setContentView可以呼叫多次,反正會removeAllViews),然後呼叫LayoutInflater.inflate(),將我們設定的佈局檔案新增到mContentParent中。接著獲取了一個Callback物件,那這個是在Activity的attach方法中設定的一個回撥
mWindow.setCallback(this);
所以可以得出在Activity中一定有一個onContentChanged回撥,我們來看下這個回撥
public void onContentChanged() {}
額,空空如也。但是我們可以在自己的Activity中重寫這個回撥,用於在setContentView之後做一些事情,比如findViewById,但貌似實際場景也不需要。。。
好了,現在我們回到上面提到的installDecor()方法,好長,我們撿重要的看吧。
private void installDecor() { //初始化decorView if (mDecor == null) { mDecor = generateDecor(); mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); mDecor.setIsRootNamespace(true); if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) { mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); } } //初始化mContentParent if (mContentParent == null) { mContentParent = generateLayout(mDecor); ...... //設定一堆屬性值 } }
看下PhoneWindow中的generateDecor()方法
protected DecorView generateDecor() { return new DecorView(getContext(), -1); }
只是單純的new了一個DecorView例項。這個DecorView是什麼鬼。其實它是PhoneWindow的一個內部類,是整個window介面最頂層的view。包含ActionBar,內容塊等。好了,現在我們縷一下Window,PhoneWindow,decorView的關係
1.Window類是一個抽象類,提供了繪製視窗的一組通用API。可以將之理解為一個載體,各種View在這個載體上顯示。
2.PhoneWindow是Window的一個子類,是Window的具體實現,包含一個內部類DecorView,PhoneWindow是將decorView進行了一定包裝,並提供一些方法用於操作視窗。
3。DecorView繼承自FrameLayout,是視窗的根view。
好了,接著看mContentParent的初始化,generateLayout(mDecor).這裡傳入了上一部初始化好的DecorView. 又是一個長方法,我們還是挑出重要的部分。
protected ViewGroup generateLayout(DecorView decor) { // Apply data from current theme. TypedArray a = getWindowStyle(); //...... //根據定義的style設定一些值,比如是否顯示ActionBar, // Inflate the window decor. int layoutResource; int features = getLocalFeatures(); //...... //根據設定好的features值選擇不同的視窗修飾佈局檔案, //得到layoutResource值,系統定義了不同的layout,比如 //R.layout.screen_custom_title,R.layout.screen_simple //把選中的視窗修飾佈局檔案新增到DecorView物件裡,並且指定contentParent值 View in = mLayoutInflater.inflate(layoutResource, null); decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); mContentRoot = (ViewGroup) in; ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); if (contentParent == null) { throw new RuntimeException("Window couldn't find content container view"); } //...... //繼續一堆屬性設定,返回contentParent return contentParent; }
根據不同的features值,設定layoutResource,最終新增到decorView中,所以我們通過在xml中設定的theme,還有在程式碼中設定的requestWindowFeature,都是用來設定features值,這也是為什麼requestWindowFeature方法必須在setContentView之前的原因。
這樣看來,如果我們設定我們的Theme為NoTitleBar,最終layoutResource的值為R.layout.screen_simple
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" android:orientation="vertical"> <ViewStub android:id="@+id/action_mode_bar_stub" android:inflatedId="@+id/action_mode_bar" android:layout="@layout/action_mode_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="?attr/actionBarTheme" /> <FrameLayout android:id="@android:id/content" android:layout_width="match_parent" android:layout_height="match_parent" android:foregroundInsidePadding="false" android:foregroundGravity="fill_horizontal|top" android:foreground="?android:attr/windowContentOverlay" /> </LinearLayout>
來看下去處標題欄後的檢視樹

setContentView
所以installDecor主要是初始化了PhoneWindow中的DecorView.和contentParent,之後在setContentView()中通過mLayoutInflater.inflate(layoutResID, mContentParent);將layoutResId,add到初始化好的contentParent中。
大家是否好奇狀態列怎麼被載入進DecorView的,我們來看下DecorView中的updateColorViewInt方法
private View updateColorViewInt(View view, int sysUiVis, int systemUiHideFlag, int translucentFlag, int color, int height, int verticalGravity, String transitionName, int id, boolean hiddenByWindowFlag) { ...... if (view == null) { if (show) { view = new View(mContext); view.setBackgroundColor(color); view.setTransitionName(transitionName); view.setId(id); addView(view, new LayoutParams(LayoutParams.MATCH_PARENT, height, Gravity.START | verticalGravity)); } } else { ...... } return view; }
可以看到直接new了一個view,這個view就是狀態列,然後將狀態列新增到了DecorView,其實這個狀態列只是一個單純的佔位view。被updateColorViews方法呼叫,比如當我們呼叫setStatusBarColor時就是呼叫了updateColorViews這個方法。這裡先不做過多介紹。
findViewById
那麼將layout新增進decorView中後,我們是怎麼通過findViewById找到View的呢?
看下Activity的findViewById方法
/** * Finds a view that was identified by the id attribute from the XML that * was processed in {@link #onCreate}. * * @return The view if found or null otherwise. */ public View findViewById(int id) { return getWindow().findViewById(id); }
又是到了window中,看下window中的方法
public View findViewById(int id) { return getDecorView().findViewById(id); }
是呼叫了getDecorView的findViewById,也就是呼叫了view的findViewById,我們來看下view類中
public final View findViewById(int id) { if (id < 0) { return null; } return findViewTraversal(id); } protected View findViewTraversal(int id) { if (id == mID) { return this; } return null; }
到這我們就疑惑了,直接判斷了id是否為view的id,是的話就返回。怎麼也應該有一個迴圈或者遞迴查詢啊,什麼都沒有。
這時我們來看下,mID是怎麼初始化的
.... case com.android.internal.R.styleable.View_id: mID = a.getResourceId(attr, NO_ID); break; ...
喔,這個id就是我們在xml中設定的id。那會不會在ViewGroup中進行查詢的呢?來看下
protected View findViewTraversal(int id) { if (id == mID) { return this; } final View[] where = mChildren; final int len = mChildrenCount; for (int i = 0; i < len; i++) { View v = where[i]; if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) { v = v.findViewById(id); if (v != null) { return v; } } } return null; }
果然, ViewGroup重寫了View的findViewTraversal()方法,遍歷了自己的child的findViewById方法,如果找到了返回View自身。
ok,到現在我們就理解了view是怎麼findViewById的了。
總結
上面我們介紹了,Activity setContentView和findViewById的流程,是不是又多了一層理解呢,喜歡的話就點個贊吧~