Android 布局加載之LayoutInflater
Activity 在界面創建時需要將 XML 布局文件中的內容加載進來,正如我們在 ListView 或者 RecyclerView 中需要將 Item 的布局加載進來一樣,都是使用 LayoutInflater 來進行操作的。
LayoutInflater 實例的獲取有多種方式,但最終是通過 (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) 來得到的,也就是說加載布局的 LayoutInflater 是來自于系統服務的。
由于 Android 系統源碼中關于 Content 部分采用的是裝飾模式,Context 的具體功能都是由 ContextImpl 來實現的。通過在 ContextImpl 中找到 getSystemService 的代碼,一路跟進,得知最后返回的實例是 PhoneLayoutInflater 。
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
new CachedServiceFetcher<LayoutInflater>() {
@Override
public LayoutInflater createService(ContextImpl ctx) {
return new PhoneLayoutInflater(ctx.getOuterContext());
}});
LayoutInflater 只是一個抽象類,而 PhoneLayoutInflater 才是具體的實現類。
inflate 方法加載 View
使用 LayoutInflater 時常用方法就是 inflate 方法了,將一個布局文件 ID 傳入并最后解析成一個 View 。
LayoutInflater 加載布局的 inflate 方法也有多種重載形式:
View inflate(@LayoutRes int resource, @Nullable ViewGroup root)
View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
而這兩者的差別就在于是否要將 resource 布局文件加載到 root 布局中去。
不過有點需要注意的地方,若 root 為 null,則在 xml 布局中為 resource 設置的屬性會失效,只是單純的加載布局。
// temp 是 xml 布局中的頂層 View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) { // root
// root 不為 null 才會生成 layoutParams
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// 如果不添加到 root 中,則直接把布局參數設置給 temp
temp.setLayoutParams(params);
}
}
// 加載子 View
rInflateChildren(parser, temp, attrs, true);
if (root != null && attachToRoot) {
root.addView(temp, params);//添加到布局中,則布局參數用到 addView 中去
}
if (root == null || !attachToRoot) {
result = temp;
}
跟進 createViewFromTag 方法查看 View 是如何創建出來的。
View view; // 最后要返回的 View
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs); // 是否設置了 Factory2
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs); // 是否設置了 Factory
} else {
view = null;
}
if (view == null && mPrivateFactory != null) { // 是否設置了 PrivateFactory
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) { // 如果的 Factory 都沒有設置過,最后在生成 View
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) { // 系統控件
view = onCreateView(parent, name, attrs);
} else { // 非系統控件,自定義的 View
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
如果設置過 Factory 接口,那么將由 Factory 中的 onCreateView 方法來生成 View 。
關于 LayoutInflater.Factory 的作用,就是用來在加載布局時可以自行去創建 View,搶在系統創建 View 之前去創建。
關于 LayoutInflater.Factory 的使用場景,現在比較多的就是應用的換膚了。
若沒有設置過 Factory 接口,則是判斷是否為自定義控件或者系統控件,不管是 onCreateView 方法還是 createView 方法,內部最終都是調用到了 createView 方法,通過它來生成 View 。
// 通過反射生成 View 的參數,分別是 Context 和 AttributeSet 類
static final Class<?>[] mConstructorSignature = new Class[] {
Context.class, AttributeSet.class};
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;
if (constructor == null) { // 從緩存中得到 View 的構造器,沒有則調用 getConstructor
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);
sConstructorMap.put(name, constructor);
} else {
// If we have a filter, apply it to cached constructor
if (mFilter != null) { // 過濾,是否允許生成該 View
// 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); // 不允許生成該 View
}
}
}
Object[] args = mConstructorArgs;
args[1] = attrs;
final View view = constructor.newInstance(args); // 通過反射生成 View
return view;
在 createView 方法內部,首先從 View 的構造器緩存中查找是否有對應的緩存,若沒有則生成構造器并且放到緩存中去,若有構造器則看能否通過過濾,是否允許該 View 生成。
最后都滿足條件的則是通過 View 的構造器反射生成了 View 。
在生成 View 時采用 Constructor.newInstance 調用構造函數,而參數所需要的變量就是 mConstructorSignature 變量所定義的,分別是 Context 和 AttributeSet 。可以看到,在最后生成 View 時也傳入了對應的參數。
采用 Constructor.newInstance 的形式反射生成 View ,是為了解耦,只需要有了類名,就可以加載出來。
由此可見,LayoutInflater 加載布局仍然是需要傳遞 Context 的,不光是為了得到 LayoutInflater ,在反射生成 View 時同樣會用到。
深度遍歷加載布局
如果需要加載的布局只有一個控件,那么 LayoutInflater 返回那個 View 工作也就結束了。
若布局文件中有多個需要加載的 View ,則通過 rInflateChildren 方法繼續加載頂層 View 下的 View ,最后通過 rInflate 方法來加載。
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
// 若 while 條件不成立,則加載結束了
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName(); // 從 XmlPullParser 中得到 name 出來解析
if (TAG_REQUEST_FOCUS.equals(name)) { // name 各種情況下的解析
parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true); // 繼續遍歷
viewGroup.addView(view, params); // 頂層 View 添加 子 View
}
}
if (finishInflate) { // 遍歷解析
parent.onFinishInflate();
}
}
rInflate 方法首先判斷是否解析結束了,若沒有,則從 XmlPullParser 中加載出下一個 View 進行處理,中間還會對不同的類型進行處理,比如 TAG_REQUEST_FOCUS 、 TAG_TAG 、 TAG_INCLUDE 、 TAG_MERGE 等等。
最后仍然還是通過 createViewFromTag 來生成 View ,并以這個生成的 View 為父節點,開始深度遍歷,繼續調用 rInflateChildren 方法加載布局,并把這個 View 加入到它的父 View 中去。
至于為什么生成 View 的方法名字 createViewFromTag 從字面上來看是來自于 Tag 標簽,想必是和 XmlPullParser 解析布局生成的內容有關。
參考
1、http://www.sunnyang.com/661.html?utm_source=tuicool&utm_medium=referral
2、http://blog.csdn.net/lmj623565791/article/details/51503977
3、https://segmentfault.com/a/1190000003813755
4、http://blog.csdn.net/panda1234lee/article/details/9009719
來自:http://www.glumes.com/android-layoutinflater/