UI 優(yōu)化系列專題,來聊一聊 Android 渲染相關(guān)知識,主要涉及 UI 渲染背景知識、如何優(yōu)化 UI 渲染兩部分內(nèi)容。
UI 優(yōu)化系列專題
- UI 渲染背景知識
《View 繪制流程之 setContentView() 到底做了什么腕唧?》
《View 繪制流程之 DecorView 添加至窗口的過程》
《深入 Activity 三部曲(3)View 繪制流程》
《Android 之 LayoutInflater 全面解析》
《關(guān)于渲染,你需要了解什么瘾英?》
《Android 之 Choreographer 詳細(xì)分析》
- 如何優(yōu)化 UI 渲染
《Android 之如何優(yōu)化 UI 渲染(上)》
《Android 之如何優(yōu)化 UI 渲染(下)》
對于 LayoutInflater枣接,相信每個 Android 開發(fā)人員都不會感到陌生。業(yè)界一般稱它為布局解析器(或填充器)缺谴,翻開 LayoutInflater 源碼發(fā)現(xiàn)它是一個抽象類但惶,我們先來看下它的自我介紹。
LayoutInflater 就是將 XML 布局文件實(shí)例化為對應(yīng)的 View 對象湿蛔,LayoutInflater 不能直接通過 new 的方式獲取膀曾,需要通過 Activity.getLayoutInflater() 或 Context.getSystemService() 獲取與當(dāng)前 Context 已經(jīng)關(guān)聯(lián)且正確配置的標(biāo)準(zhǔn) LayoutInflater。
也就是說 LayoutInflater 不能被外部實(shí)例化阳啥,只能通過系統(tǒng)提供的固有方式獲取添谊,但也正因如此,相信很多開發(fā)人員對它的認(rèn)識仍然停留在如下代碼:
final View content = LayoutInflater.from(this).inflate(R.layout.content, root, false);
今天我們就從源碼的角度察迟,進(jìn)一步分析 LayoutInflater 的工作原理斩狱,主要涉及到如下幾塊兒內(nèi)容:
- LayoutInflater 創(chuàng)建過程與實(shí)際類型
- LayoutInflater 的布局解析原理
- 不容忽視的 View 創(chuàng)建耗時
- LayoutInflater 的高階使用技巧
LayoutInflater 創(chuàng)建過程與實(shí)際類型
系統(tǒng)在 Context 中默認(rèn)提供的兩種獲取 LayoutInflater 的方式如下:
final LayoutInflater getLayoutInflater = getLayoutInflater();
final LayoutInflater getSystemServiceInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
不過它們與直接使用 LayoutInflater.from() 除了 API 不同并沒有本質(zhì)上的差異耳高,所以我們直接從 LayoutInflater 的 from 方法開始入手:
public static LayoutInflater from(Context context) {
// 這里的context可以是Activity、Application所踊、Service泌枪。
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
注意參數(shù) Context,這里可以是 Activity秕岛、Application 或 Service碌燕。不知大家是否有跟蹤過它們之間有什么區(qū)別嗎?接下類我們就重點(diǎn)分析下這部分內(nèi)容瓣蛀。
Context 的 getSystemService 方法默認(rèn)是抽象的,看下它在 Context 中的聲明:
public abstract @Nullable String getSystemServiceName(@NonNull Class<?> serviceClass);
這里我們主要以 Activity 為例(其他類型也會在此引申出)雷厂,在 Activity 的直接父類 ContextThemeWrapper 中重寫了 getSystemService 方法惋增。
@Override
public Object getSystemService(String name) {
if (LAYOUT_INFLATER_SERVICE.equals(name)) {
if (mInflater == null) {
// 每個Activity都有自己獨(dú)一無二的Layoutflater
// 這里首先拿到在SystemServiceRegistry中注冊的Application的Layoutflater
// 然后根據(jù)該創(chuàng)建屬于每個Activity的PhoneLayoutInflater
mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
}
return mInflater;
}
return getBaseContext().getSystemService(name);
}
可以看到,Activity 對于 LayoutInflater 服務(wù)的 LAYOUT_INFLATER_SERVICE 做了單獨(dú)處理改鲫,使每個 Activity 都有其獨(dú)立的 LayoutInflater诈皿。否則直接通過 getBaseContext().getSystemService() 獲取相關(guān)服務(wù)。
這里像棘,我們有必要先跟蹤下 getBaseContext()稽亏,它的聲明在 ContextThemeWrapper 的直接父類 ContextWrapper 中,如下:
public Context getBaseContext() {
// mBase的實(shí)際類型是ContextImpl
return mBase;
}
注意:ContextWrapper 是 Application 和 Service 的直接父類
mBase 的實(shí)際類型是 ContextImpl缕题。在 Android 中截歉,Application、Service 和 Activity 在創(chuàng)建后會首先回調(diào)其 attach 方法烟零,并在該方法為其關(guān)聯(lián)一個 ContextImpl 對象(該部分源碼可以參考 Activity / Application 的創(chuàng)建過程在 ActivityThread 中)瘪松。
故,這里的 getSystemService() 實(shí)際調(diào)用到 ContextImpl 的 getSystemService 方法:
@Override
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}
可以看到在 ContextImpl 內(nèi)部锨阿,又委托給了 SystemServiceRegistry宵睦。SystemServiceRegistry 是應(yīng)用進(jìn)程的系統(tǒng)服務(wù)注冊機(jī),在其內(nèi)部的靜態(tài)代碼塊中默認(rèn)注冊了大量系統(tǒng)服務(wù)墅诡,包括 WINDOW_SERVICE壳嚎、LOCATION_SERVICE 、AUDIO_SERVICE 等等末早,這里我們重點(diǎn)看下 LayoutInflater 的注冊過程:
static {
// ... 省略
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
new CachedServiceFetcher<LayoutInflater>() {
@Override
public LayoutInflater createService(ContextImpl ctx) {
// LayoutInflater 的實(shí)際類型是PhoneLayoutInflater
return new PhoneLayoutInflater(ctx.getOuterContext());
}}
);
// ... 省略
}
然后通過 SystemServiceRegistry 的 getSystemService 方法獲取相關(guān)服務(wù)過程如下:
public static Object getSystemService(ContextImpl ctx, String name) {
// 根據(jù)name在SYSTEM_SERVICE_FETCHERS獲取該服務(wù)類型的ServiceFetcher
ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
// 通過該Fetcher獲取對應(yīng)的服務(wù)類型烟馅,如果是第一次調(diào)用fetcher.getServie()
// 會調(diào)用其內(nèi)部的createSercice()
return fetcher != null ? fetcher.getService(ctx) : null;
}
SYSTEM_SERVICE_FETCHERS 是一個靜態(tài)的 Map 容器,上面在 static {} 代碼塊中注冊的服務(wù)都將保存在該容器然磷。這里首先根據(jù)服務(wù)的 name 獲取對應(yīng)的 Fetcher焙糟,然后通過該 Fetcher 的 getService 方法創(chuàng)建相應(yīng) LayoutInflater 對象。
當(dāng)我們首次獲取某個服務(wù)類型時样屠,fetcher.getService() 會執(zhí)行其內(nèi)部的 createService() 創(chuàng)建對應(yīng)服務(wù)穿撮,然后每個服務(wù)都會被保存在 SystemServiceRegistry 中缺脉,這里實(shí)際間接保存在 Fetcher 中。
在 createService 方法悦穿,我們發(fā)現(xiàn) LayoutInflater 的實(shí)際類型是 PhoneLayoutInflater攻礼,類定義如下:
public class PhoneLayoutInflater extends LayoutInflater {
/**
* 系統(tǒng)默認(rèn) View 目錄
*/
private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};
/**
* Application 級的 LayoutInflater 使用該構(gòu)造方法,就是在
* SystemServiceRegistry 的靜態(tài)代碼款中注冊的栗柒。
*/
public PhoneLayoutInflater(Context context) {
super(context);
}
/**
* 在Activity中使用LayoutInflater.form()時候調(diào)用該構(gòu)造方法
*/
protected PhoneLayoutInflater(LayoutInflater original, Context newContext) {
super(original, newContext);
}
/**
* 默認(rèn)View 的創(chuàng)建流程這里(非自定義控件)
*/
@Override
protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
for (String prefix : sClassPrefixList) {
try {
// 首先查找:android/widget目錄下礁扮,如 Seekbar
// 然后查找:android/webkit目錄下,如 WebView
// 最后查找:android/app目錄下瞬沦,如 ActionBar
View view = createView(name, prefix, attrs);
if (view != null) {
return view;
}
} catch (ClassNotFoundException e) {}
}
// 如果以上都不能滿足太伊,就是:android/view 目錄下
return super.onCreateView(name, attrs);
}
/**
* newContext是具體的Activity
*/
public LayoutInflater cloneInContext(Context newContext) {
// 每個Activity都由其獨(dú)一無二的 Layoutflater
return new PhoneLayoutInflater(this, newContext);
}
}
注意 PhoneLayoutInflater 重寫了 LayoutInflater 的 onCreateView 方法,這在后面 View 的創(chuàng)建階段將會分析到逛钻。
注意觀察 PhoneLayoutInflater 的最后 cloneInContext()僚焦,重新回到上面 ContextThemeWrapper 的 getSystemService 方法,大家是否注意到通過 LayoutInflater.from() 獲取到 LayoutInflater 對象后曙痘,又調(diào)用其 cloneInContext 方法芳悲,該方法實(shí)際調(diào)用到 PhoneLayoutInflater 的 cloneInContext 方法。
此時會為每個 Activity 單獨(dú)創(chuàng)建一個 LayoutInflater边坤。之所以叫做 “clone”名扛,是因?yàn)椋合到y(tǒng)默認(rèn)會將進(jìn)程級的 LayoutInflater 配置給每個 Activity 的 LayoutInflater,這也符合了 LayoutInflater 的自我介紹 “且正確配置的標(biāo)準(zhǔn) LayoutInflater”茧痒“谷停看下這一過程(實(shí)際是配置內(nèi)部的 Factory):
// original是應(yīng)用進(jìn)程級的LayoutInflater,即在SystemServiceRegistry中保存
// 的LayoutInflater實(shí)例旺订。
protected LayoutInflater(LayoutInflater original, Context newContext) {
mContext = newContext;
mFactory = original.mFactory;
mFactory2 = original.mFactory2;
mPrivateFactory = original.mPrivateFactory;
setFilter(original.mFilter);
}
跟蹤到這惹苗,LayoutInflater 的創(chuàng)建及實(shí)際類型就已經(jīng)非常清晰了,并且根據(jù)不同的 Context 參數(shù)耸峭,我們可以總結(jié)出如下幾條規(guī)律:
由于 Application 和 Service 都是 ContextWrapper 的直接子類桩蓉,它們并沒有對 getSystemService 方法做單獨(dú)處理。故都是通過 ContextImpl 獲取的同一個劳闹,也就是保存在 SystemServiceRegistry 中的 LayoutInflater院究。
每個 Activity 都有其獨(dú)一無二的 LayoutInflater,它的實(shí)際類型是 PhoneLayoutInflater本涕。當(dāng)首次獲取某個 Activity 的 LayoutInflater 時业汰,系統(tǒng)首先會根據(jù) Application 級的 LayoutInflater 創(chuàng)建并配置對應(yīng) Activity 的 LayoutInflater样漆。
LayoutInflater 布局解析
分析完了 LayoutInflater 的創(chuàng)建過程晦闰,接下來我們看下大家最熟悉的 xml 布局解析階段 inflate鳍怨。
final View content = LayoutInflater.from(this).inflate(R.layout.content, root, false);
有關(guān) LayoutInflater 的布局解析過程在之前已經(jīng)做過詳細(xì)的分析鞋喇,具體你可以參考《View 繪制流程之 setContentView() 到底做了什么 ?》眉撵,這里我們再來回顧與總結(jié)下涉及的核心問題:
- <include /> 標(biāo)簽為什么不能作為布局的根節(jié)點(diǎn)侦香?
- <merge /> 標(biāo)簽為什么要作為布局資源的根節(jié)點(diǎn)?
- inflate ( int resource, ViewGroup root, boolean attachToRoot) 參數(shù) root 和 attachToRoot 的作用和規(guī)則纽疟?
這里直接跟蹤布局解析的核心過程 inflate 方法:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
// 獲取在XML設(shè)置的屬性
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
// 注意root容器在這里罐韩,在我們當(dāng)前分析中該root就是mContentParent
View result = root;
try {
// 查找xml布局的根節(jié)點(diǎn)
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
// 找到起始根節(jié)點(diǎn)
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
// 獲取到節(jié)點(diǎn)名稱
final String name = parser.getName();
// 判斷是否是merge標(biāo)簽
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
// 此時如果ViewGroup==null,與attachToRoot==false將會拋出異常
// merge必須添加到ViewGroup中,這也是merge為什么要作為布局的根節(jié)點(diǎn)污朽,它要添加到上層容器中
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// 否則創(chuàng)建該節(jié)點(diǎn)View對象
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
// 如果contentParent不為null散吵,在分析setContentView中,這里不為null
if (root != null) {
// 通過root(參數(shù)中的 ViewGroup)創(chuàng)建對應(yīng)LayoutParams
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// 如果不需要添加到 root膘壶,直接設(shè)置該View的LayoutParams
temp.setLayoutParams(params);
}
}
// 解析Child
rInflateChildren(parser, temp, attrs, true);
if (root != null && attachToRoot) {
// 添加到ViewGroup
root.addView(temp, params);
}
if (root == null || !attachToRoot) {
// 此時布局根節(jié)點(diǎn)為temp
result = temp;
}
}
} catch (XmlPullParserException e) {} catch (Exception e) {} finally {
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}
while 循環(huán)部分错蝴,首先找到 XML 布局文件的根節(jié)點(diǎn)洲愤,如果未找到:if (type != XmlPullParser.START_TAG) 直接拋出異常颓芭。否則獲取到該節(jié)點(diǎn)名稱,判斷如果是 merge 標(biāo)簽柬赐,此時需要注意參數(shù) root 和 attachToRoot亡问,root 必須不為null,并且 attachToRoot 必須為 true肛宋,即 merge 內(nèi)容必須要添加到 root 容器中州藕。
如果不是 merge 標(biāo)簽,此時根據(jù)標(biāo)簽名 name 調(diào)用 createViewFromTag() 創(chuàng)建該 View 對象酝陈,rInflate 和 rInflateChildren 都是去解析子 View,rInflateChildren 方法實(shí)際也是調(diào)用到了 rInflate 方法:
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
//還是調(diào)用rInflate方法
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
區(qū)別在于最后一個參數(shù) finishInflate,它的作用是標(biāo)志當(dāng)前 ViewGroup 樹創(chuàng)建完成后回調(diào)其 onFinishInflate 方法待牵。
如果根標(biāo)簽是 merge缨该,此時 finishInflate 為 false蛤袒,這也很容易理解汗盘,此時的父容器為 inflate() 傳入的 ViewGroup,它是不需要再次回調(diào) onFinishInflate() 菱阵,該過程如下:
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
boolean pendingRequestFocus = false;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
// 獲取到節(jié)點(diǎn)名稱
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
pendingRequestFocus = true;
consumeChildElements(parser);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
// include標(biāo)簽
if (parser.getDepth() == 0) {
// include如果為根節(jié)點(diǎn)則拋出異常了
// include不能作為布局文件的根節(jié)點(diǎn)
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
// 如果此時包含merge標(biāo)簽嫡锌,此時也會拋出異常
// merge只能作為布局文件的根節(jié)點(diǎn)
throw new InflateException("<merge /> must be the root element");
} else {
// 創(chuàng)建該節(jié)點(diǎn)的View對象
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);
}
}
if (pendingRequestFocus) {
parent.restoreDefaultFocus();
}
if (finishInflate) {
// 回調(diào)ViewGroup的onFinishInflate方法
parent.onFinishInflate();
}
}
while 循環(huán)部分蛛倦,parser.next() 獲取下一個節(jié)點(diǎn),如果獲取到節(jié)點(diǎn)名為 include且改,此時 parse.getDepth() == 0 表示根節(jié)點(diǎn),直接拋出異常:“<include /> cannot be the root element”慨蓝,即 <include /> 不能作為布局的根節(jié)點(diǎn)。
如果此時獲取到節(jié)點(diǎn)名稱為 merge济丘,也是直接拋出異常了疟赊,即 <merge /> 只能作為布局的根節(jié)點(diǎn):“<merge /> must be the root element”。
否則創(chuàng)建該節(jié)點(diǎn)對應(yīng) View 對象吉执,rInflateChildren 遞歸完成以上步驟,并將解析到的 View 添加到其直接父容器:viewGroup.addView(view, params)咕宿。
注意方法的最后通知調(diào)用每個 ViewGroup 的 onFinishInflate(),大家是否有注意到這其實(shí)是入棧的操作试浙,即最頂層的 ViewGroup 最后回調(diào) onFinishInflate()力细。
至此眠蚂,我們可以回答上面提出的相關(guān)問題了昔脯,先來通過一張流程圖加深下對 LayoutInflater 的解析過程(該圖基于 setContentView()):
如果布局根節(jié)點(diǎn)為 merge ,會判斷 inflate 方法參數(shù) if ( root != null && attachToRoot == true )鲸拥,表示布局文件要直接添加到 root 中捏浊,否則拋出異常:“<merge /> can be used only with a valid ViewGroup root and attachToRoot=true”;
繼續(xù)解析子節(jié)點(diǎn)的過程中如果再次解析到 merge 標(biāo)簽劣领,則直接拋出異常:“<merge /> must be the root element”。既 <merge /> 標(biāo)簽必須作為布局文件的根節(jié)點(diǎn)惊暴。
如果解析到節(jié)點(diǎn)名稱為 include,會判斷當(dāng)前節(jié)點(diǎn)深度是否為 0油啤,0 表示當(dāng)前處于根節(jié)點(diǎn),此時直接拋出異常:“<include /> cannot be the root element”幽告。即 <include /> 不能作為布局文件的根節(jié)點(diǎn)。
不容忽視的 View 創(chuàng)建耗時
在分析 XML 布局解析階段冻河,我們忽略了一個非常重要的 View 創(chuàng)建過程 createViewFromTag 方法,接下來我們就詳細(xì)跟蹤下這部分內(nèi)容廷蓉。
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
// Apply a theme wrapper, if allowed and one is specified.
if (!ignoreThemeAttr) {
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
try {
View view;
// 首先通過mFactory2加載View
if (mFactory2 != null) {
// 交給Factory2工程創(chuàng)建
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
// 其次通過mFactory工程創(chuàng)建
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
// 私有工廠
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
// 如果上邊都沒有滿足,走默認(rèn)
if (view == null) {
// 保存最后一次上下文
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
// 判斷name是否包含點(diǎn)马昙,表示是否是自定義控件
// 比如如果類名是 ImageView桃犬,實(shí)際是反射創(chuàng)建,也就是要通過類的全限定名進(jìn)行加載
// Android 系統(tǒng)默認(rèn)加載View目錄有三個在PhoneLayoutInflater中
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
// 否則是 custom view
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
} catch (InflateException e) {} catch (ClassNotFoundException e) {} catch (Exception e) {}
}
注意行楞,在 createViewFromTag 方法會依次判斷 mFactory2攒暇、mFactory、mPrivateFactory 是否為 null。也就是會依次根據(jù) mFactory2解愤、mFactory、mPrivateFactory 來創(chuàng)建 View 對象宠互∷副海看下他們在 LayoutInflater 中的聲明:
public abstract class LayoutInflater {
// ... 省略
private Factory mFactory;
private Factory2 mFactory2;
private Factory2 mPrivateFactory;
private Filter mFilter;
// ... 省略
}
Factory 和 Factory2 都屬于 LayoutInflater 的內(nèi)部接口岔激,聲明如下:
public interface Factory {
public View onCreateView(String name, Context context, AttributeSet attrs);
}
public interface Factory2 extends Factory {
public View onCreateView(String name, Context context, AttributeSet attrs);
}
Factory2 是在 Android 3.0 版本添加乐尊,兩者功能基本一致(Factory2 優(yōu)先級高于 Factory)世澜。其實(shí)前面我們講到 Activity 的 LayoutInflater 是通過 cloneInContext 方法創(chuàng)建诺舔,這一過程就是要復(fù)用它的 mFactory2、mFactory悉患、mPrivateFactory 這樣不需要再重新設(shè)置了(關(guān)聯(lián)且正確配置的標(biāo)準(zhǔn) LayoutInflater)烁涌。
- Factory 只包括一個核心的 onCreateView 方法谋币,即創(chuàng)建 View 對象的過程帜乞。這一特性為我們提供了 View 創(chuàng)建過程的 Hack 機(jī)會武翎,例如替換某個 View 類型垫毙,動態(tài)換膚消请、View 復(fù)用等卢厂。這部分內(nèi)容在后面高階技巧中再詳細(xì)介紹。
如果以上條件都不滿足惠啄,則執(zhí)行 LayoutInflater 的默認(rèn) View 創(chuàng)建流程慎恒,注意這里首先會根據(jù)解析到的標(biāo)簽名 name 是否包含 “.” ,用于判斷當(dāng)前標(biāo)簽是否屬于自定義控件類型撵渡。
- 類加載器只能通過類的全限定名來加載對應(yīng)的類融柬。例如 ImageView,此時系統(tǒng)要為其補(bǔ)齊前綴后變?yōu)椋骸癮ndroid.weight.ImageView”趋距。但是我們在 XML 中只是聲明了 View 的名稱如下(自定義 View 除外粒氧,因?yàn)槠渎暶饕呀?jīng)是全限定名):
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- 系統(tǒng)提供的 View -->
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<!-- 自定義 View,已經(jīng)是全限定名 -->
<com.xxx.android.custom.CircleView
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
接下來棚品,我們就看下系統(tǒng)是如何加載對應(yīng) View 標(biāo)簽以及創(chuàng)建過程:
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}
注意: 由于 LayoutInflater 的實(shí)際類型為 PhoneLayoutInflater靠欢,還記得上面貼出的 PhoneLayoutInflater 中重寫了 onCreateView 方法:
protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
for (String prefix : sClassPrefixList) {
try {
// 首先查找:android/widget目錄下,如 Seekbar
// 然后查找:android/webkit目錄下铜跑,如 WebView
// 最后查找:android/app目錄下门怪,如 SurfaceView
View view = createView(name, prefix, attrs);
if (view != null) {
return view;
}
} catch (ClassNotFoundException e) {}
}
// 如果以上都不能滿足,就是:android/view 目錄下
return super.onCreateView(name, attrs);
}
這里將依次遍歷原生 View 所在目錄锅纺,這一過程就是為解析到的 View 標(biāo)簽補(bǔ)齊前綴組成類的全限定名掷空,然后通過 ClassLoader 進(jìn)行加載,直到加載成功囤锉,否則拋出異常坦弟。Android 系統(tǒng)提供的 View 視圖目錄如下(其實(shí)這里還包括一個 android.view,它將作為最后):
private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};
View 的創(chuàng)建過程 createView 方法如下官地,name 表示在 XML 中的標(biāo)簽名稱如 “ImageView”酿傍,prefix 表示 ImageView 標(biāo)簽的前綴為“android.widget”,組成 “android.widget.ImageView” 交給 ClassLoader 嘗試加載:
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
// 查找name類型的的構(gòu)造方法
Constructor<? extends View> constructor = sConstructorMap.get(name);
// verifyClassLoader方法驗(yàn)證是否是同一個ClassLoader
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
// 如果ClassLoader不匹配驱入,則刪除該類型的緩存
sConstructorMap.remove(name);
}
Class<? extends View> clazz = null;
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
if (constructor == null) {
// 類未在緩存中找到赤炒,通過ClassLoader根據(jù)類全限定名加載,并緩存到sConstructorMap容器
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
// mFilter 可以攔截是否被允許創(chuàng)建該視圖類的對象
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
}
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
// 進(jìn)行緩存
sConstructorMap.put(name, constructor);
} else {
// 否則判斷是否設(shè)置了 Filter
// Filter 的主要作用是攔截當(dāng)前視圖類是否可以創(chuàng)建視圖對象
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) {
// 這里將會直接拋出異常表示不允許創(chuàng)建該視圖類對象
failNotAllowed(name, prefix, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, attrs);
}
}
}
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;
// 發(fā)射創(chuàng)建該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) {
// ... 省略所有異常報錯
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
sConstructorMap 是一個 static Map 容器亏较,用于緩存某個 View 類的構(gòu)造方法莺褒,這算是一層優(yōu)化。避免每次 loadClass() 執(zhí)行類的加載過程雪情。
注意查看 ClassLoader 的 loadClass 方法遵岩,將 prefix 和 name 組成類的全限定名進(jìn)行加載,如果成功加載對應(yīng)類巡通,獲取它的構(gòu)造方法并緩存在 sConstructorMap 容器尘执。
mFilter 是一個 Filter 類型對象舍哄,用于決定是否允許創(chuàng)建某個 View 類的對象,它的聲明如下:
public interface Filter {
// 參數(shù)clazz正卧,表示即將要創(chuàng)建視圖對象的類對象
// 返回值 true 表示允許創(chuàng)建該視圖類對象蠢熄,否則返回 false跪解,不允許炉旷。
boolean onLoadClass(Class clazz);
}
- 最后通過反射 newInstance() 創(chuàng)建對應(yīng)的 View 對象并返回。
LayoutInflater 在 View 對象的創(chuàng)建過程使用了大量反射叉讥,如果某個布局界面內(nèi)容又較復(fù)雜窘行,該過程耗時是不容忽視的。更極端的情況可能是某個 View 的創(chuàng)建過程需要執(zhí)行 4 次图仓,例如 SurfaceView罐盔,因?yàn)橄到y(tǒng)默認(rèn)遍歷規(guī)則依次為 android/weight、android/webkit 和 android/app救崔,但是由于 SurfaceView 屬于 android/view 目錄下惶看,故此時需要第 4 次 loadClass 才可以正確加載,這個效率會有多差(在 AppCompatActivity 中該過程略有改善六孵,后面的高階階段介紹)纬黎!
至此 LayoutInflater 的工作原理就已經(jīng)分析完了,個人認(rèn)為 Android 系統(tǒng)對布局 View 的創(chuàng)建過程處理的過于簡單粗暴了劫窒。但是換個角度本今,這也給我們留下更多優(yōu)化和學(xué)習(xí)的空間。
LayoutInflater 的高階使用技巧
通過上面的分析主巍,其實(shí)大家也能猜到這部分主要圍繞 LayoutInflater.Factory 展開冠息,接下來我們就來看下利用 LayoutInflater.Factory 可以幫助我們完成哪些工作?
1. Activity 默認(rèn)實(shí)現(xiàn)了 LayoutInflater.Factory2 接口
這可能也是很多開發(fā)人員所不了解的孕索,其實(shí)我們完全可以在自己的 Xxx-Activity 中重寫對應(yīng)方法逛艰,實(shí)現(xiàn)例如 View 替換、復(fù)用等機(jī)制搞旭。
public class Activity extends ContextThemeWrapper
implements LayoutInflater.Factory2 ... {
/**
* LayoutInflater.Factory散怖,LayoutInflater.Factory2 繼承自 LayoutInflater.Factory
*/
@Nullable
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
/**
* LayoutInflater.Factory2
*/
@Override
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);
}
}
2. AppCompatActivity 兼容設(shè)計(jì)
Android 在 5.0 之后引入了 Material Design 的設(shè)計(jì),為了更好的支撐 Material 主題选脊、調(diào)色版杭抠、Toolbar 等各種新特性,兼容版本的 AppCompatActivity 就應(yīng)運(yùn)而生了恳啥。大家是否有注意過偏灿,使用 AppCompatActivity 之后,所有(需要兼容特性的 View) View 控件都被替換成了 AppCompat-Xxx 類型:
AppCompatActivity 則是利用了 AppCompatDelegate 在不同的 Android 版本之間實(shí)現(xiàn)兼容配置钝的。其中將 View 的創(chuàng)建階段又單獨(dú)委托給 AppCompatViewInflater翁垂,它本質(zhì)還是利用了 LayoutInflater.Factory2 接口:
public final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
// ... 省略
View view = null;
// 根據(jù)View類型替換成對應(yīng)的AppCompat類型
switch (name) {
case "TextView":
view = new AppCompatTextView(context, attrs);
break;
case "ImageView":
view = new AppCompatImageView(context, attrs);
break;
case "Button":
view = new AppCompatButton(context, attrs);
break;
case "EditText":
view = new AppCompatEditText(context, attrs);
break;
case "Spinner":
view = new AppCompatSpinner(context, attrs);
break;
case "ImageButton":
view = new AppCompatImageButton(context, attrs);
break;
case "CheckBox":
view = new AppCompatCheckBox(context, attrs);
break;
case "RadioButton":
view = new AppCompatRadioButton(context, attrs);
break;
case "CheckedTextView":
view = new AppCompatCheckedTextView(context, attrs);
break;
case "AutoCompleteTextView":
view = new AppCompatAutoCompleteTextView(context, attrs);
break;
case "MultiAutoCompleteTextView":
view = new AppCompatMultiAutoCompleteTextView(context, attrs);
break;
case "RatingBar":
view = new AppCompatRatingBar(context, attrs);
break;
case "SeekBar":
view = new AppCompatSeekBar(context, attrs);
break;
}
if (view == null && originalContext != context) {
// If the original context does not equal our themed context, then we need to manually
// inflate it using the name so that android:theme takes effect.
view = createViewFromTag(context, name, attrs);
}
if (view != null) {
// If we have created a view, check its android:onClick
checkOnClickListener(view, attrs);
}
return view;
}
可以非常清晰的看到铆遭,整個兼容版 View 的替換過程。不過還是有一些 View 需要通過反射加載創(chuàng)建沿猜。其實(shí)這一部分也是我們最應(yīng)該優(yōu)化的枚荣,有關(guān)布局 View 的創(chuàng)建過程,我們完全可以將其接手以避免反射或極端的遍歷加載過程啼肩。
3. 動態(tài)換膚
關(guān)于動態(tài)換膚橄妆,業(yè)界做的比較好的要屬網(wǎng)易云音樂了。通過動態(tài)換膚滿足用戶的新鮮感祈坠,提升增值業(yè)務(wù)產(chǎn)品吸引力害碾。
動態(tài)換膚主要涉及兩個核心過程:① 采集需要換膚的控件,② 加載相應(yīng)皮膚包赦拘,并替換所有需要換膚的控件慌随。
如何確定哪些控件需要動態(tài)換膚呢?這里簡單提供一種思路躺同,首先要明確換膚到底是換的什么阁猜?只要理解了換的是什么,我們就知道要查詢哪些屬性了:
static {
mAttributes.add("background");
mAttributes.add("src");
mAttributes.add("textColor");
mAttributes.add("drawableLeft");
mAttributes.add("drawableTop");
mAttributes.add("drawableRight");
mAttributes.add("drawableBottom");
}
那如何采集呢蹋艺?其實(shí)這一過程就可以利用到今天介紹的 LayoutInflater.Factory剃袍。而且還可以結(jié)合 ActivityLifecycleCallback 進(jìn)一步減少代碼的侵入性。
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
// ... View 的創(chuàng)建過程省略
// 篩選符合換膚條件的 View
skinAttribute.load(view, attrs);
return view;
}
篩選過程主要是遍歷 View 的屬性集合 AttributeSet车海,查找 View 是否包含匹配的換膚屬性笛园。該過程如下:
public void load(View view, AttributeSet attrs) {
final List<SkinPair> skinPairs = new ArrayList<>();
for (int i = 0; i < attrs.getAttributeCount(); i++) {
// 獲得屬性名
String attributeName = attrs.getAttributeName(i);
// 是否符合需要篩選的屬性名
if (mAttributes.contains(attributeName)) {
String attributeValue = attrs.getAttributeValue(i);
if (attributeValue.startsWith("#")) {
// 屬性資源寫死了,無法替換
continue;
}
//資源id
int resId;
if (attributeValue.startsWith("?")) {
// 主題資源侍芝,attr Id
int attrId = Integer.parseInt(attributeValue.substring(1));
// 獲得主題style中對應(yīng) attr 的資源id值
resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0];
} else {
// @12343455332
resId = Integer.parseInt(attributeValue.substring(1));
}
if (resId != 0) {
// 可以被替換的屬性
SkinPair skinPair = new SkinPair(attributeName, resId);
skinPairs.add(skinPair);
}
}
}
// 將View與之對應(yīng)的可以動態(tài)替換的屬性集合放入集合中
if (!skinPairs.isEmpty()) {
// 每個SkinView表示可以被換膚的控件
// skinPairs表示該控件哪些屬性需要被換膚
SkinView skinView = new SkinView(view, skinPairs);
skinView.applySkin();
mSkinViews.add(skinView);
}
}
有關(guān) Android 換膚原理網(wǎng)上資料也比較多研铆,感興趣的朋友可以進(jìn)一步學(xué)習(xí)理解。
4. setFactory / setFactory2
LayoutInflater 內(nèi)部為開發(fā)者提供了直接設(shè)置 Factory 的方法州叠,不過需要注意該方法只能被設(shè)置一次棵红,否則將會拋出異常。聰明的你很快就會想到可以利用反射將其修改(LayoutInflater 并沒有被 @hide 聲明)咧栗。
public void setFactory(Factory factory) {
if (mFactorySet) {
// 注意該變量逆甜,被設(shè)置過一次后會被置為true
throw new IllegalStateException("A factory has already been set on this LayoutInflater");
}
if (factory == null) {
// factory 不能為null
throw new NullPointerException("Given factory can not be null");
}
mFactorySet = true;
if (mFactory == null) {
// 如果之前不存在,直接賦值
mFactory = factory;
} else {
// 否則合并
mFactory = new FactoryMerger(factory, null, mFactory, mFactory2);
}
}
看似一個簡單的 LayoutInflater.Factory 可以說在 View 的加載和創(chuàng)建過程提供了“無限種”可能致板。這也體現(xiàn)了優(yōu)秀的策略模式交煞,這種策略對于應(yīng)用的擴(kuò)展和兼容都提供了很大的幫助,AppCompat 就是比較經(jīng)典的例子斟或。
通過今天的分析素征,在你的項(xiàng)目中 View 的創(chuàng)建過程是否存在優(yōu)化的空間呢?可以將今天的內(nèi)容優(yōu)化到具體的應(yīng)用中,以幫助我們更好的優(yōu)化 UI 渲染性能御毅。
思考: Fragment 中也會使用到 LayoutInflater根欧,它是否和 Activity 使用的同一個呢?歡迎大家的分享留言或指正端蛆。
最后凤粗,文章如果對你有幫助,請留個贊吧今豆。
擴(kuò)展閱讀
- Android 之如何優(yōu)化 UI 渲染(上)
- 深入 Activity 三部曲(3)之 View 繪制流程
- 關(guān)于 UI 渲染嫌拣,你需要了解什么?
- Android 之你真的了解 View.post() 原理嗎晚凿?
- Android 之 ViewTreeObserver 全面解析
其他系列專題