layoutInflater.inflate()
是经常使用的一个系统方法,使用它将XML
加载为一个View
对象,来具体扒一扒它的源码。
1. inflate
inflate一共有四个重载方法:
inflate(int resource, ViewGroup root)
inflate(XmlPullParser parser, ViewGroup root)
inflate(int resource, ViewGroup root, boolean attachToRoot)
inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)
前三个方法最后都调用到了第四个重载方法,不同的就是第三个方法中有一个tryInflatePrecompiled()
方法,后续读取资源ID,转换为XmlResourceParser
之后,还是调用了第四个重载方法,所有的逻辑都是在第四个重载方法之中。
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();
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();
}
}
复制代码
进入tryInflatePrecompiled()
查看这个方法会有一个if (!mUseCompiledView)
的退出判断。查这个变量的赋值的地方,默认的初始化方法是设置为false
,给它赋值为true
的地方只有一个,在setPrecompiledLayoutsEnabledForTesting()
,这是一个只针对单元测试开放的方法。
所以tryInflatePrecompiled()
方法一般不会走到。
class LayoutInflater {
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;
...
}
/**
* @hide for use by CTS tests
*/
@TestApi
public void setPrecompiledLayoutsEnabledForTesting(boolean enablePrecompiledLayouts) {
initPrecompiledViews(enablePrecompiledLayouts);
}
View tryInflatePrecompiled(int resource, Resources res, ViewGroup root, boolean attachToRoot) {
if (!mUseCompiledView) {
return null;
}
...
}
}
复制代码
1.1. XmlResourceParser
前面通过调用res.getLayout(resource)
,将LayoutRes
转换为了XmlResourceParser
对象。
XmlResourceParser
是XML资源的解析接口,简单来说就是一个XML资源解析器,用于解析xml资源,在后面会大面积用到,这里对它的方法做一些简单介绍。
-
int getEventType()
返回当前的事件类型,主要类型类型包括:
-
int START_DOCUMENT = 0
:XML文档开始标志 -
int END_DOCUMENT = 1
:XML文档结束标志 -
int START_TAG = 2
:XML标签开始标志,比如<LinearLayout
。 -
int END_TAG = 3
:xml 标签结束标志,比如</LinearLayout>
或者/>
。
-
-
String getAttributeValue(String namespace,String name)
返回指定的属性名对应的属性值,如果没有使用命名空间,则第一个参数传入null。
-
String getName()
返回当前 TAG 的名字,比如
<LinearLayout
返回LinearLayout
。 -
int next()
光标移动到下一个标签。
-
int getDepth()
获取当前标签的深度,根布局深度为1,根布局的直接子控件深度为2 ,以此类推,每多一层深度加1。
1.2. 第四个重载方法
这个方法中是inflate
的主要逻辑。
在这个方法中,首先是对merge
做了特殊处理,直接调用了rInflate()
继续遍历。
merge
并不是一个ViewGroup
或者View
,它相当于声明了一些视图等待被添加到另一个ViewGroup
中,可以减少很多不必要的布局。(使用merge
的时候可以使用tools:parentTag
属性进行预览添加到对应ViewGroup
的效果)
对于不是merge
的情况,则调用CreateViewFromTag()
创建根视图之后,根视图将作为root
调用rInflateChildren()
继续遍历。这几个方法后文都有介绍。
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
。。。
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser); //属性集合
View result = root;
try {
//校验START_TAG标签
advanceToRootNode(parser);
final String name = parser.getName();
if (TAG_MERGE.equals(name)) {
//使用merge的时候,inflate必须有root且attachToRoot==true
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
//创建根视图
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
//inflate的时候如果没有传root,给根view设置的属性会全部失效
if (root != null) {
// 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)
//如果attachToRoot==false,直接将param设置给根视图
//如果attachToRoot==true,通过addView设置给根视图
temp.setLayoutParams(params);
}
}
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
// 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);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
//如果root != null && attachToRoot,会直接将root传回去,其他情况下将根视图传回去
if (root == null || !attachToRoot) {
result = temp;
}
}
}
return result;
}
}
复制代码
接着从这个方法还可以看到平时使用inflate
的几个特性:
merge
并不是视图,对它设置的标签都是无效的- 如果根布局是
merge
,使用inflate
加载时,root
不能为空且attchToRoot
不能是false
。 - 如果
root
为空,根视图的所有标签都将失效,原因应该是必须根据root
的类型去创建不同的layoutParam
。 - 如果
root != null && attachToRoot
,根视图会直接添加到root
,并且root
将作为返回值,否则根视图作为返回值。
1.3. rInflate
接着往下看,rInflateChildren()
调用的还是rInflate()
方法,两个的区别也就是context
获取不太同。
rInflate()
中首先是一个大循环,看这个循环条件((type = parser.next()) != XmlPullParser.END_TAG ||parser.getDepth() > depth)
,只有两者同时不满足(即走到了END
标签,并且深度不大于parent
的深度)才会退出循环,一般来说,也就是走到parent
的END_TAG
标签退出循环。
循环内部,对requestFocus
、tag
、include
标签做了特殊的处理,其他情况下调用createViewFromTag()
创建当前View
。并且调用rInflateChildren()
进行递归往下调用。如果不是ViewGroup
,会在下一次递归中读取到当前view
的END_TAG
标签,也就是进不去循环之中。
所以可以确定的是,每一层rInflate()
调用的时候,创建出来的都只有parent
的直接子View
。最终创建出来的子view
都会被添加到对应的parent
。
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
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;
}
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
//requestFocus:parent获得焦点
pendingRequestFocus = true;
consumeChildElements(parser); //跳过到parent层级
} else if (TAG_TAG.equals(name)) {
//tag: 调用parent.setTag(key, value)设置tag标签
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
//添加include 标签
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
//merge只能是根标签
throw new InflateException("<merge /> must be the root element");
} else {
// createViewFromTag创建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) {
parent.onFinishInflate();
}
}
复制代码
1.4. createViewFromTag
前面的方法中,多次通过调用createViewFromTag()
创建了控件。
这个方法中,首先特殊处理了view
标签,将class
路径作为name
读取出来。之后读取主题。
分为两步进行了尝试创建,首先是调用tryCreateView()
进行创建。如果返回NULL
,接着判断路径名小数点的数量,分别走不同的方法,回想一下,没有小数点的标签,也就是系统自带的各种view
,在XML中不需要输入全路径而只需要名称即可。
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
//小写view,可以直接传入class路径,将name重置掉
name = attrs.getAttributeValue(null, "class");
}
//读取主题
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) {
...
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(context, parent, name, attrs);
} else {
view = createView(context, name, null, attrs);
}
}
}
return view;
}
...
}
复制代码
1.4.1. tryCreateView
接着看下如何通过tryCreateView()
尝试创建的。
方法首先判断blink
标签,是留下来的一个小彩蛋,Let's party like it's 1995!
后续,调用mFactory
,mFactory2
和mPrivateFactory
将创建方法代理出去。
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;
}
复制代码
为了快速找到这几个工厂实现方法在哪里,直接打个断点找一下。
可以看到,主要调用的mFactory2
的实现AppCompatDelegateImpl
和mPrivateFactory
的实现Activity
。
mPrivateFactory
的实现Activity
的逻辑集中在FragmentActivity
中,其中对fragment
标签做特殊处理,并且可以自己通过Activity
去代理拦截创建。这里不向里面探索,主要来看下mFactory2
的实现AppCompatDelegateImpl
。
AppCompatDelegateImpl
中层层传递,最后在AppCompatDelegateImpl.createView()
中代理给了AppCompatViewInflater
实现。
在AppCompatViewInflater.createView()
中可以看到,对标签为TextView
的直接创建返回了AppCompatTextView
。
这可不就是AppCompatXXX
之类的控件转换的地方!意外收获。也就搞清楚了,为什么XML
中就自动转换成对应的AppCompatXXX
,而自定义View
的时候需要直接继承AppCompatXXX
。
要是在这里没有拦截,一般就返回了NULL
,交由LayoutInflater
创建。
//AppCompatDelegateImpl
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
...
if (mAppCompatViewInflater == null) {
mAppCompatViewInflater = new AppCompatViewInflater();
...
}
return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
true, /* Read read app:theme as a fallback at all times for legacy reasons */
VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
);
}
//AppCompatViewInflater
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;
// 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;
...
}
return view;
}
//AppCompatViewInflater
protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
return new AppCompatTextView(context, attrs);
}
复制代码
1.4.2. createView
tryCreateView()
如果没有成功创建,会继续往下走,根据有没有小数点进入不同的onCreateView()
或者createView()
,首先来看下onCreateView()
是如何处理系统控件的。
1.4.2.1. onCreateView
在LayoutInflater
中,onCreateView()
也是层层传递,最后直接调用了createView
,只是将包名前缀android.view.
作为perfix
属性带上进入了createView()
,看到这也就知道,之所以不需要写完整包名,是因为自动进行了补充。
但走到这,有一个大疑惑,系统组件并不止android.view
这一个包名,查了一下,发现LayoutInflater
有一个实现类PhoneLayoutInflater
重写了onCreateView()
方法,在里面分别尝试了android.widget
,android.webkit
和android.app
的包名进行创建,也就是说,平时使用的LayoutInflater
其实是PhoneLayoutInflater
对象。
//LayoutInflater
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}
public class PhoneLayoutInflater extends android.view.LayoutInflater {
private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};
@Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
for (String prefix : sClassPrefixList) {
try {
View view = createView(name, prefix, attrs);
if (view != null) {
return view;
}
}
}
return super.onCreateView(name, attrs);
}
}
复制代码
1.4.2.2. createView
createView()
终于看到了创建逻辑,首先在缓存中查找有没有创建器。
如果没有找到,就尝试通过包名去加载CLASS
对象并拿到它的加载器缓存起来,这里使用的就是刚才传入的prefix + name
,prefix
就是oncreateView()
中添加的完整包名前缀。
之后就是调用newInstance()
创建对象,传入的参数Object[]
,共有两位,第一位是Context
,第二位是AttributeSet
。对应的就是View(Context context, AttributeSet attrs)
构造方法。这里也就对应了如果自定义View
只是实现了Context
的构造方法,代码中动态创建不会出错,但是不能在XML
使用。
另一个点,就是这里反射并没有更改访问权限的检查,所以JAVA
中需要给CLASS
设置public
(Kotlin
中默认是public
=_=)。
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);
}
Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = viewContext;
Object[] args = mConstructorArgs;
args[1] = attrs;
try {
final View view = constructor.newInstance(args);
return view;
} finally {
mConstructorArgs[0] = lastContext;
}
}
...
}
复制代码
1.5. parseInclude
前面看到过,对于include
标签做了特殊处理。
逻辑和inflate
基本一致,读取到了layout
的id
之后,先调用tryInflatePrecompiled()
,之前介绍过这个方法只是为了测试用。
然后根据是不是merge
标签,走不同的分支进入到rInflate()
,进入创建流程。
private void parseInclude(XmlPullParser parser, Context context, View parent,
AttributeSet attrs) throws XmlPullParserException, IOException {
int type;
int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
...
final View precompiled = tryInflatePrecompiled(layout, context.getResources(),
(ViewGroup) parent, /*attachToRoot=*/true);
if (precompiled == null) {
final XmlResourceParser childParser = context.getResources().getLayout(layout);
try {
...
if (TAG_MERGE.equals(childName)) {
rInflate(childParser, parent, context, childAttrs, false);
} else {
final View view = createViewFromTag(parent, childName,
context, childAttrs, hasThemeOverride);
final ViewGroup group = (ViewGroup) parent;
// Inflate all children.
rInflateChildren(childParser, view, childAttrs, true);
...
group.addView(view);
}
} finally {
childParser.close();
}
}
}
复制代码
2. Inflate获取
-
通过系统服务获取
LayoutInflater LayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 复制代码
-
通过
from
获取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; } 复制代码
-
通过
activity
获取activity
从PhoneWindow
中获取,PhoneWindow
中也是通过from
获取。public LayoutInflater getLayoutInflater() { return getWindow().getLayoutInflater(); } public class PhoneWindow extends Window implements MenuBuilder.Callback { private LayoutInflater mLayoutInflater; public PhoneWindow(Context context) { super(context); mLayoutInflater = LayoutInflater.from(context); } public LayoutInflater getLayoutInflater() { return mLayoutInflater; } } 复制代码
-
通过
View
获取View
留有inflate
方法,通过from
获取LayoutInflater
。public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) { LayoutInflater factory = LayoutInflater.from(context); return factory.inflate(resource, root); } 复制代码
如果觉得这篇文章不错,请点个赞支持一下。