LayoutInflater 載入佈局檔案原理,過程分析
工作之餘,研究了研究,寫了一個外掛換膚的小框架,準備這段時間寫兩三篇文章做一下總結,有問題的話歡迎批評指正,因為侵入式外掛換膚的框架中有涉及到LayoutInflater載入佈局檔案的相關知識,所以,本篇文章先針對LayoutInflater載入佈局的過程以及原理做一些分析,網上也有不少的相關的文章,也感謝優秀的Android大牛們的分享,為我們平時的開發以及學習提供了很好的借鑑。
話不多說,下面開始:
文章的重點會放在LayoutInflater載入佈局檔案的原理以及過程上,這裡稍微提一下LayoutInflater的獲取方式:
方式一:通過Context的getSystemService(String name)方法,熟悉Context的朋友應該知道,Context是一個抽象類,它有兩個實現類ContextWrapper和ContextImpl,我們來看下ContextWrapper的getSystemService()方法
@Override
public Object getSystemService(String name) {
return mBase.getSystemService(name);
}
我們可以看到,該方法內部呼叫了mBase.getSystemService()的方法,這個mBase是ContextImpl類的物件,在這裡我就不解釋為什麼是ContextImpl類的物件了,因為這不是本文的重點,考慮以後有時間寫一篇專門介紹Context的文章,在這裡知道就行了。我們來看下ContextImpl類的getSystemService()方法:
@Override public Object getSystemService(String name) { return SystemServiceRegistry.getSystemService(this, name); }
我用android sdk版本是26,不同的sdk版本ContextImpl中getSystemService()方法的實現可能不太一樣,我們再來看一下SystemServiceRegistery.getSystemService()方法的原始碼
public static Object getSystemService(ContextImpl ctx, String name) { ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name); return fetcher != null ? fetcher.getService(ctx) : null; }
不管使用的sdk版本是多少,但是最終應該都會呼叫到上述程式碼來獲取LayoutInflater物件,這裡簡單說一下,SYSTEM_SERVICE_FETCHERS是一個HasMap物件,儲存了很多的服務 ,準確的說,是儲存了很多服務的獲取器,就是ServiceFetcher,通過獲取器的getService方法,最終獲取到對應的服務,除了LayoutInflater之外,還有WindowManager、ActivityManager等等。
方法二:通過LayoutInflater.from(Context context)來獲取,我們來看下該方法的程式碼
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
我們可以看到該方法最終還是呼叫了Context的getSystemService(String name)方法來獲取物件
方法三:如果是在Activity中,可以通過Activity的getLayoutInflater方法獲取,我們來看下該方法的程式碼
public LayoutInflater getLayoutInflater() {
return getWindow().getLayoutInflater();
}
getWindow()獲得一個Window的物件,Window的實現類時PhoneWindow,我們來看下PnhoneWindow的getLayoutInflater()方法
@Override
public LayoutInflater getLayoutInflater() {
return mLayoutInflater;
}
該方法直接return了一個LayoutInflater的物件,該物件是在PhoneWindow的構造方法裡建立的
public PhoneWindow(Context context) {
super(context);
mLayoutInflater = LayoutInflater.from(context);
}
可以看到在PhoneWindow的構造方法裡面,是通過LayoutInflater.from(Context)來獲得的一個LayoutInflater物件
因此上述三種方法最終都是通過Context.getSystemService()來獲得的LayoutInflater物件。
下面我們來看一下LayoutInflater是怎麼把xml檔案轉換成View的:
入口是LayoutInflater的inflate()方法,inflater有四個不同的實現方法
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root)
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
public View inflate(XmlPullParser parser, @Nullable ViewGroup root)
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
下面我們以public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) 為例,來分析一下LayoutInflater把xml檔案轉換成View的原理和過程
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
我們可以看到在該方法的內部又呼叫了另一個三個引數的inflate方法
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
在該方法裡通過Resources的getLayout(Resource)方法獲得一個XmlResourceParser物件,用該物件作為引數呼叫另一個inflate()方法,在這裡我稍微解釋一下XmlResourceParser和Resources的getLayout()方法:
public interface XmlResourceParser extends XmlPullParser, AttributeSet, AutoCloseable {
/**
* Close this interface to the resource. Calls on the interface are no
* longer value after this call.
*/
public void close();
}
XmlResourceParser可以理解成Xml檔案的直譯器,是一個介面,定義了一系列的解析xml檔案的API
接下來我們來看下Resources的getLayout()方法是怎麼獲取到XmlResourceParser物件的
public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException {
return loadXmlResourceParser(id, "layout");
}
getLayout()方法呼叫了loadXmlResourceParser()方法
1 @NonNull
2 XmlResourceParser loadXmlResourceParser(@AnyRes int id, @NonNull String type)
3 throws NotFoundException {
4 final TypedValue value = obtainTempTypedValue();
5 try {
6 final ResourcesImpl impl = mResourcesImpl;
7 impl.getValue(id, value, true);
8 if (value.type == TypedValue.TYPE_STRING) {
9 return impl.loadXmlResourceParser(value.string.toString(), id,
10 value.assetCookie, type);
11 }
12 throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
13 + " type #0x" + Integer.toHexString(value.type) + " is not valid");
14 } finally {
15 releaseTempTypedValue(value);
16 }
17 }
由於loadXmlResourceParser方法的程式碼量略大,加上了行號方便解釋,在該方法的第6行中,獲得了一個ResourcesImpl物件impl,在第9行通過呼叫impl.loadXmlResourceParser()來獲取一個XmlResourceParser
1 @NonNull
2 XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int
3 assetCookie,@NonNull String type) throws NotFoundException {
4 if (id != 0) {
5 try {
6 synchronized (mCachedXmlBlocks) {
7 final int[] cachedXmlBlockCookies = mCachedXmlBlockCookies;
8 final String[] cachedXmlBlockFiles = mCachedXmlBlockFiles;
9 final XmlBlock[] cachedXmlBlocks = mCachedXmlBlocks;
10 // First see if this block is in our cache.
11 final int num = cachedXmlBlockFiles.length;
12 for (int i = 0; i < num; i++) {
/*
*如果快取中存在符合條件的XmlBlock,就通過呼叫快取的XmlBlock物件的
* newParser()方法來獲取XmlResourceParser物件
*/
13 if (cachedXmlBlockCookies[i] == assetCookie &&
14 cachedXmlBlockFiles[i] != null
15 && cachedXmlBlockFiles[i].equals(file)) {
16 return cachedXmlBlocks[i].newParser();
17 }
18 }
19
20 // Not in the cache, create a new block and put it at
21 // the next slot in the cache.
//通過AssetManager物件的openXmlBlockAsset方法獲得一個XmlBlock物件
22 final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file);
23 if (block != null) {
24 final int pos = (mLastCachedXmlBlockIndex + 1) % num;
25 mLastCachedXmlBlockIndex = pos;
26 final XmlBlock oldBlock = cachedXmlBlocks[pos];
27 if (oldBlock != null) {
28 oldBlock.close();
29 }
30 cachedXmlBlockCookies[pos] = assetCookie;
31 cachedXmlBlockFiles[pos] = file;
32 cachedXmlBlocks[pos] = block;
//呼叫XmlBlock物件的newParser()方法來獲得XmlResourceParser物件
33 return block.newParser();
34 }
35 }
36 } catch (Exception e) {
37 final NotFoundException rnf = new NotFoundException("File " + file
38 + " from xml type " + type + " resource ID #0x" +
39 Integer.toHexString(id));
40 rnf.initCause(e);
41 throw rnf;
42 }
43 }
44
45 throw new NotFoundException("File " + file + " from xml type " + type + "
46 resource ID #0x"
47 + Integer.toHexString(id));
48 }
上述程式碼中的中文部分是我加的註釋,基本上可以清楚,在loadXmlResourceParser()方法中,如果快取中有XmlBlock物件就從快取中獲取,否則的話就通過呼叫AssetManager中的openXmlBlockAsset()方法來獲得一個XmlBlock物件,最終都會呼叫XmlBlock物件的newParser()方法來獲得一個XmlResourceParser物件,具體newParser()方法中程式碼邏輯是怎樣的,我就不再往下追溯了,有興趣的朋友可以自己研究一下,這樣的話在inflate()方法中就通過Resources的getLayout方法獲得了一個XmlResourceParser物件,好,下面我們回到上文提到的inflate()方法中來,在獲得XmlResourceParser物件之後,把XmlResourceParser的物件作為引數,呼叫另一個inflate()方法,程式碼如下:
1 public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean
2 attachToRoot) {
3 synchronized (mConstructorArgs) {
4 Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
5 final Context inflaterContext = mContext;
//XmlResourceParser多繼承了XmlPullParser、AttributeSet等介面
//把XmlPullParser物件強轉成AttributeSet物件
6 final AttributeSet attrs = Xml.asAttributeSet(parser);
7 Context lastContext = (Context) mConstructorArgs[0];
8 mConstructorArgs[0] = inflaterContext;
9 View result = root;
10 try {
11 // Look for the root node.
12 int type;
//該while迴圈的作用應該是尋找 整個佈局檔案的根節點
13 while ((type = parser.next()) != XmlPullParser.START_TAG &&
14 type != XmlPullParser.END_DOCUMENT) {
15 // Empty
16 }
//如果沒有找到根節點,就會報錯
17 if (type != XmlPullParser.START_TAG) {
18 throw new InflateException(parser.getPositionDescription()
19 + ": No start tag found!");
20 }
//獲取根節點的名稱
21 final String name = parser.getName();
22 if (DEBUG) {
23 System.out.println("**************************");
24 System.out.println("Creating root view: "
25 + name);
26 System.out.println("**************************");
27 }
//如果根節點是merge標籤
28 if (TAG_MERGE.equals(name)) {
/*如果根節點是merge標籤
*但是父View,即root為null,或不新增到root中,即attachToRoot為false
*會報錯
*/
29 if (root == null || !attachToRoot) {
30 throw new InflateException("<merge /> can be used only
31 with a valid "
32 + "ViewGroup root and attachToRoot=true");
33 }
//遞迴生成子節點
34 rInflate(parser, root, inflaterContext, attrs, false);
35 } else {
36 // Temp is the root view that was found in the xml
// 通過createViewFromTag生成根節點View
37 final View temp = createViewFromTag(root, name,
38 inflaterContext, attrs);
39 ViewGroup.LayoutParams params = null;
40 if (root != null) {
41 if (DEBUG) {
42 System.out.println("Creating params from root: " +
43 root);
44 }
45 // Create layout params that match root, if supplied
/*
* 呼叫父View即root的generateLayoutParams()方法
* 生成根節點View的LayoutParams
*/
46 params = root.generateLayoutParams(attrs);
47 if (!attachToRoot) {
48 // Set the layout params for temp if we are not
49 // attaching. (If we are, we use addView, below)
/*
* 如果不需要將根節點View新增到父View中,
* 則對根節點View自身設定LayoutParams
*/
50 temp.setLayoutParams(params);
51 }
52 }
53 if (DEBUG) {
54 System.out.println("-----> start inflating children");
55 }
56 // Inflate all children under temp against its context.
//解析根節點下的子節點
57 rInflateChildren(parser, temp, attrs, true);
58
59 if (DEBUG) {
60 System.out.println("-----> done inflating children");
61 }
62 // We are supposed to attach all the views we found (int
temp)
63 // to root. Do that now.
64 if (root != null && attachToRoot) {
/*
* 如果父View不為null,並且根節點需要新增到父View中
* 則呼叫root的addView方法新增根節點的View
*/
65 root.addView(temp, params);
66 }
67 // Decide whether to return the root that was passed in or
the
68 // top view found in xml.
69 if (root == null || !attachToRoot) {
//如果父View=root為null或者attachToRoot為false則返回根節點
70 result = temp;
71 }
72 }
73 } catch (XmlPullParserException e) {
74 final InflateException ie = new InflateException(e.getMessage(), 75 e);
76 ie.setStackTrace(EMPTY_STACK_TRACE);
77 throw ie;
78 } catch (Exception e) {
79 final InflateException ie = new
80 InflateException(parser.getPositionDescription()
81 + ": " + e.getMessage(), e);
82 ie.setStackTrace(EMPTY_STACK_TRACE);
83 throw ie;
84 } finally {
85 // Don't retain static reference on context.
86 mConstructorArgs[0] = lastContext;
87 mConstructorArgs[1] = null;
88 Trace.traceEnd(Trace.TRACE_TAG_VIEW);
89 }
90 return result;
91 }
92 }
關於通過inflate()方法載入View的整個過程,上述程式碼中我已經在比較重要的地方加了中文註釋,稍微再一點,在上述程式碼的第46行,當root不為null時,會呼叫root的generateLayoutParams()方法生成一個LayoutParams物件,generateLayoutParams()是ViewGroup中定義的方法,不同的子類有不同的實現方式,當root != null && attachToRoot == true的時候,會在上述程式碼的第64行呼叫addView()方法,把節點的View新增到父View中。
下面我們來考慮一種場景,假如root代表的父View是FrameLayout,在需要載入的xml檔案中定義了android:layout_centerHorizontal屬性,該屬性將會失效,原因就是FrameLayout.LayoutParam中並不支援layout_centerHorizontal屬性,同理當父View是其他的LinearLayout、RelativeLayout等View的時候,如果xml檔案中有不支援的屬性,也會出現失效的問題。
當傳入的root即父View為null時,xml檔案中以“layout_”開頭的屬性都會消失,因為沒有父View root生成其對應的LayoutParams,如果出入的root為空,根節點定義的寬和高將會失效,因為當一個根節點沒有設定LayoutParams的時候,預設的LayoutParams將會是wrap_content。
下面我們來看下在上述inflate()方法中比較重要的幾個方法rInflate(),rInflateChildren(),以及createViewFromTag()
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
我們可以看到在rInflateChidlren()方法內部,也是呼叫了rInflate()方法,下面我們首先來看下載入子節點View的rInflate()方法
1 void rInflate(XmlPullParser parser, View parent, Context context,
2 AttributeSet attrs, boolean finishInflate) throws
3 XmlPullParserException, IOException {
//獲取View樹的深度
4 final int depth = parser.getDepth();
5 int type;
//當前節點不是尾節點的時候,才會去執行while迴圈內部的邏輯
6 while (((type = parser.next()) != XmlPullParser.END_TAG ||
7 parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT)
8 {
9 if (type != XmlPullParser.START_TAG) {
10 continue;
11 }
//獲取節點的名稱
12 final String name = parser.getName();
13 if (TAG_REQUEST_FOCUS.equals(name)) {
14 parseRequestFocus(parser, parent);
15 } else if (TAG_TAG.equals(name)) {
16 parseViewTag(parser, parent, attrs);
17 } else if (TAG_INCLUDE.equals(name)) {
//當節點的標籤是inlcude時候,執行以下操作
18 if (parser.getDepth() == 0) {
19 throw new InflateException("<include /> cannot be the root
20 element");
21 }
//解析include的xml檔案
22 parseInclude(parser, context, parent, attrs);
23 } else if (TAG_MERGE.equals(name)) {
24 throw new InflateException("<merge /> must be the root
25 element");
26 } else {
//呼叫createViewFromTag()建立節點View
27 final View view = createViewFromTag(parent, name, context,
28 attrs);
29 final ViewGroup viewGroup = (ViewGroup) parent;
//呼叫generateLayoutParams()建立節點View的LayoutParams
30 final ViewGroup.LayoutParams params =
31 viewGroup.generateLayoutParams(attrs);
32 rInflateChildren(parser, view, attrs, true);
33 viewGroup.addView(view, params);
34 }
35 }
36 if (finishInflate) {
37 parent.onFinishInflate();
38 }
39 }
我們可以看到,在rInflate()方法中,最終也是會呼叫createViewFromTag()建立節點的View,然後再呼叫rInflateChildren()去遍歷子節點View,最終呼叫父View的addView()方法新增到父View中,到現在為止我們基本能瞭解rInflate()是怎麼遍歷子節點並且建立的View的了,下面我們來看下createViewFromTag()方法是怎麼建立View的,在對createViewFrom()方法分析的過程中,某些地方也是外掛外掛換膚所要掌握的,下面我們來看下createViewFromTag()方法:
1 View createViewFromTag(View parent, String name, Context context, AttributeSet
2 attrs,boolean ignoreThemeAttr) {
3 //省略部分不重要的程式碼,重點說一下後面View建立的過程
4 ......
5 ......
6 ......
7 try {
8 View view;
/*
* Factory2和Factory是兩個介面,
* 如果我們自己在程式碼中設定了mFactory2或者mFactory,
* 在createViewFromTag()方法中建立View的時候就會呼叫mFactory2或者mFactory
* 的onCrateView()方法,通過我們自己實現的onCreateView()來建立View
* 侵入式的外掛換膚框架,雖然邏輯有所不同,原理上基本都是自己設定了mFactory2,
* 實現了onCreateView,用來hook View的建立過程,標記需要被換膚的View
*/
9 if (mFactory2 != null) {
10 view = mFactory2.onCreateView(parent, name, context, attrs);
11 } else if (mFactory != null) {
12 view = mFactory.onCreateView(name, context, attrs);
13 } else {
14 view = null;
15 }
16 if (view == null && mPrivateFactory != null) {
17 view = mPrivateFactory.onCreateView(parent, name, context,
18 attrs);
19 }
/*
* 如果沒有設定mFactory2和mFactory,或者最後return null的話,
* 就會執行以下程式碼,最終通過反射的方式構建View
*/
20 if (view == null) {
21 final Object lastContext = mConstructorArgs[0];
22 mConstructorArgs[0] = context;
23 try {
24 if (-1 == name.indexOf('.')) {
//onCreateView()最終也是呼叫了27行的createView()方法
25 view = onCreateView(parent, name, attrs);
26 } else {
//通過createView()方法來建立View
27 view = createView(name, null, attrs);
28 }
29 } finally {
30 mConstructorArgs[0] = lastContext;
31 }
32 }
33 return view;
34 } catch (InflateException e) {
35 throw e;
36 } catch (ClassNotFoundException e) {
37 final InflateException ie = new
38 InflateException(attrs.getPositionDescription()
39 + ": Error inflating class " + name, e);
40 ie.setStackTrace(EMPTY_STACK_TRACE);
41 throw ie;
42 } catch (Exception e) {
43 final InflateException ie = new
44 InflateException(attrs.getPositionDescription()
45 + ": Error inflating class " + name, e);
46 ie.setStackTrace(EMPTY_STACK_TRACE);
47 throw ie;
48 }
49 }
上面程式碼的中文描述還算比較清楚了,在此我再多提一下,在侵入式外掛換膚在標記需要換膚的View的時候,都是通過setFactory2()方法設定mFactory2,重寫onCreateView()方法,hook住View的建立過程,從而標記需要換膚的View。在我後面介紹侵入式外掛換膚的文章裡,也會提到這塊。下面我們來看下上述程式碼27行中的createView()方法
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
//從快取中通過name獲取View的構造方法
Constructor<? extends View> constructor = sConstructorMap.get(name);
......
Class<? extends View> clazz = null;
try {
......
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
/*
* 當獲取的構造方法為null時候,表示還未快取過該View的構造方法
* 通過ClassLoader的loadClass()載入View對應的class
*/
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
......
//反射獲取View的構造方法,儲存在Map裡
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
......
}
Object lastContext = mConstructorArgs[0];
if (mConstructorArgs[0] == null) {
// Fill in the context if not already within inflation.
mConstructorArgs[0] = mContext;
}
Object[] args = mConstructorArgs;
args[1] = attrs;
/*
* mConstructorArgs是一個長度為2的陣列,
* 把 mConstructorArgs賦值給args,作為引數呼叫constructor構造方法,
* 從而建立View物件
*/
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]));
}
mConstructorArgs[0] = lastContext;
return view;
} catch (NoSuchMethodException e) {
......
} catch (ClassCastException e) {
......
} catch (ClassNotFoundException e) {
......
} catch (Exception e) {
......
} finally {
......
}
}
通過上述的程式碼可以看出,最終建立View的時候是呼叫的View的兩個引數的構造方法,這也解釋了為什麼我們在自定義View的時候,如果沒有重寫兩個引數的構造方法的話,就會報錯。
好了,基本上LayoutInflater載入xml檔案的整個過程就分析完畢了。!
本人從事工作剛過一年,有些地方可能認識的也沒那麼深刻,大家如果發現什麼問題歡迎批評指正,共同進步,多謝!!!