一、inflate的基本使用
inflate方法非常基础且常用,但是好像很多人都用错了,比如说自定义view的时候多了一层父布局等。刚好再处理inflate的优化,所以总结一下我理解的inflate()方法,(如有内容错误,还麻烦指出,大家一起进步~)
好像除了activity的onCreate()方法内可以调用setContentView()之外,加载一个布局都需要使用Inflate()方法。
LayoutInflater.from(context).inflate(@LayoutRes int resource, @Nullable ViewGroup root)
View.inflate(Context context, @LayoutRes int resource, ViewGroup root)
Activity.setContentView(@LayoutRes int layoutResID)
其实这三个方法底层逻辑都是LayoutInflater#inflate方法。
二、inflate 详细解析
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
...
}
2.1参数解释
resource:加载的layoutId
root 和attachToRoot 结合起来理解:当root 可以为null,表示直接加载layout,不做任何处理,attachToRoot没有任何意义。如果不为null 且 attachToRoot为true,那么就会把解析的layout添加到root里面。如果attachToRoot 为false,那么只是限制了这个根节点的部分属性(换句话说xml中根节点的属性不一定全部都会生效,具体要看root支持哪些)
接下来一行一行看代码:
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) + ")");
}
//这个方案android还不支持,具体可以看我之前的一篇分析。所以这个view一定是null
View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
if (view != null) {
return view;
}
//拿到parser 进入关键函数
XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
这里面有两个知识点
三、涉及知识点
1、tryInflatePrecompiled(resource, res, root, attachToRoot)
这个方案android还不支持,具体可以看我之前的一篇分析。所以这个view一定是null
2、XmlResourceParser
XmlResourceParser是一个xml解析工具,通过res.getLayout(resource)获取,通过调用next()方法遍历XmlResourceParser,可以获取xml中所有内容。parser内部有个类似于指针的东西,执行一次next()方法后,指针就会指向下一个节点,通过demo验证,他是一个一个标签深度遍历的。
具体可以看这篇文章https://www.jianshu.com/p/d3c801584f8f
[图片上传失败...(image-5f8f2c-1663308292830)]
接下来看最重要的方法inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
//3:拿到attrs
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
//直接进入根节点,因为一份xml可能存在一些其他标签,执行这个之后parser指针指向根节点
advanceToRootNode(parser);
//拿到根节点的标签名
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
//根节点是merge
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
//4:解析根节点为merge的layout
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
//5:实例化根节点view
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
//6:获取根节点的attr
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
//设置根节点的params
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// Inflate all children under temp against its context.
//7:解析根节点内部的子view
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
//将根节点添加到root上,使用的布局参数是layout中定义的。
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(
getParserStateDescription(inflaterContext, attrs)
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}
这个方法很重要,只要稍微看漏一点就理解错了。
3、final AttributeSet attrs = Xml.asAttributeSet(parser)
public static AttributeSet asAttributeSet(XmlPullParser parser) {
return (parser instanceof AttributeSet)
? (AttributeSet) parser
: new XmlPullAttributes(parser);
}
这个方法其实返回的就是他自己。但是AttributeSet相对与XmlResourceParser来说,少了next()方法,通过这个attr,只能获取xml中某个节点。这边需要了解的一点是attrs 和parse是同一个对象,当parse执行next()方法时,通过attr解析的节点就不是同一个了。
4、merge标签
解析根节点有两种情况,一种是以<merge>开头的,一种是其他类型的。
1、<merge>开头的 root 不能为 null 且attachToRoot要为true
merge简单来理解就是跳过根节点标签,将子view全部添加到root里。
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
//获取parser的深度
final int depth = parser.getDepth();
int type;
boolean pendingRequestFocus = false;
//while循环遍历xml:当下一个节点不是</> 或者 当前指针指向的节点在内部
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();
//4.1 遍历节点
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)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
//初始化name所对应的节点view
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
//根据viewGroup解析该节点上的attr生成params设置给view
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
//解析子view
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
4.1遍历节点
从merge这边过来,parser.next()之后就已经指向第二个节点了。节点开始的标签支持三个特殊标签"requestFocus"、"tag"、"include"和其他标签。其中"include"场景使用较多。
其他标签只得就是view了 就是这三步骤
- 初始化name所对应的节点view
- 使用viewGroup解析该节点上的attr生成params设置给view,generateLayoutParams会放到后面重点说明
- 使用递归方式解析子view
5、其他标签
其他标签就是view的了,直接看createViewFromTag。
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
//如果标签名为view,name真实的值为class所对应的值。
//ps:好像很少看到这样的写法
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
// Apply a theme wrapper, if allowed and one is specified.
//ignoreThemeAttr = false 解析xml中的theme标签
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 {
//5.1:对外暴露的钩子
View view = tryCreateView(parent, name, context, attrs);
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
//5.2:原生view,比如ImageView
if (-1 == name.indexOf('.')) {
view = onCreateView(context, parent, name, attrs);
} else {
//5.3:自定义及第三方view
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;
}
}
5.1 tryCreateView(parent, name, context, attrs);
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!
//一个不停闪烁的view,比如时钟上的:闪烁,可以用这个。
//但是只要被添加到窗口,就会开始闪烁,无法控制他的开始和暂停等,如果需要更多功能的,可以模仿他写一个自定义的。
return new BlinkLayout(context, attrs);
}
View view;
if (mFactory2 != null) {
//mFactory2和mFactory是可以由有外部传入的,这个也是对外暴露的方法。
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;
}
factory有暴露接口设置进来,但是factory只能被设置一次,使用AppCompatActivity都有设置,详情可参考:
https://blog.51cto.com/u_15064646/2575022
factory处理这些方法create方法有几个好处:
- 一个是不需要走到系统的方法再通过反射去创建view,如果找到相关的view,直接new。
- 可以创建view的时候统一处理一下,比如xml定义了一个<TextView>,使用AppCompatActivity都会给转成<AppCompatTextView>,也可以改改背景等,网易云之前的换肤方案用的就是这个。
- 可以打印onCreateView的时间。
@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");
}
}
}
ps:我使用的appcompat 1.5.0版本,已经是setFactory2了。
5.2 createView(context, name, null, attrs)
如果view为null,就会走原生的方式解析view。view只有前面的factory没有匹配上时为null。
原生处理方式分两种:-1 == name.indexOf('.'),表示name标签没有.,也就是那些不用写包名的控件。其实这个在后面会自动加上前缀:
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}
public final View createView(@NonNull Context viewContext, @NonNull String name,
@Nullable String prefix, @Nullable AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Objects.requireNonNull(viewContext);
Objects.requireNonNull(name);
//先从缓存里找是否有已经有name对应的view
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(name);
}
Class<? extends View> clazz = null;
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
//反射找到对应的view
clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, viewContext, attrs);
}
}
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
// If we have a filter, apply it to cached constructor
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 = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);
boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);
if (!allowed) {
failNotAllowed(name, prefix, viewContext, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, viewContext, attrs);
}
}
}
Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = viewContext;
Object[] args = mConstructorArgs;
args[1] = attrs;
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;
}
} catch (NoSuchMethodException e) {
final InflateException ie = new InflateException(
getParserStateDescription(viewContext, attrs)
+ ": Error inflating class " + (prefix != null ? (prefix + name) : name), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (ClassCastException e) {
// If loaded class is not a View subclass
final InflateException ie = new InflateException(
getParserStateDescription(viewContext, attrs)
+ ": Class is not a View " + (prefix != null ? (prefix + name) : name), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (ClassNotFoundException e) {
// If loadClass fails, we should propagate the exception.
throw e;
} catch (Exception e) {
final InflateException ie = new InflateException(
getParserStateDescription(viewContext, attrs) + ": Error inflating class "
+ (clazz == null ? "<unknown>" : clazz.getName()), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
走原生的方式就是通过反射去实例化name对应的view,从mConstructorSignature可以看出来,调用的构造方法是两个参数的。到这里,view就创建完成了。
static final Class<?>[] mConstructorSignature = new Class[] {
Context.class, AttributeSet.class};
6、root.generateLayoutParams(attrs)
这个方法见过很多次了。这里需要理解的有两个点
attr所对应的内容不是固定的,他随着parse指针的变化,获取到的attr也是变化的。按照上面的流程,可以确定attr和当前name随对应的节点是一一对应的。
-
attr中的属性并不是所有的都会生效,取决于root的generateLayoutParams方法,root支持解析哪些属性,那么就只有那些属性会生效。
比如FrameLayout只会解析宽高layout_gravity、layout_width、layout_height,LinearLayout还会解析layout_weight,其他的viewGroup解析的就更多了。
public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) { super(c, attrs); final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.FrameLayout_Layout); gravity = a.getInt(R.styleable.FrameLayout_Layout_layout_gravity, UNSPECIFIED_GRAVITY); a.recycle(); }
public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); TypedArray a = c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout); weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0); gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1); a.recycle(); }
7、rInflateChildren(parser, temp, attrs, true);
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
嵌套调用解析子view。