LayoutInflater 源码分析

Posted 吴豪杰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LayoutInflater 源码分析相关的知识,希望对你有一定的参考价值。

0. 前言

LayoutInflater(布局填充器) 在安卓开发中,可以说是扮演着相当重要的角色,它让我们的 ListViewRecyclerView 等很容易变得多姿多彩,也正是它如此容易的操作,让它不由地多出了一份神秘…这篇博文将基于 android 6.0LayoutInflater 的源码进行一定分析。

1. 获取实例

protected LayoutInflater(Context context) 
        mContext = context;
    

保护权限的构造方法使得获取 LayoutInflater 并不能直接使用 new 关键字,需要使用静态方法 from(context) 来从 SystemService 取得:

/**
 * 从给定的Context获取实例
 */
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;

getSystemService(...) 的最终实现在 ContextImpl 调用的 SystemServiceRegistry 类中:

public static Object getSystemService(ContextImpl ctx, String name) 
    ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
    return fetcher != null ? fetcher.getService(ctx) : null;

也就是从 SYSTEM_SERVICE_FETCHERS 这个常量 HashMap 中获取,那么这些系统服务是什么时候 put 进去的呢?答案就在类开头的静态代码块:

// 静态代码块 第一次访问此类(SystemServiceRegistry)时执行
static 
    // 注册辅助功能服务
    registerService(Context.ACCESSIBILITY_SERVICE, AccessibilityManager.class,
            new CachedServiceFetcher<AccessibilityManager>() 
        @Override
        public AccessibilityManager createService(ContextImpl ctx) 
            return AccessibilityManager.getInstance(ctx);
        );
    ...
    // 注册布局填充器服务
    registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
        new CachedServiceFetcher<LayoutInflater>() 
            @Override
            public LayoutInflater createService(ContextImpl ctx) 
                return new PhoneLayoutInflater(ctx.getOuterContext());
            
        );
    ...


// 注册的时候放入SYSTEM_SERVICE_FETCHERS
private static <T> void registerService(String serviceName, Class<T> serviceClass,
        ServiceFetcher<T> serviceFetcher) 
    SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
    SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);

既然放进去了,那么如何取出呢?

return fetcher != null ? fetcher.getService(ctx) : null;

取出服务是通过 Fetcher 取出的, Fetcher 又是什么呢?

// ServiceFetcher是一个抽象接口
static abstract interface ServiceFetcher<T> 
    T getService(ContextImpl ctx);


// CachedServiceFetcher 抽象类为其实现
static abstract class CachedServiceFetcher<T> implements ServiceFetcher<T> 
    ...
    @Override
    public final T getService(ContextImpl ctx) 
        // 取得缓存区
        final Object[] cache = ctx.mServiceCache;
        synchronized (cache) 
            // 从缓存区中获取
            Object service = cache[mCacheIndex];
            if (service == null) 
                // 如果缓存区中没有就调用createService(context)
                service = createService(ctx);
                // 并且放入缓存区
                cache[mCacheIndex] = service;
            
            return (T)service;
        
    

    // 创建实例抽象方法
    public abstract T createService(ContextImpl ctx);

好了,这下终于找到 LayoutInflater 的实例化入口了,就在 CachedServiceFetcher<T> 接口的实现中,也就是 registerService(...) 的第三个参数:

new CachedServiceFetcher<LayoutInflater>() 
    @Override
    public LayoutInflater createService(ContextImpl ctx) 
        // 创建实例
        return new PhoneLayoutInflater(ctx.getOuterContext());
    

这里 new 出了 PhoneLayoutInflater 实例,原来 PhoneLayoutInflater 才是我们使用到 LayoutInflater :

// 继承自LayoutInflater
public class PhoneLayoutInflater extends LayoutInflater 
    ...

2. 填充视图

inflate(...) 有多个方法重载,此处以参数较为全面的,也是常用的一个进行分析:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
    // 获取Resources
    final Resources res = getContext().getResources();
    if (DEBUG) 
        Log.d(TAG, "INFLATING from resource: \\"" + res.getResourceName(resource) + "\\" ("
                + Integer.toHexString(resource) + ")");
    
    // 传入一个Layout 返回该xml的解析器
    final XmlResourceParser parser = res.getLayout(resource);
    try 
        // 接着调用含有XmlPullParser参数的重载方法
        return inflate(parser, root, attachToRoot);
     finally 
        parser.close();
    

接下来就来到那个含有XmlPullParser参数的重载方法:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) 
    // 首先启用同步锁确保线程安全
    synchronized (mConstructorArgs) 
        // 开始事务
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
        final Context inflaterContext = mContext;
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        Context lastContext = (Context) mConstructorArgs[0];
        mConstructorArgs[0] = inflaterContext;
        // 参数root将作为返回初值 防止填充错误返回null
        View result = root;
        try 
            // 查找根节点
            int type;
            while ((type = parser.next()) != XmlPullParser.START_TAG &&
                    type != XmlPullParser.END_DOCUMENT) 
                // 跳出循环这意味到达根节点
            
            if (type != XmlPullParser.START_TAG) 
                throw new InflateException(parser.getPositionDescription()
                        + ": No start tag found!");
            
            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)) 
                // 那么必须要展示在root布局中
                if (root == null || !attachToRoot) 
                    throw new InflateException("<merge /> can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                
                // 遍历子view
                rInflate(parser, root, inflaterContext, attrs, false);
             else 
                // 填充根ViewGroup
                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);
                    
                    // 取得当前父容器的LayoutParams
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) 
                        // 如果attachToRoot为false 则应用父容器的LayoutParams
                        temp.setLayoutParams(params);
                    
                
                if (DEBUG) 
                    System.out.println("-----> start inflating children");
                
                // 遍历子view
                rInflateChildren(parser, temp, attrs, true);
                if (DEBUG) 
                    System.out.println("-----> done inflating children");
                
                // 如果attachToRoot为false 则调用父容器的addView方法
                if (root != null && attachToRoot) 
                    root.addView(temp, params);
                
                // 其它情况直接返回填充好的view
                if (root == null || !attachToRoot) 
                    result = temp;
                
            
         catch (XmlPullParserException e) 
            InflateException ex = new InflateException(e.getMessage());
            ex.initCause(e);
            throw ex;
         catch (Exception e) 
            InflateException ex = new InflateException(
                    parser.getPositionDescription()
                            + ": " + e.getMessage());
            ex.initCause(e);
            throw ex;
         finally 
            // 保存最后一次的Context 以便它用
            mConstructorArgs[0] = lastContext;
            mConstructorArgs[1] = null;
        
        // 事务结束
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        return result;
    

思路就是解析 xml ,对参数进行合理的应用,然后遍历子 view ,从而带领出两条路: 一个是填充根 view group ,一个是遍历子 view ,分别是以下两个后面将进行分析的方法:

// 根布局
createViewFromTag(root, name, inflaterContext, attrs);
// 子布局
rInflate(parser, root, inflaterContext, attrs, false);


还有一个重点是,这段代码让我们更加理解了 inflate(...) 后面两个参数 ViewGroup rootboolean attachToRoot 的联系:

  • root 指的是需要将填充好的view所放在的父容器
  • attachToRoot 指的是是否链接到root父容器,如果为true,则调用父容器的addView()方法,否则应用父容器的LayoutParams


另外这里补充几个标签的知识:

<merge/><include/><View Stub/> 标签的使用与区别

  • <merge/> 将视图组合,减少UI层次,但只能用于根节点,比如include一个layout,这个layout中就可以用merge作为根节点,防止include后造成多层嵌套
  • <include/> 包含一个layout布局,减少代码重复
  • <View Stub/> 用于需要时才加载的布局,比如错误信息的展示

2.1 填充根布局

填充根布局调用 createViewFromTag(...) 来获取 View

private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) 
    // 调用下面的重载方法
    return createViewFromTag(parent, name, context, attrs, false);

其实调用的是含包访问权限的其重载方法:

// 参数parent 需要显示在之上的父容器
// 参数name 解析xml中跟节点名字
// 参数attrs 主题属性
// 参数ignoreThemeAttr 是否忽略主题样式
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
                boolean ignoreThemeAttr) 
    // 如果标签是view
    if (name.equals("view")) 
        // 则获取null命名空间中class属性的值
        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) 
            // 使用带有主题样式的ContextThemeWrapper来替换原有的context
            context = new ContextThemeWrapper(context, themeResId);
        
        ta.recycle();
    
    // 如果标签是blink
    if (name.equals(TAG_1995)) 
        // Let's party like it's 1995!
        // 1955是什么典故么?
        // 返回 内部继承自FrameLayout的BlinkLayout
        return new BlinkLayout(context, attrs);
    
    try 
        // 使用非空的Factory调用onCreateView(...)方法
        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);
        
        if (view == null) 
            // 如果Factory都无法正常工作
            final Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = context;
            // 则直接调用内部onCreateView(...)方法
            try 
                // 判断是否包含'.'
                // contains()方法内部也是通过调用indexOf()
                if (-1 == name.indexOf('.')) 
                    // 包含表明指定了完整类名
                    // 比如 android.support.v7.widget.CardView
                    view = onCreateView(parent, name, attrs);
                 else 
                    // 否则传入null使用默认的前缀
                    view = createView(name, null, attrs);
                
             finally 
                mConstructorArgs[0] = lastContext;
            
        
        return view;
     catch (InflateException e) 
        throw e;
     catch (ClassNotFoundException e) 
        final InflateException ie = new InflateException(attrs.getPositionDescription()
                + ": Error inflating class " + name);
        ie.initCause(e);
        throw ie;
     catch (Exception e) 
        final InflateException ie = new InflateException(attrs.getPositionDescription()
                + ": Error inflating class " + name);
        ie.initCause(e);
        throw ie;
    

经过一番折腾,原来真正的创建 ViewcreateView(name, null, attrs) 方法中:

public final View createView(String name, String prefix, AttributeSet attrs)
                        throws ClassNotFoundException, InflateException 
    // 依旧从缓存区中获取构造器
    Constructor<? extends View> constructor = sConstructorMap.get(name);
    Class<? extends View> clazz = null;
    try 
        // 开始事务
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
        if (constructor == null) 
            // 如果构造器为获取到 就使用反射获取 并强制转换为View的class
            clazz = mContext.getClassLoader().loadClass(
                    prefix != null ? (prefix + name) : name).asSubclass(View.class);
            // 允许客户端对其进行过滤 比如RemoteViews
            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 
            // 过滤
            if (mFilter != null) 
                Boolean allowedState = mFilterMap.get(name);
                if (allowedState == null) 
                    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[] args = mConstructorArgs;
        args[1] = attrs;
        // 使用构造器创建对象
        final View view = constructor.newInstance(args);
        if (view instanceof ViewStub) 
            // 如果是ViewStub 就为其设置LayoutInflater 以便后续inflate
            final ViewStub viewStub = (ViewStub) view;
            viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
        
        return view;
     catch (NoSuchMethodException e) 
    ...

至此,根布局的填充算是完成了。

2.2 遍历子布局

接下来便是遍历子布局:

// 深度优先遍历
void rInflate(XmlPullParser parser, View parent, Context context,
              AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException 
    // 获取深度
    final int depth = parser.getDepth();
    int type;
    // 只要到达结束标签 就一直循环
    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)) 
            parseRequestFocus(parser, parent);
         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 
            // 如果是ViewGroup 调用填充父容器用到的方法
            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中
            viewGroup.addView(view, params);
        
    
    if (finishInflate) 
        parent.onFinishInflate();
    

遍历子布局就是一个不断递归的过程,递归完毕parent就被充满了内容,这时返回到 inflate() 方法中,各种绚丽花哨的效果就被填充完毕,就可以随时进行展示了。

总结

LayoutInflater 是一个神奇有力的工具,将它用好,相信你的App一定会更加绚丽多姿,通过此文,希望你对 LayoutInflater 有更近一步的了解,对安卓源码的精妙设计也更感兴趣!

以上是关于LayoutInflater 源码分析的主要内容,如果未能解决你的问题,请参考以下文章

LayoutInflater 源码分析

LayoutInflater源码解析

[Android FrameWork 6.0源码学习] LayoutInflater 类分析

Android LayoutInflater.inflate的使用及源码分析

Android LayoutInflater

Android源码梳理:setContentView(...)与LayoutInflater的加载机制分析