由 setContentView 看 Android View 的渲染流程

1,937 阅读5分钟

这是我参与8月更文挑战的第6天,活动详情查看: 8月更文挑战

借 8 月更文挑战督促自己,感谢掘金!

一、背景

昨天,在谈论 RecyclerView 的复用机制的时候,说到了事件的分发以及 View 的大致渲染流程,今天看了一下源码,做一下记录。

二、流程分析

1、入口

如果创建一个Activity ,系统为我们默认继承 AppCompatAvtovity ,而不是 Activity。在这里,我们把自定义的 Activity 改成继承 Activity,然后进入 setContentView,代码如下:

第一行代码,获取了一个 Window 对象,然后调用这个 Window 的 setContentView ,点进源码一看,这个 Window 是一个抽象类:

通过源码知道,这个 Window 有一个唯一的实现类 PhoneWindow,接下来看 PhoneWindow 的 setContentView:

在 PhoneWindow 的setContentView 主要做了两件事情:

  • 创建一个 DecorView。
  • 通过 LayoutInflater.inflate 去加载布局。

什么是 DecorView 呢?在 PhoneWindow 中,通过查看源码可以知道,这是一个顶层的 View,承载我们整个视图树,源码如下:

接下来是真正的 View 的渲染流过 LayoutInflater.inflate。

2、渲染流程

LayoutInflater 有四个不同的构造方法,但是对视图的渲染很明显最终会走到下面的这个方法中:

这里有三个参数,parser 、root 以及 attachToRoot ,我们在使用 inflate 方法时通常传入的是 布局文件,布局文件的会被映射成一个 int 类型的值,那么它是在什么时候进行转化的呢?

看看我们调用时的构造方法

解析的重点已经标注了,获得 parser 之后把它作为参数传递到另一个构造函数,来看一下获取parser 最终调用的函数:

具体过程已经折叠了,需要注意的是在转换的过程中有一个 加锁 的行为。接下来看最终调用的函数(已删除不重要的代码):

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
  			// 1、在 inflate 的过程中使用了 synchronized,这就意味着不可能同时存在多个线程对同一个 View 进行渲染
        synchronized (mConstructorArgs) {
            final Context inflaterContext = mContext;
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            View result = root;

            try {
                advanceToRootNode(parser);
                final String name = parser.getName();
              // 2、如果最外层的节点是 merge 标签,就要求传入的 root 不为 null,并且传入 attachToRoot 为 true,否则抛出异常
                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }
                  // 重点一
                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                // 重点二
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                    ViewGroup.LayoutParams params = null;
                  
                  // 3、第一种情况 root != null
                    if (root != null) {
                      // 获取布局参数
                        params = root.generateLayoutParams(attrs);
                      
                        if (!attachToRoot) {
                            temp.setLayoutParams(params);
                        }
                    }
                  // 重点三
                    rInflateChildren(parser, temp, attrs, true);
                  
                  // 4、第二种情况 root != null && attachToRoot
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }
                  
                  // 5、第三种情况,root == null || !attachToRoot
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }
            } catch (XmlPullParserException e) {
            } finally {
            }
            return result;
        }
    }

分析都在代码里了,现在来对上面的分析做一下总结。

  • 首先会对最外层的标签进行检测,如果是 merger 的话,需要做特殊处理,如果这里不抛出异常的话,就会执行rInflate
  • createViewFromTag 主要是通过节点名创建 View 对象。
  • 会对传入的 rootattachToRoot进行判断,总共有三种情况
    • root !=null && attachToRoot 为 false 的情况,会对最外层的 layout 属性进行设置。

    • root !=null && attachToRoot 为 true 的情况,会给加载的布局指定一个父布局。

    • root == null 的话,attachToRoot 不起任何作用。

我们似乎还有细节没有弄清楚,rInflate() 的执行流程是怎么样的?createViewFromTag()又是怎么通过 tag 创建 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();
    }
    try {
    // 重点关注
        View view = tryCreateView(parent, name, context, attrs);
        if (view == null) {
            final Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = context;
            try {
                if (-1 == name.indexOf('.')) {
                    view = onCreateView(context, parent, name, attrs);
                } else {
                    view = createView(context, name, null, attrs);
                }
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        }
        return view;
    } catch (InflateException e) {
    } catch (ClassNotFoundException e) {
    } catch (Exception e) {
    }
}

在 createViewFromTag() 中其实需要重点关注 tryCreateView()

image.png

通过源码可以发现,这里通过 Factory 的 onCreateView() 方法创建 View,那这个 Factory 又是什么呢?

image.png

可以看到,Factory 提供了一种 hook,方便我们来拦截使用 LayoutInflate 创建 View 的过程。因此,我们可以利用这种方式来把系统的的 View 替换成 自定义的 View。

到此为止,我们看完了 createViewFromTag() 的流程,现在来看 rInflate() 的相关执行流程:

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)) {
            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 {
            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) {
        parent.onFinishInflate();
    }
}

代码其实很好理解,在这里获取 View 的层级,从 父 View 一级一级进行递归调用。

三、总结

总结一下,我们需要继承 Activity ,而不是 AppCompatActivity,这两者有什么区别呢?通过源码可以看到,AppCompatActivity 是 Activity 的子类,与 Activity 相比,它会在界面中设置标题栏。当然这是可以隐藏的,只需要在代码中进行控制就可以了。

总结一下大致流程:

  • Activity 中,setContentView() 其实是获取 PhoneWindow 的实例,最后实际上是调用了 PhoneWindowsetContentView()
  • 在 PhoneWindow 中,如果没有初始化 decorView 就先进行初始化,然后开始执行 LayoutInflateinflate() 方法。
  • 在 inflate() 中使用 tag name 反射创建创建 View,然后再通递归调用解析 View,展示出来。

另外,由于水平有限,有错误的地方在所难免,未免误导他人,欢迎大佬指正!码字不易,感谢大家的点赞关注!🙏