Android(安卓)之 LayoutInflater 全面解析
UI 优化系列专题,来聊一聊 Android 渲染相关知识,主要涉及 UI 渲染背景知识、如何优化 UI 渲染两部分内容。
UI 优化系列专题
- UI 渲染背景知识
《View 绘制流程之 setContentView() 到底做了什么?》
《View 绘制流程之 DecorView 添加至窗口的过程》
《深入 Activity 三部曲(3)View 绘制流程》
《Android 之 LayoutInflater 全面解析》
《关于渲染,你需要了解什么?》
《Android 之 Choreographer 详细分析》
- 如何优化 UI 渲染
《Android 之如何优化 UI 渲染(上)》
《Android 之如何优化 UI 渲染(下)》
对于 LayoutInflater,相信每个 Android 开发人员都不会感到陌生。业界一般称它为布局解析器(或填充器),翻开 LayoutInflater 源码发现它是一个抽象类,我们先来看下它的自我介绍。
LayoutInflater 就是将 XML 布局文件实例化为对应的 View 对象,LayoutInflater 不能直接通过 new 的方式获取,需要通过 Activity.getLayoutInflater() 或 Context.getSystemService() 获取与当前 Context 已经关联且正确配置的标准 LayoutInflater。
也就是说 LayoutInflater 不能被外部实例化,只能通过系统提供的固有方式获取,但也正因如此,相信很多开发人员对它的认识仍然停留在如下代码:
final View content = LayoutInflater.from(this).inflate(R.layout.content, root, false);
今天我们就从源码的角度,进一步分析 LayoutInflater 的工作原理,主要涉及到如下几块儿内容:
- LayoutInflater 创建过程与实际类型
- LayoutInflater 的布局解析原理
- 不容忽视的 View 创建耗时
- LayoutInflater 的高阶使用技巧
LayoutInflater 创建过程与实际类型
系统在 Context 中默认提供的两种获取 LayoutInflater 的方式如下:
final LayoutInflater getLayoutInflater = getLayoutInflater();final LayoutInflater getSystemServiceInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
不过它们与直接使用 LayoutInflater.from() 除了 API 不同并没有本质上的差异,所以我们直接从 LayoutInflater 的 from 方法开始入手:
public static LayoutInflater from(Context context) { // 这里的context可以是Activity、Application、Service。 LayoutInflater LayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); if (LayoutInflater == null) { throw new AssertionError("LayoutInflater not found."); } return LayoutInflater;}
注意参数 Context,这里可以是 Activity、Application 或 Service。不知大家是否有跟踪过它们之间有什么区别吗?接下类我们就重点分析下这部分内容。
Context 的 getSystemService 方法默认是抽象的,看下它在 Context 中的声明:
public abstract @Nullable String getSystemServiceName(@NonNull Class<?> serviceClass);
这里我们主要以 Activity 为例(其他类型也会在此引申出),在 Activity 的直接父类 ContextThemeWrapper 中重写了 getSystemService 方法。
@Overridepublic Object getSystemService(String name) { if (LAYOUT_INFLATER_SERVICE.equals(name)) { if (mInflater == null) { // 每个Activity都有自己独一无二的Layoutflater // 这里首先拿到在SystemServiceRegistry中注册的Application的Layoutflater // 然后根据该创建属于每个Activity的PhoneLayoutInflater mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this); } return mInflater; } return getBaseContext().getSystemService(name);}
可以看到,Activity 对于 LayoutInflater 服务的 LAYOUT_INFLATER_SERVICE 做了单独处理,使每个 Activity 都有其独立的 LayoutInflater。否则直接通过 getBaseContext().getSystemService() 获取相关服务。
这里,我们有必要先跟踪下 getBaseContext(),它的声明在 ContextThemeWrapper 的直接父类 ContextWrapper 中,如下:
public Context getBaseContext() { // mBase的实际类型是ContextImpl return mBase;}
注意:ContextWrapper 是 Application 和 Service 的直接父类
mBase 的实际类型是 ContextImpl。在 Android 中,Application、Service 和 Activity 在创建后会首先回调其 attach 方法,并在该方法为其关联一个 ContextImpl 对象(该部分源码可以参考 Activity / Application 的创建过程在 ActivityThread 中)。
故,这里的 getSystemService() 实际调用到 ContextImpl 的 getSystemService 方法:
@Overridepublic Object getSystemService(String name) { return SystemServiceRegistry.getSystemService(this, name);}
可以看到在 ContextImpl 内部,又委托给了 SystemServiceRegistry。SystemServiceRegistry 是应用进程的系统服务注册机,在其内部的静态代码块中默认注册了大量系统服务,包括 WINDOW_SERVICE、LOCATION_SERVICE 、AUDIO_SERVICE 等等,这里我们重点看下 LayoutInflater 的注册过程:
static { // ... 省略 registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class, new CachedServiceFetcher() { @Override public LayoutInflater createService(ContextImpl ctx) { // LayoutInflater 的实际类型是PhoneLayoutInflater return new PhoneLayoutInflater(ctx.getOuterContext()); }} ); // ... 省略}
然后通过 SystemServiceRegistry 的 getSystemService 方法获取相关服务过程如下:
public static Object getSystemService(ContextImpl ctx, String name) { // 根据name在SYSTEM_SERVICE_FETCHERS获取该服务类型的ServiceFetcher ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name); // 通过该Fetcher获取对应的服务类型,如果是第一次调用fetcher.getServie() // 会调用其内部的createSercice() return fetcher != null ? fetcher.getService(ctx) : null;}
SYSTEM_SERVICE_FETCHERS 是一个静态的 Map 容器,上面在 static {} 代码块中注册的服务都将保存在该容器。这里首先根据服务的 name 获取对应的 Fetcher,然后通过该 Fetcher 的 getService 方法创建相应 LayoutInflater 对象。
当我们首次获取某个服务类型时,fetcher.getService() 会执行其内部的 createService() 创建对应服务,然后每个服务都会被保存在 SystemServiceRegistry 中,这里实际间接保存在 Fetcher 中。
在 createService 方法,我们发现 LayoutInflater 的实际类型是 PhoneLayoutInflater,类定义如下:
public class PhoneLayoutInflater extends LayoutInflater { /** * 系统默认 View 目录 */ private static final String[] sClassPrefixList = { "android.widget.", "android.webkit.", "android.app." }; /** * Application 级的 LayoutInflater 使用该构造方法,就是在 * SystemServiceRegistry 的静态代码款中注册的。 */ public PhoneLayoutInflater(Context context) { super(context); } /** * 在Activity中使用LayoutInflater.form()时候调用该构造方法 */ protected PhoneLayoutInflater(LayoutInflater original, Context newContext) { super(original, newContext); } /** * 默认View 的创建流程这里(非自定义控件) */ @Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { for (String prefix : sClassPrefixList) { try { // 首先查找:android/widget目录下,如 Seekbar // 然后查找:android/webkit目录下,如 WebView // 最后查找:android/app目录下,如 ActionBar View view = createView(name, prefix, attrs); if (view != null) { return view; } } catch (ClassNotFoundException e) {} } // 如果以上都不能满足,就是:android/view 目录下 return super.onCreateView(name, attrs); } /** * newContext是具体的Activity */ public LayoutInflater cloneInContext(Context newContext) { // 每个Activity都由其独一无二的 Layoutflater return new PhoneLayoutInflater(this, newContext); }}
注意 PhoneLayoutInflater 重写了 LayoutInflater 的 onCreateView 方法,这在后面 View 的创建阶段将会分析到。
注意观察 PhoneLayoutInflater 的最后 cloneInContext(),重新回到上面 ContextThemeWrapper 的 getSystemService 方法,大家是否注意到通过 LayoutInflater.from() 获取到 LayoutInflater 对象后,又调用其 cloneInContext 方法,该方法实际调用到 PhoneLayoutInflater 的 cloneInContext 方法。
此时会为每个 Activity 单独创建一个 LayoutInflater。之所以叫做 “clone”,是因为:系统默认会将进程级的 LayoutInflater 配置给每个 Activity 的 LayoutInflater,这也符合了 LayoutInflater 的自我介绍 “且正确配置的标准 LayoutInflater”。看下这一过程(实际是配置内部的 Factory):
// original是应用进程级的LayoutInflater,即在SystemServiceRegistry中保存// 的LayoutInflater实例。protected LayoutInflater(LayoutInflater original, Context newContext) { mContext = newContext; mFactory = original.mFactory; mFactory2 = original.mFactory2; mPrivateFactory = original.mPrivateFactory; setFilter(original.mFilter);}
跟踪到这,LayoutInflater 的创建及实际类型就已经非常清晰了,并且根据不同的 Context 参数,我们可以总结出如下几条规律:
由于 Application 和 Service 都是 ContextWrapper 的直接子类,它们并没有对 getSystemService 方法做单独处理。故都是通过 ContextImpl 获取的同一个,也就是保存在 SystemServiceRegistry 中的 LayoutInflater。
每个 Activity 都有其独一无二的 LayoutInflater,它的实际类型是 PhoneLayoutInflater。当首次获取某个 Activity 的 LayoutInflater 时,系统首先会根据 Application 级的 LayoutInflater 创建并配置对应 Activity 的 LayoutInflater。
LayoutInflater 布局解析
分析完了 LayoutInflater 的创建过程,接下来我们看下大家最熟悉的 xml 布局解析阶段 inflate。
final View content = LayoutInflater.from(this).inflate(R.layout.content, root, false);
有关 LayoutInflater 的布局解析过程在之前已经做过详细的分析,具体你可以参考《View 绘制流程之 setContentView() 到底做了什么 ?》,这里我们再来回顾与总结下涉及的核心问题:
标签为什么不能作为布局的根节点? 标签为什么要作为布局资源的根节点? - inflate ( int resource, ViewGroup root, boolean attachToRoot) 参数 root 和 attachToRoot 的作用和规则?
这里直接跟踪布局解析的核心过程 inflate 方法:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate"); final Context inflaterContext = mContext; // 获取在XML设置的属性 final AttributeSet attrs = Xml.asAttributeSet(parser); Context lastContext = (Context) mConstructorArgs[0]; mConstructorArgs[0] = inflaterContext; // 注意root容器在这里,在我们当前分析中该root就是mContentParent View result = root; try { // 查找xml布局的根节点 int type; while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { // Empty } // 找到起始根节点 if (type != XmlPullParser.START_TAG) { throw new InflateException(parser.getPositionDescription() + ": No start tag found!"); } // 获取到节点名称 final String name = parser.getName(); // 判断是否是merge标签 if (TAG_MERGE.equals(name)) { if (root == null || !attachToRoot) { // 此时如果ViewGroup==null,与attachToRoot==false将会抛出异常 // merge必须添加到ViewGroup中,这也是merge为什么要作为布局的根节点,它要添加到上层容器中 throw new InflateException(" can be used only with a valid " + "ViewGroup root and attachToRoot=true"); } rInflate(parser, root, inflaterContext, attrs, false); } else { // 否则创建该节点View对象 final View temp = createViewFromTag(root, name, inflaterContext, attrs); ViewGroup.LayoutParams params = null; // 如果contentParent不为null,在分析setContentView中,这里不为null if (root != null) { // 通过root(参数中的 ViewGroup)创建对应LayoutParams params = root.generateLayoutParams(attrs); if (!attachToRoot) { // 如果不需要添加到 root,直接设置该View的LayoutParams temp.setLayoutParams(params); } } // 解析Child rInflateChildren(parser, temp, attrs, true); if (root != null && attachToRoot) { // 添加到ViewGroup root.addView(temp, params); } if (root == null || !attachToRoot) { // 此时布局根节点为temp result = temp; } } } catch (XmlPullParserException e) {} catch (Exception e) {} finally { mConstructorArgs[0] = lastContext; mConstructorArgs[1] = null; Trace.traceEnd(Trace.TRACE_TAG_VIEW); } return result; }}
while 循环部分,首先找到 XML 布局文件的根节点,如果未找到:if (type != XmlPullParser.START_TAG) 直接抛出异常。否则获取到该节点名称,判断如果是 merge 标签,此时需要注意参数 root 和 attachToRoot,root 必须不为null,并且 attachToRoot 必须为 true,即 merge 内容必须要添加到 root 容器中。
如果不是 merge 标签,此时根据标签名 name 调用 createViewFromTag() 创建该 View 对象,rInflate 和 rInflateChildren 都是去解析子 View,rInflateChildren 方法实际也是调用到了 rInflate 方法:
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException { //还是调用rInflate方法 rInflate(parser, parent, parent.getContext(), attrs, finishInflate);}
区别在于最后一个参数 finishInflate,它的作用是标志当前 ViewGroup 树创建完成后回调其 onFinishInflate 方法。
如果根标签是 merge,此时 finishInflate 为 false,这也很容易理解,此时的父容器为 inflate() 传入的 ViewGroup,它是不需要再次回调 onFinishInflate() ,该过程如下:
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)) { pendingRequestFocus = true; consumeChildElements(parser); } else if (TAG_TAG.equals(name)) { parseViewTag(parser, parent, attrs); } else if (TAG_INCLUDE.equals(name)) { // include标签 if (parser.getDepth() == 0) { // include如果为根节点则抛出异常了 // include不能作为布局文件的根节点 throw new InflateException(" cannot be the root element"); } parseInclude(parser, context, parent, attrs); } else if (TAG_MERGE.equals(name)) { // 如果此时包含merge标签,此时也会抛出异常 // merge只能作为布局文件的根节点 throw new InflateException(" 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); } } if (pendingRequestFocus) { parent.restoreDefaultFocus(); } if (finishInflate) { // 回调ViewGroup的onFinishInflate方法 parent.onFinishInflate(); }}
while 循环部分,parser.next() 获取下一个节点,如果获取到节点名为 include,此时 parse.getDepth() == 0 表示根节点,直接抛出异常:“
如果此时获取到节点名称为 merge,也是直接抛出异常了,即
否则创建该节点对应 View 对象,rInflateChildren 递归完成以上步骤,并将解析到的 View 添加到其直接父容器:viewGroup.addView(view, params)。
注意方法的最后通知调用每个 ViewGroup 的 onFinishInflate(),大家是否有注意到这其实是入栈的操作,即最顶层的 ViewGroup 最后回调 onFinishInflate()。
至此,我们可以回答上面提出的相关问题了,先来通过一张流程图加深下对 LayoutInflater 的解析过程(该图基于 setContentView()):
如果布局根节点为 merge ,会判断 inflate 方法参数 if ( root != null && attachToRoot == true ),表示布局文件要直接添加到 root 中,否则抛出异常:“
can be used only with a valid ViewGroup root and attachToRoot=true”; 继续解析子节点的过程中如果再次解析到 merge 标签,则直接抛出异常:“
must be the root element”。既 标签必须作为布局文件的根节点。 如果解析到节点名称为 include,会判断当前节点深度是否为 0,0 表示当前处于根节点,此时直接抛出异常:“
cannot be the root element”。即 不能作为布局文件的根节点。
不容忽视的 View 创建耗时
在分析 XML 布局解析阶段,我们忽略了一个非常重要的 View 创建过程 createViewFromTag 方法,接下来我们就详细跟踪下这部分内容。
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(); } if (name.equals(TAG_1995)) { // Let's party like it's 1995! return new BlinkLayout(context, attrs); } try { View view; // 首先通过mFactory2加载View if (mFactory2 != null) { // 交给Factory2工程创建 view = mFactory2.onCreateView(parent, name, context, attrs); } else if (mFactory != null) { // 其次通过mFactory工程创建 view = 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 { // 判断name是否包含点,表示是否是自定义控件 // 比如如果类名是 ImageView,实际是反射创建,也就是要通过类的全限定名进行加载 // Android 系统默认加载View目录有三个在PhoneLayoutInflater中 if (-1 == name.indexOf('.')) { view = onCreateView(parent, name, attrs); } else { // 否则是 custom view view = createView(name, null, attrs); } } finally { mConstructorArgs[0] = lastContext; } } return view; } catch (InflateException e) {} catch (ClassNotFoundException e) {} catch (Exception e) {}}
注意,在 createViewFromTag 方法会依次判断 mFactory2、mFactory、mPrivateFactory 是否为 null。也就是会依次根据 mFactory2、mFactory、mPrivateFactory 来创建 View 对象。看下他们在 LayoutInflater 中的声明:
public abstract class LayoutInflater { // ... 省略 private Factory mFactory; private Factory2 mFactory2; private Factory2 mPrivateFactory; private Filter mFilter; // ... 省略}
Factory 和 Factory2 都属于 LayoutInflater 的内部接口,声明如下:
public interface Factory { public View onCreateView(String name, Context context, AttributeSet attrs);}
public interface Factory2 extends Factory { public View onCreateView(String name, Context context, AttributeSet attrs);}
Factory2 是在 Android 3.0 版本添加,两者功能基本一致(Factory2 优先级高于 Factory)。其实前面我们讲到 Activity 的 LayoutInflater 是通过 cloneInContext 方法创建,这一过程就是要复用它的 mFactory2、mFactory、mPrivateFactory 这样不需要再重新设置了(关联且正确配置的标准 LayoutInflater)。
- Factory 只包括一个核心的 onCreateView 方法,即创建 View 对象的过程。这一特性为我们提供了 View 创建过程的 Hack 机会,例如替换某个 View 类型,动态换肤、View 复用等。这部分内容在后面高阶技巧中再详细介绍。
如果以上条件都不满足,则执行 LayoutInflater 的默认 View 创建流程,注意这里首先会根据解析到的标签名 name 是否包含 “.” ,用于判断当前标签是否属于自定义控件类型。
- 类加载器只能通过类的全限定名来加载对应的类。例如 ImageView,此时系统要为其补齐前缀后变为:“android.weight.ImageView”。但是我们在 XML 中只是声明了 View 的名称如下(自定义 View 除外,因为其声明已经是全限定名):
接下来,我们就看下系统是如何加载对应 View 标签以及创建过程:
protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { return createView(name, "android.view.", attrs);}
注意: 由于 LayoutInflater 的实际类型为 PhoneLayoutInflater,还记得上面贴出的 PhoneLayoutInflater 中重写了 onCreateView 方法:
protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { for (String prefix : sClassPrefixList) { try { // 首先查找:android/widget目录下,如 Seekbar // 然后查找:android/webkit目录下,如 WebView // 最后查找:android/app目录下,如 SurfaceView View view = createView(name, prefix, attrs); if (view != null) { return view; } } catch (ClassNotFoundException e) {} } // 如果以上都不能满足,就是:android/view 目录下 return super.onCreateView(name, attrs);}
这里将依次遍历原生 View 所在目录,这一过程就是为解析到的 View 标签补齐前缀组成类的全限定名,然后通过 ClassLoader 进行加载,直到加载成功,否则抛出异常。Android 系统提供的 View 视图目录如下(其实这里还包括一个 android.view,它将作为最后):
private static final String[] sClassPrefixList = { "android.widget.", "android.webkit.", "android.app."};
View 的创建过程 createView 方法如下,name 表示在 XML 中的标签名称如 “ImageView”,prefix 表示 ImageView 标签的前缀为“android.widget”,组成 “android.widget.ImageView” 交给 ClassLoader 尝试加载:
public final View createView(String name, String prefix, AttributeSet attrs) throws ClassNotFoundException, InflateException { // 查找name类型的的构造方法 Constructor<? extends View> constructor = sConstructorMap.get(name); // verifyClassLoader方法验证是否是同一个ClassLoader if (constructor != null && !verifyClassLoader(constructor)) { constructor = null; // 如果ClassLoader不匹配,则删除该类型的缓存 sConstructorMap.remove(name); } Class<? extends View> clazz = null; try { Trace.traceBegin(Trace.TRACE_TAG_VIEW, name); if (constructor == null) { // 类未在缓存中找到,通过ClassLoader根据类全限定名加载,并缓存到sConstructorMap容器 clazz = mContext.getClassLoader().loadClass( prefix != null ? (prefix + name) : name).asSubclass(View.class); // mFilter 可以拦截是否被允许创建该视图类的对象 if (mFilter != null && clazz != null) { boolean allowed = mFilter.onLoadClass(clazz); if (!allowed) { failNotAllowed(name, prefix, attrs); } } constructor = clazz.getConstructor(mConstructorSignature); constructor.setAccessible(true); // 进行缓存 sConstructorMap.put(name, constructor); } else { // 否则判断是否设置了 Filter // Filter 的主要作用是拦截当前视图类是否可以创建视图对象 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 = 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) { // Use the same context when inflating ViewStub later. final ViewStub viewStub = (ViewStub) view; viewStub.setLayoutInflater(cloneInContext((Context) args[0])); } mConstructorArgs[0] = lastContext; return view; } catch (NoSuchMethodException e) { // ... 省略所有异常报错 } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); }}
sConstructorMap 是一个 static Map 容器,用于缓存某个 View 类的构造方法,这算是一层优化。避免每次 loadClass() 执行类的加载过程。
注意查看 ClassLoader 的 loadClass 方法,将 prefix 和 name 组成类的全限定名进行加载,如果成功加载对应类,获取它的构造方法并缓存在 sConstructorMap 容器。
mFilter 是一个 Filter 类型对象,用于决定是否允许创建某个 View 类的对象,它的声明如下:
public interface Filter { // 参数clazz,表示即将要创建视图对象的类对象 // 返回值 true 表示允许创建该视图类对象,否则返回 false,不允许。 boolean onLoadClass(Class clazz);}
- 最后通过反射 newInstance() 创建对应的 View 对象并返回。
LayoutInflater 在 View 对象的创建过程使用了大量反射,如果某个布局界面内容又较复杂,该过程耗时是不容忽视的。更极端的情况可能是某个 View 的创建过程需要执行 4 次,例如 SurfaceView,因为系统默认遍历规则依次为 android/weight、android/webkit 和 android/app,但是由于 SurfaceView 属于 android/view 目录下,故此时需要第 4 次 loadClass 才可以正确加载,这个效率会有多差(在 AppCompatActivity 中该过程略有改善,后面的高阶阶段介绍)!
至此 LayoutInflater 的工作原理就已经分析完了,个人认为 Android 系统对布局 View 的创建过程处理的过于简单粗暴了。但是换个角度,这也给我们留下更多优化和学习的空间。
LayoutInflater 的高阶使用技巧
通过上面的分析,其实大家也能猜到这部分主要围绕 LayoutInflater.Factory 展开,接下来我们就来看下利用 LayoutInflater.Factory 可以帮助我们完成哪些工作?
1. Activity 默认实现了 LayoutInflater.Factory2 接口
这可能也是很多开发人员所不了解的,其实我们完全可以在自己的 Xxx-Activity 中重写对应方法,实现例如 View 替换、复用等机制。
public class Activity extends ContextThemeWrapper implements LayoutInflater.Factory2 ... { /** * LayoutInflater.Factory,LayoutInflater.Factory2 继承自 LayoutInflater.Factory */ @Nullable @Override public View onCreateView(String name, Context context, AttributeSet attrs) { return null; } /** * LayoutInflater.Factory2 */ @Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { if (!"fragment".equals(name)) { return onCreateView(name, context, attrs); } return mFragments.onCreateView(parent, name, context, attrs); }}
2. AppCompatActivity 兼容设计
Android 在 5.0 之后引入了 Material Design 的设计,为了更好的支撑 Material 主题、调色版、Toolbar 等各种新特性,兼容版本的 AppCompatActivity 就应运而生了。大家是否有注意过,使用 AppCompatActivity 之后,所有(需要兼容特性的 View) View 控件都被替换成了 AppCompat-Xxx 类型:
AppCompatActivity 则是利用了 AppCompatDelegate 在不同的 Android 版本之间实现兼容配置。其中将 View 的创建阶段又单独委托给 AppCompatViewInflater,它本质还是利用了 LayoutInflater.Factory2 接口:
public 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; // 根据View类型替换成对应的AppCompat类型 switch (name) { case "TextView": view = new AppCompatTextView(context, attrs); break; case "ImageView": view = new AppCompatImageView(context, attrs); break; case "Button": view = new AppCompatButton(context, attrs); break; case "EditText": view = new AppCompatEditText(context, attrs); break; case "Spinner": view = new AppCompatSpinner(context, attrs); break; case "ImageButton": view = new AppCompatImageButton(context, attrs); break; case "CheckBox": view = new AppCompatCheckBox(context, attrs); break; case "RadioButton": view = new AppCompatRadioButton(context, attrs); break; case "CheckedTextView": view = new AppCompatCheckedTextView(context, attrs); break; case "AutoCompleteTextView": view = new AppCompatAutoCompleteTextView(context, attrs); break; case "MultiAutoCompleteTextView": view = new AppCompatMultiAutoCompleteTextView(context, attrs); break; case "RatingBar": view = new AppCompatRatingBar(context, attrs); break; case "SeekBar": view = new AppCompatSeekBar(context, attrs); break; } if (view == null && originalContext != context) { // If the original context does not equal our themed context, then we need to manually // inflate it using the name so that android:theme takes effect. view = createViewFromTag(context, name, attrs); } if (view != null) { // If we have created a view, check its android:onClick checkOnClickListener(view, attrs); } return view; }
可以非常清晰的看到,整个兼容版 View 的替换过程。不过还是有一些 View 需要通过反射加载创建。其实这一部分也是我们最应该优化的,有关布局 View 的创建过程,我们完全可以将其接手以避免反射或极端的遍历加载过程。
3. 动态换肤
关于动态换肤,业界做的比较好的要属网易云音乐了。通过动态换肤满足用户的新鲜感,提升增值业务产品吸引力。
动态换肤主要涉及两个核心过程:① 采集需要换肤的控件,② 加载相应皮肤包,并替换所有需要换肤的控件。
如何确定哪些控件需要动态换肤呢?这里简单提供一种思路,首先要明确换肤到底是换的什么?只要理解了换的是什么,我们就知道要查询哪些属性了:
static { mAttributes.add("background"); mAttributes.add("src"); mAttributes.add("textColor"); mAttributes.add("drawableLeft"); mAttributes.add("drawableTop"); mAttributes.add("drawableRight"); mAttributes.add("drawableBottom"); }
那如何采集呢?其实这一过程就可以利用到今天介绍的 LayoutInflater.Factory。而且还可以结合 ActivityLifecycleCallback 进一步减少代码的侵入性。
@Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { // ... View 的创建过程省略 // 筛选符合换肤条件的 View skinAttribute.load(view, attrs); return view; }
筛选过程主要是遍历 View 的属性集合 AttributeSet,查找 View 是否包含匹配的换肤属性。该过程如下:
public void load(View view, AttributeSet attrs) { final List skinPairs = new ArrayList<>(); for (int i = 0; i < attrs.getAttributeCount(); i++) { // 获得属性名 String attributeName = attrs.getAttributeName(i); // 是否符合需要筛选的属性名 if (mAttributes.contains(attributeName)) { String attributeValue = attrs.getAttributeValue(i); if (attributeValue.startsWith("#")) { // 属性资源写死了,无法替换 continue; } //资源id int resId; if (attributeValue.startsWith("?")) { // 主题资源,attr Id int attrId = Integer.parseInt(attributeValue.substring(1)); // 获得主题style中对应 attr 的资源id值 resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0]; } else { // @12343455332 resId = Integer.parseInt(attributeValue.substring(1)); } if (resId != 0) { // 可以被替换的属性 SkinPair skinPair = new SkinPair(attributeName, resId); skinPairs.add(skinPair); } } } // 将View与之对应的可以动态替换的属性集合放入集合中 if (!skinPairs.isEmpty()) { // 每个SkinView表示可以被换肤的控件 // skinPairs表示该控件哪些属性需要被换肤 SkinView skinView = new SkinView(view, skinPairs); skinView.applySkin(); mSkinViews.add(skinView); } }
有关 Android 换肤原理网上资料也比较多,感兴趣的朋友可以进一步学习理解。
4. setFactory / setFactory2
LayoutInflater 内部为开发者提供了直接设置 Factory 的方法,不过需要注意该方法只能被设置一次,否则将会抛出异常。聪明的你很快就会想到可以利用反射将其修改(LayoutInflater 并没有被 @hide 声明)。
public void setFactory(Factory factory) { if (mFactorySet) { // 注意该变量,被设置过一次后会被置为true throw new IllegalStateException("A factory has already been set on this LayoutInflater"); } if (factory == null) { // factory 不能为null throw new NullPointerException("Given factory can not be null"); } mFactorySet = true; if (mFactory == null) { // 如果之前不存在,直接赋值 mFactory = factory; } else { // 否则合并 mFactory = new FactoryMerger(factory, null, mFactory, mFactory2); } }
看似一个简单的 LayoutInflater.Factory 可以说在 View 的加载和创建过程提供了“无限种”可能。这也体现了优秀的策略模式,这种策略对于应用的扩展和兼容都提供了很大的帮助,AppCompat 就是比较经典的例子。
通过今天的分析,在你的项目中 View 的创建过程是否存在优化的空间呢?可以将今天的内容优化到具体的应用中,以帮助我们更好的优化 UI 渲染性能。
思考: Fragment 中也会使用到 LayoutInflater,它是否和 Activity 使用的同一个呢?欢迎大家的分享留言或指正。
最后,文章如果对你有帮助,请留个赞吧。
扩展阅读
- Android 之如何优化 UI 渲染(上)
- 深入 Activity 三部曲(3)之 View 绘制流程
- 关于 UI 渲染,你需要了解什么?
- Android 之你真的了解 View.post() 原理吗?
- Android 之 ViewTreeObserver 全面解析
其他系列专题
- Android 存储优化系列专题
- Android 对象序列化之追求完美的 Serial
- Android 之不要滥用 SharedPreferences(上)
- Android 存储选项之 SQLite 优化那些事儿
更多相关文章
- 一步步探索学习Android(安卓)Touch事件分发传递机制(一)
- Android开发笔记(一百二十三)下拉刷新布局SwipeRefreshLayout
- android 菜单设计
- Android实现固定屏幕显示的方法
- Android三种菜单实例分析
- Android中DownLoadManager的使用
- Android应用程序UI硬件加速渲染的Display List构建过程分析
- Android(安卓)Studio快捷键
- Android: JNI本地函数控制Java端代码