LayoutInflater是什麼?
LayoutInflater是Android系統的一個服務,我們可以通過它,把佈局文件動態生成View,實現View視圖的動態添加。
LayoutInflater在Android日常開發工作中經常使用到,並且我們經常調用的Activity的setContentView方法,它的內部實現就用到了LayoutInflater。
LayoutInflater對象的獲取方式
我們可以通過多種方式來調用LayoutInflater服務。
方法一:LayoutInflater.from(context)
我們可以通過LayoutInflater.from(context)方法來返回LayoutInflater對象,來看源碼:
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;
}
邏輯解析:
- 這裏可以看到,LayoutInflater確實是一個系統服務,服務名稱是Context.LAYOUT_INFLATER_SERVICE,可以通過context.getSystemService來獲得。
- 如果獲取失敗,則拋出錯誤。
方法二:通過context.getSystemService方法來獲取
其實方法1只是方法2的簡單封裝而已,這裏我們直接通過getSystemService來獲取:
LayoutInflater layoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
方法三:在Activity中,調用getLayoutInflater方法
這個方法最簡單,我們可以直接在Activity中調用getLayoutInflater方法來獲取。
Activity的getLayoutInflater方法源碼:
public LayoutInflater getLayoutInflater() {
return getWindow().getLayoutInflater();
}
邏輯解析:
- 這裏直接返回的是getWindow()的getLayoutInflater()方法。
- getWindow()返回的是Activity所對應的PhoneWindow對象。
- PhoneWindow對象,在初始化時,通過LayoutInflater.from(context)獲取了LayoutInflater對象。
關於Activity顯示以及內部視圖的創建等邏輯,我們會在今後的文章中進行源碼分析。
LayoutInflater的使用
我們獲取LayoutInflater實例之後,通過它的inflate方法來使佈局生效。
layoutInflater.inflate(R.layout.activity_main, viewGroup);
示例中,使用layoutInflater.inflate將佈局R.layout.activity_main作爲子視圖添加到viewGroup視圖容器中。
接下來我們來分析inflate方法的源碼實現。
LayoutInflater佈局解析分析
LayoutInflater的inflate方法負責解析佈局並生成View對象,我們接下來分析它的源碼實現。
來看inflate方法:
frameworks/base/core/java/android/view/LayoutInflater.java
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
return inflate(parser, root, root != null);
}
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) + ")");
}
View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
if (view != null) {
return view;
}
XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
邏輯解析:
- 可以看到inflate方法有多個方法重載。
- 首先嚐試從預編譯緩存中獲取View對象,如果成功,則直接返回,這裏的預編譯是什麼,稍後我們介紹。
- 如果預編譯中獲取失敗,則調用res的getLayout方法獲取XML的資源解析器,這裏用的是PULL解析器(前一章中有介紹)。
- 最後調用inflate方法進行解析。
tryInflatePrecompiled方法(預編譯選項)
tryInflatePrecompiled是Android 10(Android Q)中新增的方法,用來根據佈局文件的xml預編譯生成dex,然後通過反射來生成對應的View,從而減少XmlPullParser解析Xml的時間。它是一個編譯優化選項,我們下面來分析。
private @Nullable
View tryInflatePrecompiled(@LayoutRes int resource, Resources res, @Nullable ViewGroup root,
boolean attachToRoot) {
//如果mUseCompiledView是false,則表示預編譯優化開個沒有打開,直接返回
if (!mUseCompiledView) {
return null;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate (precompiled)");
// 獲得佈局資源對應的packageName和layout名稱
String pkg = res.getResourcePackageName(resource);
String layout = res.getResourceEntryName(resource);
try {
//獲得 "包名" + ".CompiledView"的類
Class clazz = Class.forName("" + pkg + ".CompiledView", false, mPrecompiledClassLoader);
//獲得以layout名稱命名的方法
Method inflater = clazz.getMethod(layout, Context.class, int.class);
//執行該方法,返回View對象。
View view = (View) inflater.invoke(null, mContext, resource);
//最後進行View的佈局設置並根據條件判斷是否添加到父視圖中
if (view != null && root != null) {
// We were able to use the precompiled inflater, but now we need to do some work to
// attach the view to the root correctly.
XmlResourceParser parser = res.getLayout(resource);
try {
AttributeSet attrs = Xml.asAttributeSet(parser);
advanceToRootNode(parser);
ViewGroup.LayoutParams params = root.generateLayoutParams(attrs);
if (attachToRoot) {
root.addView(view, params);
} else {
view.setLayoutParams(params);
}
} finally {
parser.close();
}
}
return view;
} catch (Throwable e) {
if (DEBUG) {
Log.e(TAG, "Failed to use precompiled view", e);
}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return null;
}
邏輯解析:
- 首先根據mUseCompiledView判斷預編譯是否是開啓狀態,如果mUseCompiledView是false,則表示預編譯優化開個沒有打開,直接返回。
- 獲得佈局資源對應的packageName和layout名稱。
- 預加載編譯資源,存儲在以"包名" + ".CompiledView"的類中,並且佈局資源對應了一個以layout名爲名稱的方法。
- 使用反射調用,執行該方法,就會返回一個資源佈局對應的View視圖對象。
- 最後進行View的佈局設置並根據條件判斷是否添加到父視圖中。
預編譯選項開關
我們來看預編譯選項的開關,mUseCompiledView變量是何時設置的。
private boolean mUseCompiledView;
private void initPrecompiledViews() {
// Precompiled layouts are not supported in this release.
boolean enabled = false;
initPrecompiledViews(enabled);
}
private void initPrecompiledViews(boolean enablePrecompiledViews) {
mUseCompiledView = enablePrecompiledViews;
if (!mUseCompiledView) {
mPrecompiledClassLoader = null;
return;
}
」
它是在initPrecompiledViews方法中進行設置的,其中無參的默認爲false,表示不開啓。
我們來看initPrecompiledViews調用的地方:
protected LayoutInflater(Context context) {
mContext = context;
initPrecompiledViews();
}
/**
* @hide for use by CTS tests
*/
@TestApi
public void setPrecompiledLayoutsEnabledForTesting(boolean enablePrecompiledLayouts) {
initPrecompiledViews(enablePrecompiledLayouts);
}
可以看到,系統只調用了initPrecompiledViews()無參的方法,表示默認關閉預編譯優化。帶參數的initPrecompiledViews方法只在內部測試時使用。
inflate方法
我們繼續來看inflate方法:
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);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
//將給定的解析器推進到第一個開始標記
advanceToRootNode(parser);
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
//如果根標籤是merge標籤
if (TAG_MERGE.equals(name)) {
//如果根節點是merge,並且root是null或者attachToRoot == false,則拋出異常。
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
//遍歷佈局並生成View
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// 創建根標籤的View對象
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
//遍歷佈局文件,生成所有子View
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// 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);
}
//如果父容器root是空,或者attachToRoot==false,則直接返回佈局文件的根View。
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(
getParserStateDescription(inflaterContext, attrs)
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}
通過分析我們可以看到,該方法實現了佈局文件生成View的過程。createViewFromTag()方法實現了佈局文件根View的創建,rInflateChildren(parser, temp, attrs, true)會調用rInflate()方法,遞歸實例化子View,過程中也會使用createViewFromTag()方法創建具體的子View。
rInflate和rInflateChildren方法都是通過遞歸解析xml中的佈局,創建View,並添加到parent中,邏輯比較簡單我們就不做具體分析了。
參數含義及注意事項
我們從以上分析中可以總結出該方法及參數使用的幾個特點:
- 參數root爲默認父視圖,如果爲null,表示佈局文件生成的View不會添加到默認視圖中。attachToRoot屬性將沒有意義。
- 參數root如果不爲null,參數attachToRoot表示是否添加到父視圖中
- 當 attachToRoot爲true時,則會把佈局View添加到root中,作爲root的子視圖。
- 當 attachToRoot爲false時,不會把佈局View添加到默認父視圖root中,但是會把layout參數設置給佈局View,當佈局View被添加到父view當中時,這些layout屬性會自動生效。
- 當佈局文件根節點是merge標籤時,root必須不爲null,並且attachToRoot必須爲true,否則就會拋出InflateException異常。
- attachToRoot默認值,當root != null時爲true,root == null時爲false。
總結
- LayoutInflater是Android系統的一個服務,我們可以通過它,把佈局文件動態生成View,實現View視圖的動態添加。
- Activity的setContentView方法,它的內部實現就用到了LayoutInflater。
- 可以通過多種方式來調用LayoutInflater:LayoutInflater.from(context)、context.getSystemService方法、在Activity中,調用getLayoutInflater方法。其實本質上都是獲取LayoutInflater系統服務。
- LayoutInflater的inflate方法負責解析佈局並生成View對象。首先嚐試從預編譯緩存中獲取View對象,如果成功,則直接返回;如果預編譯中獲取失敗,則會使用PULL解析器進行解析。
- 預編譯優化是Android Q的新增邏輯,默認是關閉的。
- rInflate和rInflateChildren方法都是通過遞歸解析xml中的佈局,創建View,並添加到parent中。
LayoutInflater生成View的參數含義如下:
- 參數root爲默認父視圖,如果爲null,表示佈局文件生成的View不會添加到默認視圖中。attachToRoot屬性將沒有意義。
- 參數root如果不爲null,參數attachToRoot表示是否添加到父視圖中
- 當 attachToRoot爲true時,則會把佈局View添加到root中,作爲root的子視圖。
- 當 attachToRoot爲false時,不會把佈局View添加到默認父視圖root中,但是會把layout參數設置給佈局View,當佈局View被添加到父view當中時,這些layout屬性會自動生效。
- 當佈局文件根節點是merge標籤時,root必須不爲null,並且attachToRoot必須爲true,否則就會拋出InflateException異常。
- attachToRoot默認值,當root != null時爲true,root == null時爲false。