Android-如何把Layout和Activity建立起聯絡
從事Android開發也有一段兒時間了,在工作中大問題小問題都遇到過,不管是在網上找到方法還是自己看文件琢磨,反正最後都解決了,但是從來沒有記錄過,這是很悲哀的,因為腦容量是有限的,所以在工作中經常會出現這種情況,看到一個棘手問題後可能會有印象,我解決過該問題,但是當時是怎麼解決的,思路是什麼,從哪兒獲取的靈感等等都不記得了,這又無形的給自己的工作帶來麻煩,因此在學習Android的同時做一些必要的記錄是很重要的,本文是一篇反思,由於先前已經瞭解過,但是現在又忘了,所以在此把最初的學習複習一遍,順便把複習的結果記錄一下。
本文從最基礎的是Activity著手去研究怎樣把Xml檔案和Activity建立起聯絡展現到手機上。
一、問題
1. Xml檔案是佈局基礎,但是它是怎麼樣和Activity建立聯絡的,作為檢視展示到手機螢幕上的?
2. findViewById()是怎樣找到對應的Xml檔案中的元素並把Xml檔案中的元素展示成一個View?
3. 怎樣把一個Xml檔案解析成View展示出來的呢?
二、幾個關鍵的物件
DecorView mDecor;// This is the top-level view of the window, containing the window decor.
ViewGroup 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 mContentRoot;// This is the view in which the window contents
LayoutInflater mLayoutInflater;//
三、Window展示檢視的結構
在手機上展示出來的內容結構是下圖中這樣的,在最外層有一個頂級容器DecorView,然後是我們的內容的根檢視mContentRoot(ViewGroup),然後才是我們Xml或者new出來的View。
四、從原始碼瞭解
從setContentView(layoutResID)著手,一般我們設定Activity的Layout時都是通過該方法設定對應的layoutId,然後把layoutId對應的Xml檔案解析成我們看到的檢視介面,所以入手點就是我們熟知並且使用過千百遍的setContentView(layoutResID),先看一下原始碼:
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) {//step1
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);//step2
}
……
}
這段程式碼就是本文的入口,從step1開始分析,先判斷mContentParent是否是空的,如果是空的則執行installDecor(),初始化後第一次開啟頁面mContentParent肯定是空的,所以執行installDecor()方法,先不用管對應的else if判斷條件中的內容,這不是我們要了解的重點,那麼接下來看一下installDecor()是幹什麼的呢:
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor();
……
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
……
}
}
這個方法的比較長,大部分是和本文的主題不相關的,關鍵的也就那麼幾行,去掉不重要的程式碼讓我們的思路更清晰。只要找準這幾個關鍵的地方就可以明白這個所表達的真正含義了,其它的都是附屬品。從方法名的字面意思可以看出這個方法的目的就是install展示內容的Decor(DecorView),這就是我們在目錄二中提到的關鍵物件之一,這個物件是做什麼的呢,它就是手機上看到的應用檢視的頂級View,所有的在手機上呈現出來的view的頂級容器,它繼承自FrameLayout,每一個開啟的手機視窗首先都是有一個頂級的容器來裝載我們要展示的內容。從第一個if語句開始,如果mDecor是null則mDecor = generateDecor(),generateDecor()的目的是生成一個沒有feature的DecorView。再看第二個if語句,它的目的是生成mContentParent,也是目錄二中提到的關鍵物件之一(它是這是視窗內容被放置的檢視,它可以是mDecor本身,也可以是一個子mDecor的內容,這裡就要視情況而論了,當作為子view(inflate一個view的時候)就是mDecor本身),這個方法的內容也是非常多,關鍵的內容也還是那麼幾行,其它的都是針對設定的feature做相應的配置資訊,例如,actionbar、floatWindow等。
/**
* The ID that the main layout in the XML layout file should have.
*/
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
protected ViewGroup generateLayout(DecorView decor) {
……
mDecor.startChanging();
……
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
// ID_ANDROID_CONTENT是com.android.internal.R.id.content,這就是內容展示的主要view
……
return contentParent;
}
這樣1千多行的程式碼就被很好的分解了,得到希望看到的內容,剔除掉和目的不相關的干擾項剩下的就是真相。這個過程大體就可以清楚了,通過findViewById找到google定義的一個內部view,賦給contentParent作為返回內容,然後再回到setContentView(layoutResID)中看關鍵程式碼:
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);// 這是重點
}
繞了半天終於用到了我們最關心的一個變數layoutResID,這一句程式碼是不是很熟悉,在使用listview的時候,getView中常會用到的或者使用fragment時常用到的,當然還有很多地方我們都會用到,例如,here。
接下來,檢視方法inflate:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
然後再進入到方法inflate(resource, root, root != null):
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
……
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
現在離我們的目的已經不遠了,其實已經很明瞭了,就是通過一個Xml解析器解析我們的Xml檔案,然後返回解析後的View,我們繼續往下看:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");//記錄解析日誌
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);//通過parser中得到layout中的所有view的屬性集儲存在attrs中
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
// Look for the root node.
……
final String name = parser.getName();//得到layout的節點name,例如,view、merge、include等
……
if (TAG_MERGE.equals(name)) {//這裡忽略,先不研究merge
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else { // 忽略merge後的入口 entrence
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) { // note1
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
……
} catch (Exception e) {
……
} finally {
……
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
return result;
}
}
在這個方法中,通過resource在parser解析出layout中所有元素的屬性然後放在變數attrs中,然後在上述程式碼紅色標記的entrence處呼叫createViewFromTag方法根據attrs屬性集中的屬性創建出對應的view,到這兒,基本上已經可以大概知道view建立的流程,為了更詳細的去了解過程,我們有必要看剩下最後一個關鍵的方法createViewFromTag(root, name, inflaterContext, attrs):
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
……
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
……
}
這裡就是把從Xml中解析出來的內容根據變數name生成對應的View物件,其實後面的實現不用看原始碼也可以想到了,用反射生成對應的View物件,然後一級一級的向來時的路返回給呼叫方法。但是為了證實我們的猜測還是要仔細的研究一番,先看第一個if語句,很簡單,就是通過工廠去建立View,Activity實現了介面Factory2,在Activity原始碼中可以看到具體實現,進入Activity檢視原始碼:
Factory2:
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if (!"fragment".equals(name)) {
return onCreateView(name, context, attrs);
}
return mFragments.onCreateView(parent, name, context, attrs);
}
Factory:
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
可以看到如果我們沒有使用fragment,則最後返回的都是null,那麼再回到createViewFromTag方法中繼續看下面的程式碼,mPrivateFactory也是Factory2的一個物件,所以還是一樣的看Activity中程式碼,得出同樣的結果返回null,這樣的話,真正建立View的程式碼就是通過第三個if語句實現的,找到關鍵地方try包裹的程式碼,view = onCreateView(parent, name, attrs)和view = createView(name, null, attrs)兩個方法最終實現都會呼叫createView(String name, String prefix, AttributeSet attrs),這樣我們就可以找到源頭了:
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Constructor<? extends View> constructor = sConstructorMap.get(name);
Class<? extends View> clazz = null;
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
}
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
// 這裡是view快取
sConstructorMap.put(name, constructor);
} else {
// If we have a filter, apply it to cached constructor
if (mFilter != null) {
// Have we seen this name before?
Boolean allowedState = mFilterMap.get(name);
if (allowedState == null) {
// New class -- remember whether it is allowed
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, attrs);
}
}
}
Object[] args = mConstructorArgs;
args[1] = attrs;
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// Use the same context when inflating ViewStub later.
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
return view;
……
}
這樣就證實我們的猜想,確實是通過反射來建立View,然後我們的任務也就完成了。需要注意的是,通過反射建立的View物件返回的都是View型別的物件,在使用時需要強制轉換。可以總結為:在activity中指定的layoutId去找到對應的Xml檔案,然後通過Xml解析生成對應的View然後inflate到視窗頂級容器DecorView中繪製展現出來。
回到我們的目錄二問題中,1和3都已經清楚了,那麼問題2是什麼樣的結果呢,其實這個很簡單,在建立View的時候已經從Xml檔案中解析到完整的view屬性attrs,在使用反射建立view時會通過建構函式生成對應的物件,所以會用到View的構造方法,在View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)方法中有一句程式碼 mID = a.getResourceId(attr, NO_ID),這樣就可以從attribute中把解析到的ID放在mID變數中,然後在findViewByID(id)中,根據引數id返回對應的View,需要注意的是,在findViewById時要用到findViewTraversal(@IdRes int id)方法,在這裡如果指定find的範圍(比如,在FrameLayout中去找),則使用ViewGroup中的findViewTraversal(@IdRes int id)方法,先獲取到ViewGroup的所有子View,然後通過遍歷子View找到對應的View並返回,檢視下面程式碼。
protected View findViewTraversal(@IdRes int id) {
if (id == mID) {//如果等於當前ViewGroup的Id,則返回該ViewGroup
return this;
}
final View[] where = mChildren;
final int len = mChildrenCount;
for (int i = 0; i < len; i++) {//其它情況則遍歷所有子View,返回對應的View
View v = where[i];
if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
v = v.findViewById(id);
if (v != null) {
return v;
}
}
}
return null;
}
note1:記得在使用listView的時候的最後一個boolean引數時,看到很多人都有使用該變數,也沒去詳細瞭解,只是習慣性的去使用。在note1標記處給出了合理的解釋,大概大概意思是如果我們沒有為temp設定params則使用setLayoutParams(temp.setLayoutParams(params))方法設定,如果設定過則使用addView方法為temp新增params(root.addView(temp, params))。