Android 深入理解LayoutInflater工作机制

Android里面有很多场景会用到LayoutInflate这个类,我们通过这个类去解析指定的布局,然后展示在布局里面。api的调用是如此的简单,我们如果每次都是单纯的调用,那就无法得到提升了,所以现在我们来看一下这个流程究竟是怎么一回事。
先来看下用法跟场景:

@Override
    public AllPavilionViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        //在recyclerview/listview的适配器中 构建每个item的布局
        return new AllPavilionViewHolder(inflater.inflate(R.layout.item_location_pavilion, parent, false));
    }
 public AmountView(Context context, AttributeSet attrs) {
        super(context, attrs);
        //自定义UI中 解析指定布局去依附在自定义view中
        LayoutInflater.from(context).inflate(R.layout.ui_amount, this);
    }
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //观察源码 其实这个setContentView 内部也是调用Layoutflate.inflate的方式来构建view的
        setContentView(R.layout.activity_reserve);
    }

可能还有一些其他场景会用到这个方法,这些就不一一举例了,我们直接来通过源码分析这块的流程吧。


   /**
     * Obtains the LayoutInflater from the given context.
     * //首先是初始化 可以看出我们常用的LayoutInflater其实是对如下方法的简单封装,意味着我们其实有两种方式来构建出LayoutInflater的实例
     */
    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;//为什么变量名字大写开头 我也不知道
    }

//拿到我们想要的实例之后,之后就是调用inflate方法去构建view了。

  //四种inflate方法 到最后还是殊途同归
 public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        //里面调用的还是下面那个方法 只是做了一点判断而已
        return inflate(resource, root, root != null);
    }
  //说一下参数 
  //resource代表需要解析的xml文件
  //root 表示这个xml文件外围包裹的父布局,如果不需要 直接传null便可 
  //attachToRoot 是否需要布局文件依附在root上 如果root为null的话 那肯定是依附不了的
 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) + ")");
        }

        final XmlResourceParser parser = res.getLayout(resource);//得到一个xml解析器
        try {
            return inflate(parser, root, attachToRoot);//这个方法才是真正的开始解析布局文件
        } finally {
            parser.close();//解析器用完 关掉
        }
    }
  //重点来了 
 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;
            View result = root;

            try {
                // Look for the root node.
                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!");
                }
                //开始构建 root view
                final String name = parser.getName();
                if (TAG_MERGE.equals(name)) {
                  //根布局为merge版
                    if (root == null || !attachToRoot) {
                    //判断根布局是不是merge 如果是的话 需要有父布局并且attachToRoot为true 否则抛出异常
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }

                    rInflate(parser, root, inflaterContext, attrs, false);//这里开始循环解析
                } else {
                       //根布局为非merge版
                    // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);//解析构造整个布局实例

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {//如果root 不为null 然后attachToRoot 为false
                    //只有在这种情况下 他的属性才会被真正设置进去 否则无效 root!=null&&attachToRoot==false (会影响宽高跟margin  ,关于padding 毕竟是作用在view的onDraw方法里面的,还是有效果的)
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }

                    //开始解析布局文件中 子view 循环解析所有子view
                    // 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.addView(temp, params);
                    }

                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) {//判断是返回刚构建的view 还是之前传进来的root
                        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(parser.getPositionDescription()
                        + ": " + 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;
        }
    }

我们先看createViewFromTag方法

private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
        return createViewFromTag(parent, name, context, attrs, false);
    }
//这里才是主导
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) {//默认ignoreThemeAttr 给的是false
            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();//TypedArray用完都是要回收的
        }

        if (name.equals(TAG_1995)) {
            // Let's party like it's 1995!
            return new BlinkLayout(context, attrs);//一种闪烁的布局 
          //想要看效果 可以看http://blog.csdn.net/qq_22644219/article/details/69367150
        }

        try {
            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) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(parent, name, attrs);
                    } else {
                        view = createView(name, null, attrs);//主体还是这个方法 通过反射的方式创建了view
                    }
                } 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, e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;

        } catch (Exception e) {
            final InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class " + name, e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        }
    }

根view创建好了之后,之后会调用 rInflate(parser, root, inflaterContext, attrs, false)一步步的实现每一个子view

 /**
     * Recursive method used to descend down the xml hierarchy and instantiate
     * views, instantiate their children, and then call onFinishInflate().
     * 递归的调用方法构建整个xml的布局,沿层次一个个的实例化view
     */
    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) {//循环拿到那个start_tag
            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);//这里做include内容的解析
            } else if (TAG_MERGE.equals(name)) {//意思是 merge 不能作为一个子view 除非他是根布局
                throw new InflateException("<merge /> must be the root element");
            } else {
              //这里做真正的创建view 还是通过反射的形式 另外把一些属性设置进去 再添加到父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 (finishInflate) {//当根布局不是merge 那就为true
            parent.onFinishInflate();//就是一个回调吧
        }
    }

整个流程就是这样子 ,我们再来看一下,我在最初的时候说 setContentView也是通过LayoutInflate方法的:

 @Override
    public void setContentView(@LayoutRes int layoutResID) {//我们在activity里面的调用 就是这个方法
        getDelegate().setContentView(layoutResID);
    }

//这是一个抽象的方法
    public abstract void setContentView(@LayoutRes int resId);

//然后我们看AppCompatDelegateImplV9的实现
 @Override
    public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        LayoutInflater.from(mContext).inflate(resId, contentParent);//最后 还是调用到这个方法了
        mOriginalWindowCallback.onContentChanged();
    }

总结一下LayoutInflate里面的实现,其实也是很简单的用了android提供的pull解析一步步的解析下来的,里面的每一个节点就构建成一个view了(通过反射),从根布局开始一层层的解析构建,最终形成一个完整的DOM结构,然后把根布局的引用传出去,这样inflate方法就成功完成了。

最后说一下三个inflate的三个参数作用,毕竟有时候会疑惑该如何传参:

  1. 如果root为null,attachToRoot将失去作用,设置任何值都没有意义。
  2. 如果root不为null,attachToRoot设为true,则会给加载的布局文件的指定一个父布局,即root,此时是root设置的那些宽高跟margin ,是没有效果的。
  3. 如果root不为null,attachToRoot设为false,则会将布局文件最外层的所有layout属性进行设置,当该view被添加到父view当中时,这些layout属性会自动生效。
  4. 在不设置attachToRoot参数的情况下,如果root不为null,attachToRoot参数默认为true。

还有一点,请不要在listview\recyclerview里面构建每个item的时候设置root!=null 并且attachToRoot又给了true ,这样会直接报错的,因为

@Override
  public void addView(View child) {
        throw new UnsupportedOperationException("addView(View) is not supported in AdapterView");
  }

参考:

Android LayoutInflater原理分析,带你一步步深入了解View(一)
Android LayoutInflate深度解析 给你带来全新的认识

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容