LayoutInflater 將布局文件(XML)實例化為一個 View 對象。
通常我們會通過 Activity#getLayoutInflater()
或者是 context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)
來獲取一個標準的與當前運行的 Context 相關聯的 LayoutInflater 實例。
我們以 Activity#setContentView(@LayoutRes int layoutResID)
為例來看一下 LayoutInflater 的工作流程。
源碼:Android-29、AndroidX
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
1 AppCompatActivity#setContentView 流程
AppCompatActivity 是 AndroidX 兼容包下的 Activity 的實現基類
//#AppCompatActivity
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
可以看到內部通過代理來進行設置,下面來看代理的實現 getDelegate()
////#AppCompatActivity
@NonNull
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
@NonNull
public static AppCompatDelegate create(@NonNull Activity activity,
@Nullable AppCompatCallback callback) {
return new AppCompatDelegateImpl(activity, callback);
}
通過 getDelegate()
方法創建了代理的實現類 AppCompatDelegateImpl
,下面我們來看這個代理實現類中的 setContentView
方法:
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
可以看見,AppCompatActivity#setContentView
內部是通過 LayoutInflater.from(mContext).inflate(resId, contentParent)
來加載布局的
2 LayoutInflater.from(mContext) 流程
在來看 from(mContext)
方法之前,我們先明確 mContext 的類型。
2.1 mContext 的類型
往上翻可以看到我們在創建代理對象的同時,將 Activity 作為參數傳入,也就是說 mContext 的類型是 AppCompatActivity 也就是 ContextThemeWrapper 類型。
這里我們貼上一張 Context 相關的背景知識:
2.2 LayoutInflater.from(mContext)
/**
* Obtains the LayoutInflater from the given 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
來獲取 LayoutInflater “服務”。
之前我們已經明確了 mContext 的類型是 ContextThemeWrapper
類型,我們來看看它的 from 方法:
@Override
public Object getSystemService(String name) {
if (LAYOUT_INFLATER_SERVICE.equals(name)) {
if (mInflater == null) {
mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
}
return mInflater;
}
return getBaseContext().getSystemService(name);
}
這個的 getBaseContext 獲取到的是 mBase 這個屬性,它的類型是 ContextImpl,mBase 的賦值源于 Activity 在創建后的調用的 attach 方法,這里就不再展開了。
然后讓我們來看看 ContextImpl 的 getSystemService
方法:
@Override
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}
到這里看上去是真正要獲取服務的地方了,我們根據 ContextImpl 對象本身和服務名稱 從 SystemServiceRegistry 中獲取服務,我們可以把它理解為注冊表:
public static Object getSystemService(ContextImpl ctx, String name) {
ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
return fetcher != null ? fetcher.getService(ctx) : null;
}
跟進到它的 getSystemService 方法中可以看到:
- 從 SYSTEM_SERVICE_FETCHERS 中獲取 ServiceFetcher
- 通過 ServiceFetcher 獲取服務
服務在什么時候注冊?
在 SystemServiceRegistry 的靜態代碼塊中,對服務進行了注冊:
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
new CachedServiceFetcher<LayoutInflater>() {
@Override
public LayoutInflater createService(ContextImpl ctx) {
return new PhoneLayoutInflater(ctx.getOuterContext());
}});
可以看到最終我們獲取到的 LayoutInflater 類型是 PhoneLayoutInflater 類型。
在 ContextImpl 類中有一個 mServiceCache 屬性,在聲明的時候已經初始化:
//ContextImpl
// The system service cache for the system services that are cached per-ContextImpl.
@UnsupportedAppUsage
final Object[] mServiceCache = SystemServiceRegistry.createServiceCache();
由此可見,在 ContextImpl 創建后,服務就會進行注冊,根據注釋可知,每一個 ContextImpl 對象都有自己的服務緩存。
ServiceFetcher如何獲取服務?
我們在回過頭來看服務獲取的具體邏輯(我刪除了部分代碼):
public final T getService(ContextImpl ctx) {
final Object[] cache = ctx.mServiceCache;
final int[] gates = ctx.mServiceInitializationStateArray;
for (;;) {
boolean doInitialize = false;
synchronized (cache) {
// Return it if we already have a cached instance.
//①
T service = (T) cache[mCacheIndex];
if (service != null || gates[mCacheIndex] == ContextImpl.STATE_NOT_FOUND) {
return service;
}
if (doInitialize) {
// Only the first thread gets here.
T service = null;
try {
//②
service = createService(ctx);
newState = ContextImpl.STATE_READY;
} catch (ServiceNotFoundException e) {
onServiceNotFound(e);
} finally {
synchronized (cache) {
③
cache[mCacheIndex] = service;
gates[mCacheIndex] = newState;
cache.notifyAll();
}
}
return service;
}
}
}
- 查看 ContextImpl 的緩存,緩存命中則直接返回服務
- 緩存不存在的話則通過 createService 方法創建服務
- 最后將服務緩存在 ContextImpl 中
2.3 PhoneLayoutInflater#cloneInContext
在上面 ContextThemeWrapper 獲取服務的代碼中 mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
我們在獲取 PhoneLayoutInflater 之后,還調用了 cloneInContext 方法,名字聽上去是克隆一個對象。
public LayoutInflater cloneInContext(Context newContext) {
return new PhoneLayoutInflater(this, newContext);
}
protected PhoneLayoutInflater(LayoutInflater original, Context newContext) {
super(original, newContext);
}
protected LayoutInflater(LayoutInflater original, Context newContext) {
mContext = newContext;
mFactory = original.mFactory;
mFactory2 = original.mFactory2;
mPrivateFactory = original.mPrivateFactory;
setFilter(original.mFilter);
initPrecompiledViews();
}
由此可見我們使用了一個新的 Context 替換了原先 LayoutInflater 中的 mContext 屬性,根據上面的代碼可知,我們在使用 ContextImpl 創建了 PhoneLayoutInflater 之后,將其中的 mContext 替換為 ContextThemeWrapper。
3 LayoutInflater#inflate
LayoutInflater.from(mContext).inflate(resId, contentParent);
由上面分析可知,在獲取到 PhoneLayoutInflater 對象后,接著調用它的 inflate 方法:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, 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 方法接收三個參數:
- @LayoutRes int resource 這個參數就是我們 setContentView 方法傳入的 XML 資源文件
- @Nullable ViewGroup root 這是一個可選的父布局容器
- boolean attachToRoot ,這個參數決定是否將從 XML 加載的 View 對象添加進 root 中
根據 setContentView 的調用可知,我們傳入的布局文件最后會被添加進 id 為 Content 的容器中
在 inflate 方法中我們創建了當前 XML 資源文件的解析器,并將它傳入重載的 inflate 房中:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
View result = root;
try {
//① merge 標簽判斷
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
...
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// ②創建 XMl 文件中頂層標簽中標記的 View 對象
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
// ③指定 LayoutParams
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);
}
}
//④遞歸的將子視圖填充到 temp 中
context.rInflateChildren(parser, temp, attrs, true);
//⑤將根據 XML 實例化的 View 添加到 root 中
if (root != null && attachToRoot) {
root.addView(temp, params);
}
//直接返回 temp 對象
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
....
}
return result;
}
}
這里的方法有點長,我同樣省略一些代碼,讓我們的視線集中在布局的解析上。
3.1 merge 標簽
我們判斷 XMl 的根標簽是不是 merge,如果是 merge 標記的話在直接調用遞歸填充方法 rInflater
,這里我們先不看對 merge 標簽的處理。
3.2 LayoutInflater#createViewFromTag
如果不是 merge 標簽的話,我們將調用 createViewFromTag
方法來創建 View 對象:
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
//...
try {
View view = tryCreateView(parent, name, context, attrs);
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(context, parent, name, attrs);
} else {
view = createView(context, name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
} catch (InflateException e) {
//...
}
}
這里也是分為三步:
- 嘗試調用
tryCreateView
方法創建 View - 如果不是自定義 View 則調用 onCreateView 方法
- 如果是自定義 View 則調用 createView
3.2.1 LayoutInflater#tryCreateView
public final View tryCreateView(@Nullable View parent, @NonNull String name,
@NonNull Context context,
@NonNull AttributeSet attrs) {
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
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);
}
return view;
}
- 如果 XML 的標記是 blink(TAG_1995) 的話,會創建一個 BlinkLayout 類型的對象,它會每隔 500ms 重繪一次,形成閃爍的效果。
- 嘗試通過 mFactory2 對象的 onCreateView 來創建 View 對象
- 嘗試通過 mFactory 對象的 onCreateView 來創建 View 對象
- 嘗試通過 mPrivateFactory 對象的 onCreateView 來創建 View 對象
mFactory2、mPrivateFactory 對象都是 Factory2 類型,而 mFactory 對象是 Factory 類型,Factory2 是對 Factory 接口的升級,Factory2 繼承了 Factory 接口,并且多了一個 View onCreateView(View parent, String name,Context context, @NonNull AttributeSet attrs);
相比較于 Factory 的 onCreateView 而言多了一個 View 類型的 parent 參數。
稍后我們再來看這幾個屬性是在何時被設置的,讓我們回到 createViewFromTag 方法
3.2.2 LayoutInflater#createView
如果我們沒有設置 mFactory2 這些屬性,那么 tryOnCreateView 這個方法返回的 View 對象就是 null,之后我們會根據 XML 標記是否包含 "." 來決定調用不同的方法。
如果標簽不包含 "." ,說明這是 Android 系統提供的 View 例如:TextView,ImageView...,這時我們會調用 onCreateView 方法,這個方法會調用一些列的重載方法,最后會調用 createView(String name, String prefix, AttributeSet attrs)
方法:
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Context context = (Context) mConstructorArgs[0];
if (context == null) {
context = mContext;
}
return createView(context, name, prefix, attrs);
}
在 onCreateView 方法當中,我們會調用 createView 方法,并且限定了 prefix 這個入參為 "android.view",緊接著我們會調用最后重載的 createView 方法:
public final View createView(@NonNull Context viewContext, @NonNull String name,
@Nullable String prefix, @Nullable AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(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
clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
// If we have a filter, apply it to cached constructor
...
}
try {
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;
} finally {
mConstructorArgs[0] = lastContext;
}
}
...
}
我省略一些異常捕獲和日志代碼,可以看到我們會嘗試從緩存中獲取當前 View 的構造函數,如果命中緩存的話,則直接通過反射創建 View 對象并返回,否則我們會通過入參 name 和 prefix 來拼接 View 的全路徑類名,然后在通過反射獲取它的構造函數,放入緩存之后在創建 View 返回。
到這里我們可以說 LayoutInflater 是通過反射的方式將 XML 實例化成一個 View 對象,這么說沒錯,但是之前我們有提到過,在反射創建 View 對象之前,會經過 mFactory 等對象的處理,接下來讓我們肯看這些“工廠“是什么時候設置的。
4 LayoutInflater#setFactory2
之前我們分析的是 AppCompatActivity#setContentView 流程:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
在 setContentView 之前我們調用了父類的 onCreate 方法:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
super.onCreate(savedInstanceState);
}
這里可以看到我們調用了 delegate 對象的 installViewFactory 方法:
//AppCompatDelegateImpl
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
其中 LayoutInflaterCompat.setFactory2(layoutInflater, this);
對 mFactory 進行了設置:
//LayoutInflaterCompat
class AppCompatDelegateImpl extends AppCompatDelegate
implements MenuBuilder.Callback, LayoutInflater.Factory2{
//......
}
public static void setFactory2(
@NonNull LayoutInflater inflater, @NonNull LayoutInflater.Factory2 factory) {
inflater.setFactory2(factory);
//省略了兼容性代碼......
}
AppCompatDelegateImpl 實現了 Factory2 接口,我們通過 setFactory2 方法將 this 賦值給 mFactory 和 mFactory2 對象,下面我們來看 Factory2 接口在 AppCompatDelegateImpl 中的實現:
@Override
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
return createView(parent, name, context, attrs);
}
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
//省略部分代碼......
mAppCompatViewInflater = new AppCompatViewInflater();
return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
IS_PRE_LOLLIPOP, true, VectorEnabledTintResources.shouldBeUsed()
);
}
可以看到在 createView 方法中,我們構建了一個 AppCompatViewInflater 對象,將創建 View 的操作交給了它,接著我們看這個對象的 createView 方法:
final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
final Context originalContext = context;
// We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
// by using the parent's context
if (inheritContext && parent != null) {
context = parent.getContext();
}
if (readAndroidTheme || readAppTheme) {
// We then apply the theme on the context, if specified
context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
}
if (wrapContext) {
context = TintContextWrapper.wrap(context);
}
View view = null;
// We need to 'inject' our tint aware Views in place of the standard framework versions
switch (name) {
case "TextView":
view = createTextView(context, attrs);
verifyNotNull(view, name);
break;
case "ImageView":
view = createImageView(context, attrs);
verifyNotNull(view, name);
break;
// 省略了后續代碼...
}
protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
return new AppCompatTextView(context, attrs);
}
- 可以看見,為了能夠兼容,使得 android 5.0 之前的版本能夠使用矢量圖功能,我們會對originalContext 進行包裝,通過 TintContextWrapper 的
wrapper
方法將它包裝成 TintContextWrapper 類型 - 通過字符串匹配,直接使用 new 對象的方式,創建相對應的 View,節省了反射帶來的開銷
由此可見,我們雖然我們在 XML 中聲明的是 TextView 類型,但是為了向前兼容,系統實際創建的是 AppCompatTextView 類型;另外通過 View#getContext 方法獲取到的類型也不一定是 Activity 類型,也有可能是 TintContextWrapper 類型。
5 LayoutInflater#setPrivateFactory
經過上面的流程分析,我們已經知道 mFactory 和 mFactory2 屬性是什么時候被賦值的,那么 mPrivateFactory 又是什么時候被賦值的呢?
/**
* @hide for use by framework
*/
@UnsupportedAppUsage
public void setPrivateFactory(Factory2 factory) {
if (mPrivateFactory == null) {
mPrivateFactory = factory;
} else {
mPrivateFactory = new FactoryMerger(factory, factory, mPrivateFactory, mPrivateFactory);
}
}
我們可以通過 LayoutInflater#setPrivateFactory
方法設置 mPrivateFactory,注釋中說這是個隱藏方法,被 framework 層使用,這里給大家一個看源碼的網站,就是谷歌最近剛開源的 Code Serach,我們通過這個網站來查看一下
setPrivateFactory 方法的引用。
如圖,我們在 Activity attach 方法調用的時候通過 mWindow.getLayoutInflater().setPrivateFactory(this);
對 mPrivateFactory 進行設置,并且設置的值是 this,說明我們 Activity 也實現了 Factory2 接口。
下面我們來看 Factory2 接口在 Activity 中的實現:
public View onCreateView(@Nullable View parent, @NonNull String name,
@NonNull Context context, @NonNull AttributeSet attrs) {
if (!"fragment".equals(name)) {
return onCreateView(name, context, attrs);
}
return mFragments.onCreateView(parent, name, context, attrs);
}
可以看到這里對 XML 中的 fragment 標記進行了處理,FragmentController#onCreateView 方法會對 Fragment 進行實例化,并調用 Fragment 實現的 onCreateView 方法來返回 View 對象。
到此為止我們就知道了 XML 中的 fragment 標簽是如何被處理的。
6. final
Android 系統通過給 LayoutInflater 設置工廠的方式,自己決定 View 的實例化,以此來實現向前兼容,利用 setFactory 方法我們還可以做到很多事情,比如全局字體的替換,給特定的 View 設置特定的背景...
同時我也只對最基礎的流程進行分析,里面對 merge、include、viewStub 等標簽的處理并沒有展開,其實這些標簽也只是遞歸的進行 View 的創建并添加進容器而已。
參考鏈接: