Android 界面的绘制与渲染1——setContentView()解析

241 阅读7分钟

这是Android 界面的绘制与渲染系列的第二篇。

这篇我们来从setContentView() 方法入手来看看View 间是如何布局排列的。 需要注意的是,如果我们的自定义Activity 继承自Activity 或FragmentActivity, setContentView() 方法是调用的Window 的setContentView() 方法:

    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

如果我们的Activity 是继承自AppCompatActivity, 那么setContentView() 方法调用的是AppCompatDelegate 类的setContentView() 方法:

    @Override
    public void setContentView(@LayoutRes int layoutResID) {
        getDelegate().setContentView(layoutResID);
    }

而继承父类不同的差别可以理解为Activity 和FragmentActivity 是基于运行平台的,仅仅能使用运行平台的特性代码,适配不同Android 版本的代码需要我们自己处理,而继承AppCompatActivity ,其内部包装了AppCompatDelegate及其实现类,帮我们做了运行平台代码的适配,简化我们的代码,让我们更加专注于业务实现。其实这两者的代码在这里都是殊途同归的,最后都会调用:

LayoutInflater.from(mContext).inflate(resId, contentParent);

这篇文章所讲述的内容都在这行代码中,当然调用这句代码之前还会生成DecorView,这点就留给读者自行查看了。

继续跟进这行代码,发现它会调用下面的inflate() 方法:

    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) + ")");
        }

		//1.进行预编译,属于优化
        View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
        if (view != null) {
            return view;
        }
        //2.生成xml的解析器
        XmlResourceParser parser = res.getLayout(resource);
        try {
        		//3.返回解析好的View
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

我们来看看2处是如何生成解析器的

    @NonNull
    XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie,
            @NonNull String type)
            throws NotFoundException {
        if (id != 0) {
            try {
                synchronized (mCachedXmlBlocks) {
                    final int[] cachedXmlBlockCookies = mCachedXmlBlockCookies;
                    final String[] cachedXmlBlockFiles = mCachedXmlBlockFiles;
                    final XmlBlock[] cachedXmlBlocks = mCachedXmlBlocks;
                    // First see if this block is in our cache.
                    final int num = cachedXmlBlockFiles.length;
                    for (int i = 0; i < num; i++) {
                        if (cachedXmlBlockCookies[i] == assetCookie && cachedXmlBlockFiles[i] != null
                                && cachedXmlBlockFiles[i].equals(file)) {
                            return cachedXmlBlocks[i].newParser(id);
                        }
                    }

                    // Not in the cache, create a new block and put it at
                    // the next slot in the cache.
                    final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file);
                    if (block != null) {
                        final int pos = (mLastCachedXmlBlockIndex + 1) % num;
                        mLastCachedXmlBlockIndex = pos;
                        final XmlBlock oldBlock = cachedXmlBlocks[pos];
                        if (oldBlock != null) {
                            oldBlock.close();
                        }
                        cachedXmlBlockCookies[pos] = assetCookie;
                        cachedXmlBlockFiles[pos] = file;
                        cachedXmlBlocks[pos] = block;
                        return block.newParser(id);
                    }
                }
            } catch (Exception e) {
                final NotFoundException rnf = new NotFoundException("File " + file
                        + " from xml type " + type + " resource ID #0x" + Integer.toHexString(id));
                rnf.initCause(e);
                throw rnf;
            }
        }

        throw new NotFoundException("File " + file + " from xml type " + type + " resource ID #0x"
                + Integer.toHexString(id));
    }

其实通过上述代码我们可以看出一些逻辑:Google内部对XML文件解析器相关资源也做了缓存优化,先在缓存中获取,如果没有生成并放在缓存中方便下次读取。那么block.newParser(id); 返回的又是什么呢?

    public XmlResourceParser newParser(@AnyRes int resId) {
        synchronized (this) {
            if (mNative != 0) {
                return new Parser(nativeCreateParseState(mNative, resId), this);
            }
            return null;
        }
    }

返回的是一个Parser类,而它所实现的接口是XmlPullParser,也就是说,Android 中的XML解析是使用Pull 方式解析的。这里就不对Pull解析进行会展开了,读者可在文后查看附带的扩展链接进行了解。

下面我们继续解析inflate() 方法:

	//因为原始代码过多和有些调试代码,所以做了一些删除处理,我们只分析关键代码
    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
           
            View result = root;

            try {
            
                final String name = parser.getName();

				//如果是merge标签,直接初始化XML文件内部的View并添加到root
                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 {
                	//如果是正常ViewGroup节点,先初始化该节点,再初始化内部View,最后添加到root
                    // 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) {
                       
                        // 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);
                        }
                    }

                    // 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) {
                        result = temp;
                    }
                }

            }

            return result;
        }
    }

在后面的代码中我们会了解到上面的rInflateChildren() 方法会调用上面的createViewFromTag() 方法,所以,我们先来看看rInflateChildren() 方法:

    /**
     * Recursive method used to descend down the xml hierarchy and instantiate
     * views, instantiate their children, and then call onFinishInflate().
     * <p>
     * <strong>Note:</strong> Default visibility so the BridgeInflater can
     * override it.
     */
    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();
        }
    }

虽然这个方法是有些长,但由于我认为它是整个setContentView() 方法调用过程中最精彩的地方,所以就全放出来。需要注意的是该方法在rInflateChildren() 方法中调用了自身,所以该方法使用了递归处理。现在让我们分三部分进行分析,第一部分:

        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;
            }
       }

第一行代码我们可以是获取当前的深度,这个没什么,如果我们点进去发现就是直接获取深度。第二句type = parser.next(), 是通过解析器获取下一个标签的名称,next() 方法是个抽象方法,我们来看一下Parser 类内部的实现:

        public int next() throws XmlPullParserException,IOException {
            if (!mStarted) {
                mStarted = true;
                return START_DOCUMENT;
            }
            if (mParseState == 0) {
                return END_DOCUMENT;
            }
            int ev = nativeNext(mParseState);
            if (mDecNextDepth) {
                mDepth--;
                mDecNextDepth = false;
            }
            switch (ev) {
            case START_TAG:
                mDepth++;
                break;
            case END_TAG:
                mDecNextDepth = true;
                break;
            }
            mEventType = ev;
            if (ev == END_DOCUMENT) {
                // Automatically close the parse when we reach the end of
                // a document, since the standard XmlPullParser interface
                // doesn't have such an API so most clients will leave us
                // dangling.
                close();
            }
            return ev;
        }

可以看到next() 方法里对XML树深度有所控制,就是解析到开始标签深度就会加一,解析到结束标签深度就会减一 要知道就XML解析而言,<a></a><a/>是完全一样的,所以:

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
   和
       <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
    </TextView>
   解析是一样的

所以再回到第一部分的while 循环处,每个层级的View 因为都有开始标签,所以一定都会进入循环中,而在第二遍都会因为结束标签而终止,ViewGroup会因为还没遇到自身的结束标签就一直遍历其容器内的子View, View 会因为下一个就是自身的结束标签而中止再次进入循环体内。 第二部分,则是下面的代码:

  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 {}

这一部分其实就是获取当前解析的标签名称,并对一些特殊的标签进行处理,如果有读者感兴趣,可以自行解读。

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);
            }

第三部分依然还在循环体内,可以看到在上面看到调用了createViewFromTag() 方法进行View的初始化,并添加到ViewGroup中,因为第一部分我们分析了,普通的View会因为紧随而来的结束标签而停止进入循环体内,进入循环体内的parent就一定是ViewGroup 级别的。

那么我们现在来看看createViewFromTag() 方法

 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) {
            throw e;

        } 

在这个方法中大部分都是不需要我们关注的代码,所以,我抽取了一些主要的代码进行分析,我们可以看到Google首先调用了tryCreateView() 方法进行View的创建,读者之后可以详细查看此方法的调用,根据Google的描述这个方法适用于特定的场景,当然不在这次的解析中。我们可以看到在trycatch代码中首先对name进行了判断,根据包名分隔符的.来判断是系统内置的View还是应用包内自带的View,如果是系统内置的View,它还是要依然全路径的:

    protected View onCreateView(String name, AttributeSet attrs)
            throws ClassNotFoundException {
        return createView(name, "android.view.", attrs);
    }

所以最后都调用的是createView() 方法,那么我们继续跟进:


    @Nullable
    public final View createView(@NonNull Context viewContext, @NonNull String name,
            @Nullable String prefix, @Nullable AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        if (constructor != null && !verifyClassLoader(constructor)) {
            constructor = null;
            sConstructorMap.remove(name);
        }
        Class<? extends View> clazz = null;

        try {
          

            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                        mContext.getClassLoader()).asSubclass(View.class);

            
                constructor = clazz.getConstructor(mConstructorSignature);
                constructor.setAccessible(true);
                sConstructorMap.put(name, constructor);
            } else {
                // If we have a filter, apply it to cached constructor
                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 = Class.forName(prefix != null ? (prefix + name) : name, false,
                                mContext.getClassLoader()).asSubclass(View.class);

                        boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
                        mFilterMap.put(name, allowed);
                        if (!allowed) {
                            failNotAllowed(name, prefix, viewContext, attrs);
                        }
                    } else if (allowedState.equals(Boolean.FALSE)) {
                        failNotAllowed(name, prefix, viewContext, attrs);
                    }
                }
            }

            Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = viewContext;
            Object[] args = mConstructorArgs;
            args[1] = attrs;

            try {
                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]));
                }
                return view;
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        }
    }

其实这个方法里的代码也有很多,大部分是trycatch代码,所以也是摘取了一部分,而从这部分代码中,我们也是可以看到Google为了优化而做的缓存处理,并使用了反射获取到View的构造方法,最终对View调用两个参数的构造方法初始化View。

那么到这里,setContentView()解析就算是结束了,这仅仅是将XML布局文件进行解析生成View,挂载到View树上,那么它们是什么时候绘制的呢?我们后面的文章会讲到。个人觉得整个解析过程在对递归部分代码的理解还是比较耗时的,而其他部分的代码如果已经有了一定的开发经验也会很好理解。下一篇我们将会对Activity的onCreate(), onResume() 生命周期的调用进行解析,为View的绘制打好基础。

参考

《Android 源码设计模式解析与实战》

XML 语法规则

xml中非空元素和空元素的区别

Android 之 SAX、DOM 和 Pull 解析 XML