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 表示根节点,直接抛出异常: cannot be the root element”,即 不能作为布局的根节点

如果此时获取到节点名称为 merge,也是直接抛出异常了,即 只能作为布局的根节点:“ must be the root element”

否则创建该节点对应 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 优化那些事儿

更多相关文章

  1. 一步步探索学习Android(安卓)Touch事件分发传递机制(一)
  2. Android开发笔记(一百二十三)下拉刷新布局SwipeRefreshLayout
  3. android 菜单设计
  4. Android实现固定屏幕显示的方法
  5. Android三种菜单实例分析
  6. Android中DownLoadManager的使用
  7. Android应用程序UI硬件加速渲染的Display List构建过程分析
  8. Android(安卓)Studio快捷键
  9. Android: JNI本地函数控制Java端代码

随机推荐

  1. android UI设计之 背景透明色 项目资源文
  2. Android(安卓)五大布局
  3. android 手机管理软件 发布开源代码
  4. [摘]Android如何将程序打成jar包
  5. [Android(安卓)Samples视频系列之ApiDemo
  6. 第二篇,赶脚
  7. 跨境电商的模式有哪些?4种高利润商业模式
  8. 如何自学手绘插画?手绘自学基础技巧!
  9. 一文教你快速部署OneBlog开源项目
  10. 【北亚服务器数据恢复】DELL Eq PS系列服