太原正规的做定制网站制作,网站搜索引擎优化,微信小程序网站开发教程,免费wordpress主题下载前言 最近给组内同学做了一次“动态换肤和换文案”的主题分享#xff0c;其中的核心就是LayoutInflater类#xff0c;所以把LayoutInflater源码梳理了一遍。巧了#xff0c;这周掘金新榜和部分公众号都发布了LayoutInflater或者换肤主题之类的文章。那只好站在各位大佬的肩膀…前言 最近给组内同学做了一次“动态换肤和换文案”的主题分享其中的核心就是LayoutInflater类所以把LayoutInflater源码梳理了一遍。巧了这周掘金新榜和部分公众号都发布了LayoutInflater或者换肤主题之类的文章。那只好站在各位大佬的肩膀上也来凑个热闹分析一下LayoutInflater类。前方长文预警会有很多源码分析源码基于Android 9.0 LayoutInflater简介 官方文档 developer.android.com/reference/a… 我们在加载布局的时候都会主动或者被动的用到 LayoutInflater 比如 Activity 的setContentView方法和Fragment的onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)回调等。LayoutInflater 的作用就是把布局文件xml实例化为相应的View组件。我们可以通过三种方法获取 LayoutInflater Activity.getLayoutInflater()LayoutInflater.from(context)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)每个方法都和 Context 相关联其中方法1和方法2最终都会通过方法3来实现。 获取到 LayoutInflater 后通过调用inflate方法来实例化布局。而inflate方法由很多重载我们常用的是inflate(LayoutRes int resource, Nullable ViewGroup root, boolean attachToRoot)所有 inflate 方法最终会调用到 inflate(XmlPullParser parser, Nullable ViewGroup root, boolean attachToRoot)。下面就从这个方法入手开始分析 LayoutInflater 的源码。 源码分析 inflate方法 先看一下inflate(XmlPullParser parser, Nullable ViewGroup root, boolean attachToRoot)的三个参数 XmlPullParser parser很显然是一个 XML 解析器这个解析器就是 LayoutInflater 所要加载的 XML 布局转化来的通过 PULL 方式解析。ViewGroup root装载要加载的 XML 布局的根容器比如在 Activity 的setContentView方法中就是 id 为android.R.id.content的 FrameLayout 根布局了。boolean attachToRoot是否将所解析的布局添加到根容器中同时也影响了所解析布局的宽高。被广泛讨论的是root和attachToRoot的不同传参对被加载的布局文件的影响下面看代码。 public View inflate(XmlPullParser parser, Nullable ViewGroup root, boolean attachToRoot) {synchronized (mConstructorArgs) {final Context inflaterContext mContext;// 将parser转成AttributeSet接口用来读取xml中设置的View属性final AttributeSet attrs Xml.asAttributeSet(parser);Context lastContext (Context) mConstructorArgs[0];mConstructorArgs[0] inflaterContext;View result root; // 此方法返回的View默认是roottry {// Look for the root node.int type;while ((type parser.next()) ! XmlPullParser.START_TAG type ! XmlPullParser.END_DOCUMENT) {// Empty}...final String name parser.getName(); // 获取当前的标签名...if (TAG_MERGE.equals(name)) { // 处理merge标签if (root null || !attachToRoot) {throw new InflateException(merge / can be used only with a valid ViewGroup root and attachToRoottrue);}// 递归处理rInflate(parser, root, inflaterContext, attrs, false);} else {// Temp is the root view that was found in the xml// 创建View对象final View temp createViewFromTag(root, name, inflaterContext, attrs);ViewGroup.LayoutParams params null;if (root ! null) {...// Create layout params that match root, if suppliedparams root.generateLayoutParams(attrs); // 获取根View的宽高if (!attachToRoot) { // 如果attachToRoot为false则给根View设置宽高// Set the layout params for temp if we are not// attaching. (If we are, we use addView, below)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不空且attachToRoot为true则将根View添加到容器中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) {// 如果root空或者attachToRoot为false则将返回结果设置为根Viewresult temp;}}} catch (XmlPullParserException e) {...} catch (Exception e) {...} finally {// Dont retain static reference on context.mConstructorArgs[0] lastContext;mConstructorArgs[1] null;Trace.traceEnd(Trace.TRACE_TAG_VIEW);}// 要么是root要么是创建的根Viewreturn result;}}
复制代码从代码中可以看出root和attachToRoot不同传参的影响 如果root不为nullattachToRoot设为true则会将加载的布局添加到一个父布局中即root并且返回root如果root不为nullattachToRoot设为false则会对布局文件最外层的所有layout属性进行设置并且返回该布局的根View当该view被添加到父view当中时这些layout属性会自动生效如果root为nullattachToRoot将失去作用设置任何值都没有意义返回的也是要加载的布局的根ViewrInflate方法 从上面的方法中可以看到处理merge标签时会调用rInflate处理子View时会调用rInflateChildren方法。其实rInflateChildren中调用的是rInflate而rInflate也调用了rInflateChildren从而形成了递归调用也就是递归处理子View。 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标签pendingRequestFocus true;consumeChildElements(parser);} else if (TAG_TAG.equals(name)) {// 处理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 { // 创建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); // 将View添加到父布局中}}if (pendingRequestFocus) { // 父布局处理焦点parent.restoreDefaultFocus();}if (finishInflate) { // 结束加载parent.onFinishInflate();}}
复制代码该方法中会处理requestFocus、tag、include、merge和普通View标签。其中 requestFocus是重新定位焦点的调用的consumeChildElements方法其实没干什么事只是简单的把该标签消费结束掉。tag标签一般很少用它主要用来标记View给View设置一个标签值例如 TextViewandroid:idid/tvandroid:layout_widthwrap_contentandroid:layout_heightwrap_contentandroid:textHello World! tag android:idid/tagandroid:valuehello //TextViewfindViewById(R.id.tv).setOnClickListener(new View.OnClickListener() {Overridepublic void onClick(View v) {// 试一下tag标签Toast.makeText(MainActivity.this, (String) v.getTag(R.id.tag), Toast.LENGTH_SHORT).show();}});
复制代码在ListView的自定义Adapter中应该都有用到过View的setTag方法即使用ViewHolder来重复利用View。parseViewTag方法 private void parseViewTag(XmlPullParser parser, View view, AttributeSet attrs)throws XmlPullParserException, IOException {final Context context view.getContext();final TypedArray ta context.obtainStyledAttributes(attrs, R.styleable.ViewTag);// 读取tag的idfinal int key ta.getResourceId(R.styleable.ViewTag_id, 0);// 读取tag的值final CharSequence value ta.getText(R.styleable.ViewTag_value);// 给View设置该tagview.setTag(key, value);ta.recycle();// 结束该标签子View无效consumeChildElements(parser);}
复制代码include标签不能是根标签parseInclude方法单独分析。merge标签只能是根标签这里会抛异常。parseInclude方法 private void parseInclude(XmlPullParser parser, Context context, View parent,AttributeSet attrs) throws XmlPullParserException, IOException {int type;if (parent instanceof ViewGroup) { // 必须在ViewGroup里才有效// 处理theme属性...// If the layout is pointing to a theme attribute, we have to// massage the value to get a resource identifier out of it.// 拿到layout指定的布局int layout attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);...if (layout 0) { // 必须是合法的idfinal String value attrs.getAttributeValue(null, ATTR_LAYOUT);throw new InflateException(You must specify a valid layout reference. The layout ID value is not valid.);} else { // 类似于inflate的处理// 拿到layout的解析器final XmlResourceParser childParser context.getResources().getLayout(layout);try {final AttributeSet childAttrs Xml.asAttributeSet(childParser);while ((type childParser.next()) ! XmlPullParser.START_TAG type ! XmlPullParser.END_DOCUMENT) {// Empty.}if (type ! XmlPullParser.START_TAG) {throw new InflateException(childParser.getPositionDescription() : No start tag found!);}// layout的根标签final String childName childParser.getName();if (TAG_MERGE.equals(childName)) { // 处理merge// The merge tag doesnt support android:theme, so// nothing special to do here.rInflate(childParser, parent, context, childAttrs, false);} else { // 处理Viewfinal View view createViewFromTag(parent, childName,context, childAttrs, hasThemeOverride);final ViewGroup group (ViewGroup) parent;final TypedArray a context.obtainStyledAttributes(attrs, R.styleable.Include);// 获取include里设置的idfinal int id a.getResourceId(R.styleable.Include_id, View.NO_ID);// 获取include里设置的visibilityfinal int visibility a.getInt(R.styleable.Include_visibility, -1);a.recycle();ViewGroup.LayoutParams params null;try { // 获取include里设置的宽高params group.generateLayoutParams(attrs);} catch (RuntimeException e) {// Ignore, just fail over to child attrs.}if (params null) {// 获取layout里设置的宽高params group.generateLayoutParams(childAttrs);}// include里设置的宽高优先于layout里设置的view.setLayoutParams(params);// Inflate all children.rInflateChildren(childParser, view, childAttrs, true);if (id ! View.NO_ID) {// include里设置的id优先级高view.setId(id);}// include里设置的visibility优先级高switch (visibility) {case 0:view.setVisibility(View.VISIBLE);break;case 1:view.setVisibility(View.INVISIBLE);break;case 2:view.setVisibility(View.GONE);break;}group.addView(view);}} finally {childParser.close();}}} else {throw new InflateException(include / can only be used inside of a ViewGroup);}LayoutInflater.consumeChildElements(parser);}
复制代码include里必须设置layout属性且layout的id必须合法include里设置的id优先级高于layout里设置的id即两者同时设置时后者会失效include里设置的width和height属性优先级高于layout里设置的宽高include里设置的visibility属性优先级高于layout设置的visibility。createViewFromTag方法 正常View标签都是通过createViewFromTag来创建对应的View对象的。 View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,boolean ignoreThemeAttr) {if (name.equals(view)) { // 真正的View标签名存在class属性中name attrs.getAttributeValue(null, class);}...try {View view;if (mFactory2 ! null) { // 先使用Factory2view mFactory2.onCreateView(parent, name, context, attrs);} else if (mFactory ! null) { // 再使用Factoryview mFactory.onCreateView(name, context, attrs);} else {view null;}if (view null mPrivateFactory ! null) { view mPrivateFactory.onCreateView(parent, name, context, attrs);}if (view null) {final Object lastContext mConstructorArgs[0];mConstructorArgs[0] context;try {// 通过标签名中是否包含.来区分是否为自定义Viewif (-1 name.indexOf(.)) {// 处理系统Viewview onCreateView(parent, name, attrs);} else { // 自定义View用的是全限定类名// 处理自定义Viewview createView(name, null, attrs);}} finally {mConstructorArgs[0] lastContext;}}return view;} catch (InflateException e) {...} catch (ClassNotFoundException e) {...} catch (Exception e) {...}}
复制代码优先通过Factory2和Factory来创建View这两个Factory等会再说通过标签名中是否包含.来区分待创建的View是自定义View还是系统View系统View会在onCreateView方法中添加android.view.前缀然后交由createView处理。createView方法 public final View createView(String name, String prefix, 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) { // 第一次则通过反射创建constructor// Class not found in the cache, see if its real, and try to add itclazz mContext.getClassLoader().loadClass(prefix ! null ? (prefix name) : name).asSubclass(View.class);if (mFilter ! null clazz ! null) {boolean allowed mFilter.onLoadClass(clazz);if (!allowed) {failNotAllowed(name, prefix, attrs);}}// 使用的是包含Context, AttributeSet这两个参数的构造函数constructor clazz.getConstructor(mConstructorSignature);constructor.setAccessible(true);sConstructorMap.put(name, constructor); // 添加到缓存中} else { // 命中缓存// If we have a filter, apply it to cached constructorif (mFilter ! null) { // 先过滤// Have we seen this name before?Boolean allowedState mFilterMap.get(name);if (allowedState null) {// New class -- remember whether it is allowedclazz mContext.getClassLoader().loadClass(prefix ! null ? (prefix name) : name).asSubclass(View.class);boolean allowed clazz ! null mFilter.onLoadClass(clazz);mFilterMap.put(name, allowed);if (!allowed) {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;// 反射创建View实例对象final View view constructor.newInstance(args);if (view instanceof ViewStub) {// 如果是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;} ...}
复制代码通过反射待创建View的构造函数两个参数Context和AttributeSet的构造函数来实例化View对象如果是ViewStub对象还会进行懒加载。 LayoutInflater.Factory/Factory2 通过以上流程使用LayoutInflater的infalte方法加载布局文件的整体流程就分析完了。但出现了Factory2和Factory类它们会优先创建View我们来看看着两个类到底是什么 它们都是LayoutInflater的内部类——两个接口 public interface Factory {public View onCreateView(String name, Context context, AttributeSet attrs);}public interface Factory2 extends Factory {public View onCreateView(View parent, String name, Context context, AttributeSet attrs);}
复制代码Factory2继承了Factory增加了一个带View parent参数的onCreateView重载方法。它们是在createViewFromTag中被调用的默认为null说明开发人员可以自定义这两个Factory则通过它们可以改造待加载XML布局中的View标签来使用自定义规则创建View。 来看一下它们的设置方法 public void setFactory(Factory factory) {if (mFactorySet) {throw new IllegalStateException(A factory has already been set on this LayoutInflater);}// 和setFactory2类似...}}public void setFactory2(Factory2 factory) {if (mFactorySet) { // 只能设置一次throw new IllegalStateException(A factory has already been set on this LayoutInflater);}if (factory null) {throw new NullPointerException(Given factory can not be null);}mFactorySet true;if (mFactory null) {mFactory mFactory2 factory;} else { // 合并原有的FactorymFactory mFactory2 new FactoryMerger(factory, factory, mFactory, mFactory2);}}
复制代码可以看到Factory和Factory2只能设置一次否则会抛异常。 这两个Factory的区别是什么 Factory2 是API 11 被加进来的Factory2 继承自 Factory也就说现在直接使用Factory2即可Factory2 可以对创建 View 的 Parent 进行操作那如何应用呢 Factory2/Factory的应用 AppCompatActivity中的应用 先看一张图 这个布局中使用的是正常的标签TextView和Button但通过Layout Inspector工具分析页面会发现它们被替换成了AppCompatTextView和AppCompatButton。 跟踪一下AppCompatActivity的onCreate方法 protected void onCreate(Nullable Bundle savedInstanceState) {AppCompatDelegate delegate this.getDelegate();delegate.installViewFactory();delegate.onCreate(savedInstanceState);...super.onCreate(savedInstanceState);}
复制代码委托到了AppCompatDelegate类并且调用了installViewFactory方法。找到这个类的一个实现AppCompatDelegateImpl不同版本的源码这个实现类的名字不同 class AppCompatDelegateImpl extends AppCompatDelegate implements Callback, Factory2
复制代码看到关键的Factory2了直接看installViewFactory方法 public void installViewFactory() {LayoutInflater layoutInflater LayoutInflater.from(this.mContext);if (layoutInflater.getFactory() null) { // 设置自身到LayoutInflaterLayoutInflaterCompat.setFactory2(layoutInflater, this);} else if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {Log.i(AppCompatDelegate, The Activitys LayoutInflater already has a Factory installed so we can not install AppCompats);}}
复制代码通过LayoutInflaterCompat.setFactory2将AppCompatDelegateImpl设置到LayoutInflater中。继续跟踪onCreateView的实现会走到createView方法 public View createView(View parent, String name, NonNull Context context, NonNull AttributeSet attrs) {if (this.mAppCompatViewInflater null) {TypedArray a this.mContext.obtainStyledAttributes(styleable.AppCompatTheme);String viewInflaterClassName a.getString(styleable.AppCompatTheme_viewInflaterClass);if (viewInflaterClassName ! null !AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {try {Class viewInflaterClass Class.forName(viewInflaterClassName);this.mAppCompatViewInflater (AppCompatViewInflater)viewInflaterClass.getDeclaredConstructor().newInstance();} catch (Throwable var8) {Log.i(AppCompatDelegate, Failed to instantiate custom view inflater viewInflaterClassName . Falling back to default., var8);this.mAppCompatViewInflater new AppCompatViewInflater();}} else {this.mAppCompatViewInflater new AppCompatViewInflater();}}...return this.mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext, IS_PRE_LOLLIPOP, true, VectorEnabledTintResources.shouldBeUsed());}
复制代码会创建一个AppCompatViewInflater类并且调用了它的createView方法看样子找到源头了。 来到AppCompatViewInflater类 public class AppCompatViewInflater {...final View createView(View parent, String name, NonNull Context context, NonNull AttributeSet attrs, boolean inheritContext, boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {Context originalContext context;...View view null;byte var12 -1;switch(name.hashCode()) {...case -938935918:if (name.equals(TextView)) {var12 0;}break;...}switch(var12) {case 0:view this.createTextView(context, attrs);this.verifyNotNull((View)view, name);break;...default:view this.createView(context, name, attrs);}...return (View)view;}NonNullprotected AppCompatTextView createTextView(Context context, AttributeSet attrs) {return new AppCompatTextView(context, attrs);}...
}
复制代码通过name参数拿到TextView标签后直接替换成了AppCompatTextView。 通过这波操作将一些 widget 自动变成兼容widget 例如将 TextView 变成 AppCompatTextView以便于向下兼容新版本中的特性。 那我们也可以仿照AppCompatActivity来自定义Factory实现自己需要的替换效果。 自定义Factory2 大多换肤功能的实现就是通过实现自定义Factory拦截特定View然后修改这些View的属性值或者直接返回自定义的View。举个栗子 替换TextView的文字颜色在不创建selector文件前提下实现圆角按钮先看XML LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/androidxmlns:apphttp://schemas.android.com/apk/res-autoxmlns:toolshttp://schemas.android.com/toolsandroid:layout_widthmatch_parentandroid:layout_heightmatch_parentandroid:idid/containerandroid:orientationverticaltools:ignoreMissingPrefixTextViewandroid:layout_widthmatch_parentandroid:layout_heightwrap_contentandroid:paddingTop10dpandroid:paddingBottom10dpandroid:gravitycenterandroid:textSize20spandroid:textColorcolor/third_tv_text_colorandroid:text测试设置的Factory/Buttonandroid:layout_widthwrap_contentandroid:layout_heightwrap_contentandroid:layout_marginTop10dpandroid:padding20dpandroid:layout_gravitycenter_horizontalandroid:background#ffbcccandroid:text苹果猕猴桃牛油果榴莲android:textSize15spapp:cornerRadius5dpapp:strokeWidth1dpapp:strokeColor#ccffcc//LinearLayout
复制代码注意这里用的是系统View的标签但属性里用到了自定义属性。 resourcesdeclare-styleable nameMyTextViewattr nameandroid:textColor//declare-styleabledeclare-styleable nameRoundButtonattr namecornerRadius formatdimension /attr namestrokeWidth formatdimension /attr namestrokeColor formatcolor //declare-styleable/resources
复制代码注意如果想直接替换Android自带属性需要在自定义属性里加上android:前缀。 public class MyFactory implements LayoutInflater.Factory2 {public LayoutInflater.Factory mOriginalFactory;// 这个模拟新资源private MapString, String mColorMap;public MyFactory(LayoutInflater.Factory factory) {this.mOriginalFactory factory;mColorMap new HashMap();// 模拟新的皮肤资源——新文字颜色mColorMap.put(third_tv_text_color, #0000ff);}Overridepublic View onCreateView(View parent, String name, Context context, AttributeSet attrs) {return onCreateView(name, context, attrs);}Overridepublic View onCreateView(String name, Context context, AttributeSet attrs) {View view null;if (mOriginalFactory ! null) {view mOriginalFactory.onCreateView(name, context, attrs);}if (TextView.equals(name)) {TypedArray ta context.obtainStyledAttributes(attrs, R.styleable.MyTextView);// 注意这里的属性名android:textColor不用自定义命名空间int resourceId ta.getResourceId(R.styleable.MyTextView_android_textColor, -1);String resourceName context.getResources().getResourceName(resourceId);resourceName resourceName.substring(resourceName.lastIndexOf(/) 1);view new TextView(context, attrs); // 可以直接修改原TextView的属性ta.recycle();// 这里模拟替换原TextView的textColor属性值String color mColorMap.get(resourceName);((TextView) view).setTextColor(Color.parseColor(color));}if (Button.equals(name)) {view new Button(context, attrs);// 读取自定义的属性值TypedArray ta context.obtainStyledAttributes(attrs, R.styleable.RoundButton);float radius ta.getDimension(R.styleable.RoundButton_cornerRadius, 0);float strokeWidth ta.getDimension(R.styleable.RoundButton_strokeWidth, 0);int strokeColor ta.getColor(R.styleable.RoundButton_strokeColor, -1);// 构造圆角按钮GradientDrawable drawable new GradientDrawable();drawable.setCornerRadius(DensityUtil.dip2px(context, radius));drawable.setStroke(DensityUtil.dip2px(context, strokeWidth), strokeColor);view.setBackground(drawable);ta.recycle();}return view;}
}
复制代码注意新资源可以通过其他方式存储和获取从而实现动态热换肤如果使用的是AppCompatActivity自定义Factory必须在调用super.onCreate之前设置因为它已经有了一个Factory如果使用的是Activity则必须在调用setContentView方法之前设置。 效果 可以看到TextView文字的颜色变成了蓝色在不提供自定义drawable的xml文件以及不使用自定义View标签的前提下实现了圆角按钮。 本文的相关示例代码都在zjxstar的GitHub上感兴趣的同学可以看下。 总结 LayoutInflater的相关分析就这么多文章有点长慢慢看吧 LayoutInflater的inflate的过程的核心方法是createViewFromTag 和 createView 方法LayoutInflater通过PULL解析器来解析XML布局文件通过反射来创建View对象LayoutInflater.Factory只能设置一次可以用来替换View参考资料 换肤、全局字体替换、无需编写shape、selector 的原理Factory小结mp.weixin.qq.com/s/1ua0geFnr…Android系统源码分析--View绘制流程之-inflatejuejin.im/post/5bfa7f…Android LayoutInflater Factory 源码解析www.jianshu.com/p/9c16bbaee…Android LayoutInflater 源码解析www.jianshu.com/p/f0f3de2f6…关注微信公众号最新技术干货实时推送