背景
為什么LayoutInflater.inflate布局文件加載慢?
- 第一點是通過 IO 從磁盤中加載布局文件;
- 第二點是通過反射的方式創(chuàng)建對應的 View。布局文件越大 & 層級越復雜,反射越多越耗時;
如何獲取LayoutInflater實例,有三種方式:
一:LayoutInflater inflater = getLayoutInflater(); // 調用Activity的getLayoutInflater()
二:LayoutInflater localinflater =(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
三:LayoutInflater inflater = LayoutInflater.from(context);
本質上都是調用方式二
inflate() 原理剖析
inflate() 構造方法
有 4 個構造方法,前面3個構造方法最終都會調用最后一個構造方法進行布局的解析。
// 構造方法1(會調用構造方法2)
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root);
// 構造方法2
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot);
// 構造方法3
public View inflate(XmlPullParser parser, @Nullable ViewGroup root);
// 構造方法4
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot);
著重介紹下構造方法2:
- 一參
resource:ID for an XML layout resource to load (e.g., R.layout.activity_main) - 二參
root:布局的外部再嵌套一層父布局,不需要可以填null- 如果參數(shù)三
attachToRoot為true, 則root是作為參數(shù)一中布局文件的父控件 - 如果參數(shù)三
attachToRoot為false, 則root是作為提供一組 LayoutParams 值的對象 - 不管參數(shù)三是true還是false,都需要
root的LayoutParams來正確的測量與放置resource文件所產生的View對象,唯一的不同是:attachToRoot為true時,會自動調用root.addView(view, params),最后返回root; - 如果
root為null,attachToRoot將失去作用,設置任何值都沒有意義,加載的布局文件最外層的所有resource屬性會失效,由父布局來重新指定
- 如果參數(shù)三
- 三參
attachToRoot:是否把一參視圖加入到root中 - 返回值:若
root不為空,且第attachToRoot為 true,則返回root作為根布局,否則,返回參數(shù)一 view對象的根布局 作為根布局
inflate做的事:
inflate-》rInflateChildren-》rInflate-》createViewFromTag
// 1、獲得資源
final Resources res = getContext().getResources();
// 2、讀取XML
final XmlResourceParser parser = res.getLayout(resource);
// 3、解析XML
return inflate(parser, root, attachToRoot);
// 4、從XML節(jié)點生成根節(jié)點,解析XML的過程就是遍歷節(jié)點,如果找到就
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
// 5、生成子節(jié)點
rInflateChildren(parser, temp, attrs, true);
// 6、所有生成節(jié)點的過程都是在createViewFromTag
layoutinflater 定義了 factory(和factory2)接口,定義的是生產view的時候的接口
當layoutinflater 產生view的時候(createViewFromTag),先從factory2里獲取,如果沒有才自己生產
AppCompatDelegateImpl作為factory2實現(xiàn)在負責生產的時候(LayoutInflater.Factory2.onCreateView),調用了 AppCompatViewInflater具體生產,把button等翻譯到appcompatbutton,這里直接new對象了
對于沒有factory負責生產的對象,layoutinflater自己生產,使用反射獲得構造函數(shù)并緩存。
在Activity里執(zhí)行setContentView或者inflate布局文件最終都會走到如下代碼
// 根據(jù)TAG創(chuàng)建View
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();
}
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) {
throw e;
} catch (ClassNotFoundException e) {
final InflateException ie = new InflateException(
getParserStateDescription(context, attrs)
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(
getParserStateDescription(context, attrs)
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
}
}
// 嘗試創(chuàng)建View
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;
}
在inflate時優(yōu)先使用mFactory2和mFactory實例化, 如果都實例化失敗時執(zhí)行createView函數(shù)實例化View, 而實例化是用類反射的方式實現(xiàn)的,需要完整的包名和類名; 如果是安卓原生控件需要添加android.view前綴。

createViewFromTag流程圖
在理清楚這些后,就知道hook掉LayoutInflater.Factory2這個接口,就能讓所有創(chuàng)建過程都經過我的控制。

Factory使用圖解
如何Hook 動態(tài)替換資源
public class TestFragmentActivity extends Activity {
private final String TAG = "TestHookInflater";
@Override protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
long startTime = System.currentTimeMillis();
View view = null;
try {
// 這里可以“偷梁換柱”, 實例化其它View對象或修改view屬性
view = getLayoutInflater().createView(name, null, attrs);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
long cost = System.currentTimeMillis() - startTime;
Log.d(TAG, "加載布局:" + name + "耗時:" + cost);
int n = attrs.getAttributeCount();
for (int i = 0; i < n; i++) {
Log.e(TAG, attrs.getAttributeName(i) + " , " + attrs.getAttributeValue(i));
}
// hook控件或屬性
if (view != null && view instanceof Button) {
((Button) view).setText("我是hook替換的標題");
}
return view;
}
@Override public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
});
setContentView(R.layout.activity_main);
}
Hook可以統(tǒng)一管理inflate過程, 具體的應用場景包括:
- 統(tǒng)計View的inflate時間;
- 歸口統(tǒng)一修改View的屬性或者實例化其它View對象, 例如全局替換字體。 可以在基類Activity的onCreate函數(shù)里替換所有TextView的字體。
- 換膚需求。
- 在hook函數(shù)使用new方式實例化自定義View, 但JDK8優(yōu)化了反射性能, 刪除了synchronized同步機制。
View.inflate() 和 LayoutInflater inflate() 區(qū)別
View.inflate和LayoutInflater.inflate方法區(qū)別
異步加載
Android AsyncLayoutInflater 源碼解析
參考文檔
Hook攔截View的創(chuàng)建
布局優(yōu)化之異步Inflate實戰(zhàn)
setContentView()背后的故事
Hook源碼實現(xiàn)阿里無閃爍換膚
LayoutInflater Hook控件加載耗時