背景
为什么LayoutInflater.inflate布局文件加载慢?
- 第一点是通过 IO 从磁盘中加载布局文件;
- 第二点是通过反射的方式创建对应的 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- 如果参数三
attachToRoot
为true, 则root
是作为参数一中布局文件的父控件 - 如果参数三
attachToRoot
为false, 则root
是作为提供一组 LayoutParams 值的对象 - 不管参数三是true还是false,都需要
root
的LayoutParams来正确的测量与放置resource
文件所产生的View对象,唯一的不同是:attachToRoot
为true时,会自动调用root.addView(view, params),最后返回root; - 如果
root
为null,attachToRoot
将失去作用,设置任何值都没有意义,加载的布局文件最外层的所有resource
属性会失效,由父布局来重新指定
- 如果参数三
- 三参
attachToRoot
:是否把一参视图加入到root
中 - 返回值:若
root
不为空,且第attachToRoot
为 true,则返回root
作为根布局,否则,返回参数一 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节点生成根节点,解析XML的过程就是遍历节点,如果找到就
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
// 5、生成子节点
rInflateChildren(parser, temp, attrs, true);
// 6、所有生成节点的过程都是在createViewFromTag
layoutinflater 定义了 factory(和factory2)接口,定义的是生产view的时候的接口
当layoutinflater 产生view的时候(createViewFromTag),先从factory2里获取,如果没有才自己生产
AppCompatDelegateImpl作为factory2实现在负责生产的时候(LayoutInflater.Factory2.onCreateView),调用了 AppCompatViewInflater具体生产,把button等翻译到appcompatbutton,这里直接new对象了
对于没有factory负责生产的对象,layoutinflater自己生产,使用反射获得构造函数并缓存。
在Activity里执行setContentView或者inflate布局文件最终都会走到如下代码
// 根据TAG创建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;
}
}
// 尝试创建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时优先使用mFactory2和mFactory实例化, 如果都实例化失败时执行createView函数实例化View, 而实例化是用类反射的方式实现的,需要完整的包名和类名; 如果是安卓原生控件需要添加android.view前缀。
在理清楚这些后,就知道hook掉LayoutInflater.Factory2这个接口,就能让所有创建过程都经过我的控制。
如何Hook 动态替换资源
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可以统一管理inflate过程, 具体的应用场景包括:
- 统计View的inflate时间;
- 归口统一修改View的属性或者实例化其它View对象, 例如全局替换字体。 可以在基类Activity的onCreate函数里替换所有TextView的字体。
- 换肤需求。
- 在hook函数使用new方式实例化自定义View, 但JDK8优化了反射性能, 删除了synchronized同步机制。
View.inflate() 和 LayoutInflater inflate() 区别
View.inflate和LayoutInflater.inflate方法区别
异步加载
Android AsyncLayoutInflater 源码解析
参考文档
Hook拦截View的创建
布局优化之异步Inflate实战
setContentView()背后的故事
Hook源码实现阿里无闪烁换肤
LayoutInflater Hook控件加载耗时