布局优化 include viewstub merge 及源码解析

奋斗吧
奋斗吧
擅长邻域:未填写

标签: 布局优化 include viewstub merge 及源码解析 Python博客 51CTO博客

2023-05-29 18:24:14 73浏览

布局优化 include viewstub merge 及源码解析,我只是一个无情的搬运工布局是我们再开发应用时必不可少的工作,通常情况下,布局并不会成为工作中的难点。但是


布局优化 include viewstub merge  及源码解析_ide

我只是一个无情的搬运工

布局是我们再开发应用时必不可少的工作,通常情况下,布局并不会成为工作中的难点。但是,当你的应用变得越来越富咱,页面越来越多时,布局上的优化工作就成了性能优化的第一步。因为布局上的优化并不像其他优化方式那么复杂,通过Android Sdk提供的HierarchyView可以很直接地看到冗余的层级,去除这些多次与的层级将使我们的UI变得更流畅。本小结我们就来学习一些常用的布局优化方式。

1.1 include布局

布局优化 include viewstub merge  及源码解析_xml_02

include标签实现的原理很简单,就是再解析xml布局时,如果检测到include标签,那么直接把该布局下的根视图标签添加到include所在的父视图中。对于布局xml的解析最终都会调用到LayoutInflater的inflate方法,该方法最后又会调用到rInflate方法,我们看看这个方法

/**
     * 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;
		//迭代xml中的所有元素,逐个解析
        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)) { //如果xml中的节点是include节点
                if (parser.getDepth() == 0) {      // 则调用parseInclude方法
                    throw new InflateException("<include /> cannot be the root element");
                }
				//调用parseInclude解析include标签
                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();
        }
    }

方法就其实就是遍历xml中的所有元素,然后逐个进行解析。例如,解析到一个TextView标签,那么就根据用户设置的一些layout_width、layout_height、id等属性来构造一个TextView对象,然后添加到父控件(ViewGroup类型)中,include标签也是一样的,我们看到遇到include标签时,会调用parseInclude函数,这就是对include标签的解析,我们看看下面的程序:

private void parseInclude(XmlPullParser parser, Context context, View parent,
            AttributeSet attrs) throws XmlPullParserException, IOException {
        int type;

        if (parent instanceof ViewGroup) {
            // Apply a theme wrapper, if requested. This is sort of a weird
            // edge case, since developers think the <include> overwrites
            // values in the AttributeSet of the included View. So, if the
            // included View has a theme attribute, we'll need to ignore it.
            final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
            final int themeResId = ta.getResourceId(0, 0);
            final boolean hasThemeOverride = themeResId != 0;
            if (hasThemeOverride) {
                context = new ContextThemeWrapper(context, themeResId);
            }
            ta.recycle();

            // If the layout is pointing to a theme attribute, we have to
            // massage the value to get a resource identifier out of it.
            int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
			//include标签中没有设置layout属性,会抛出异常
			//没有指定布局xml,那么include就无意义了
            if (layout == 0) {
                final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
                if (value == null || value.length() <= 0) {
                    throw new InflateException("You must specify a layout in the"
                            + " include tag: <include layout=\"@layout/layoutID\" />");
                }

                // Attempt to resolve the "?attr/name" string to an attribute
                // within the default (e.g. application) package.
                layout = context.getResources().getIdentifier(
                        value.substring(1), "attr", context.getPackageName());

            }

            // The layout might be referencing a theme attribute.
            if (mTempValue == null) {
                mTempValue = new TypedValue();
            }
            if (layout != 0 && context.getTheme().resolveAttribute(layout, mTempValue, true)) {
                layout = mTempValue.resourceId;
            }

            if (layout == 0) {
                final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
                throw new InflateException("You must specify a valid layout "
                        + "reference. The layout ID " + value + " is not valid.");
            } else {
                final XmlResourceParser childParser = context.getResources().getLayout(layout);

                try {
					//获取属性集,即 在include标签中设置的属性
                    final AttributeSet childAttrs = Xml.asAttributeSet(childParser);
					//如果不是起始或者结束标识,那么解析洗一个元素
                    while ((type = childParser.next()) != XmlPullParser.START_TAG &&
                            type != XmlPullParser.END_DOCUMENT) {
                        // Empty.
                    }

                    if (type != XmlPullParser.START_TAG) {
                        throw new InflateException(childParser.getPositionDescription() +
                                ": No start tag found!");
                    }
					// 1. 解析include中的第一个元素
                    final String childName = childParser.getName();

                    if (TAG_MERGE.equals(childName)) {
                        // The <merge> tag doesn't support android:theme, so
                        // nothing special to do here.
                        rInflate(childParser, parent, context, childAttrs, false);
                    } else {
                    	//2. 例子中的情况会走到这一步,首先根据include的属性集
                    	//创建被include进来的xml布局的根 view
                    	//这里的根view对应为my_title_layout.xml中的 RelativeLayout
                        final View view = createViewFromTag(parent, childName,
                                context, childAttrs, hasThemeOverride);
						// include标签的parent view
                        final ViewGroup group = (ViewGroup) parent;

                        final TypedArray a = context.obtainStyledAttributes(
                                attrs, R.styleable.Include);
                        final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
                        final int visibility = a.getInt(R.styleable.Include_visibility, -1);
                        a.recycle();

                        // We try to load the layout params set in the <include /> tag.
                        // If the parent can't generate layout params (ex. missing width
                        // or height for the framework ViewGroups, though this is not
                        // necessarily true of all ViewGroups) then we expect it to throw
                        // a runtime exception.
                        // We catch this exception and set localParams accordingly: true
                        // means we successfully loaded layout params from the <include>
                        // tag, false means we need to rely on the included layout params.
                        ViewGroup.LayoutParams params = null;
                        try { //3. 获取布局属性
                            params = group.generateLayoutParams(attrs);
                        } catch (RuntimeException e) {
                            // Ignore, just fail over to child attrs.
                        }
                        if (params == null) { //被 include 进来的根 view 设置布局参数
                            params = group.generateLayoutParams(childAttrs);
                        }
                        view.setLayoutParams(params);

                        // Inflate all children. 解析所有子控件
                        rInflateChildren(childParser, view, childAttrs, true);
						// 5. 如果include设置了id,则会将include中设置的id
						// 设置给comm_title.xml中的根view,因此,实际上
						// common_title.xml中的RelativeLayout的id会变成
						//include标签中的id
                        if (id != View.NO_ID) {
                            view.setId(id);
                        }

                        switch (visibility) {
                            case 0:
                                view.setVisibility(View.VISIBLE);
                                break;
                            case 1:
                                view.setVisibility(View.INVISIBLE);
                                break;
                            case 2:
                                view.setVisibility(View.GONE);
                                break;
                        }
						//6. 最后将common_title.xml中的根view添加到它的上一层父控件中
                        group.addView(view);
                    }
                } finally {
                    childParser.close();
                }
            }
        } else {
            throw new InflateException("<include /> can only be used inside of a ViewGroup");
        }

        LayoutInflater.consumeChildElements(parser);
    }

整个过程就是根据不同的标签解析不同的元素,首先会解析include元素,然后再解析被include进来的布局的root view元素。在我们的例子中,对应的root view就是RelativeLayout,然后再解析root view下面的所有元素,这个过程是上面注释的2~4的过程,然后是设置布局参数。我们看到,注释5处会判断include标签的id,如果不是View.NO_ID的画会把该id设置给呗引入的布局根元素的id,即此时在我们的例子中common_title.xml的根元素Relatvielayout的id被设置成了include标签中的top_title,即RelativeLayout的id被动态修改了。最终被include进来的布局的根视图会被添加到它的parent view中,也就实现了include功能。

1.2 merge布局

布局优化 include viewstub merge  及源码解析_ide_03

/**
     * Inflate a new view hierarchy from the specified XML node. Throws
     * {@link InflateException} if there is an error.
     * <p>
     * <em><strong>Important</strong></em>   For performance
     * reasons, view inflation relies heavily on pre-processing of XML files
     * that is done at build time. Therefore, it is not currently possible to
     * use LayoutInflater with an XmlPullParser over a plain XML file at runtime.
     *
     * @param parser XML dom node containing the description of the view
     *        hierarchy.
     * @param root Optional view to be the parent of the generated hierarchy (if
     *        <em>attachToRoot</em> is true), or else simply an object that
     *        provides a set of LayoutParams values for root of the returned
     *        hierarchy (if <em>attachToRoot</em> is false.)
     * @param attachToRoot Whether the inflated hierarchy should be attached to
     *        the root parameter? If false, root is only used to create the
     *        correct subclass of LayoutParams for the root view in the XML.
     * @return The root View of the inflated hierarchy. If root was supplied and
     *         attachToRoot is true, this is root; otherwise it is the root of
     *         the inflated XML file.
     */
    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 {
                final String name = parser.getName();

                if (DEBUG) {
                    System.out.println("**************************");
                    System.out.println("Creating root view: "
                            + name);
                    System.out.println("**************************");
                }
				// m 如果是merge标签,那么调用rInflate进行解析
                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");
                    }
					// 解析merge标签
                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                }

            } catch (XmlPullParserException e) {}

            return result;
        }
    }

从上述程序中可以看到,再inflate函数中会循环解析xml中的tag,如果解析到merge标签则会调用rinflate函数。我们看看该函数中与merge相关的实现:

/**
     * 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;
		//1. 迭代xml中的所有元素,逐个解析
        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)) { //如果xml中的节点是include节点
                if (parser.getDepth() == 0) {      // 则调用parseInclude方法
                    throw new InflateException("<include /> cannot be the root element");
                }
				//调用parseInclude解析include标签
                parseInclude(parser, context, parent, attrs);
            } else if (TAG_MERGE.equals(name)) {
                throw new InflateException("<merge /> must be the root element");
            } else { //我们的merge标签会进入这里
            // 2.根据tag创建视图
                final View view = createViewFromTag(parent, name, context, attrs);
			// 将merge标签的parent转换为ViewGroup
                final ViewGroup viewGroup = (ViewGroup) parent;
			// 获取布局参数
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
			// 3. 递归鸡西每个子元素
				rInflateChildren(parser, view, attrs, true);
			// 4.将子元素直接添加到merge标签的parent view 中
                viewGroup.addView(view, params);
            }
        }

        if (pendingRequestFocus) {
            parent.restoreDefaultFocus();
        }

        if (finishInflate) {
            parent.onFinishInflate();
        }
    }

在rinflate函数中,如果是merge标签,我们会进入到最后一个else分支。而此时在while循环中迭代查找的就是merge标签下的子视图,因为merge标签在inflate函数中已经被解析掉了。因此此时在rinflate中只解析merge的子视图,在最后一个else分支中,LayoutInflator首先通过tag创建各个子视图,然后设置视图参数、递归解析子视图下的子视图,最后,merge标签的各个子视图添加到merge标签的parent视图中,这样一来,就成功地甩掉了mege标签

1.3 ViewStub视图

ViewStub是一个不可见的和能在运行期间延迟加载目标视图的、高度都为0的View。当对一个ViewStub调用inflate()方法或设置它可见时,系统就会加载在ViewStub标签中指定的布局,然后将这个布局的根视图添加到ViewStub的父视图中。也就是说,在对ViewStub调用inflate()方法或设置visiable之前,它不占用布局空间和系统资源的,它知识一个为目标视图占了一个位置而已。当我们只需要在某些情况下才加载一些耗费资源的布局时候,ViewStub就成了我们实现这个功能的重要手段。

布局优化 include viewstub merge  及源码解析_xml_04

布局优化 include viewstub merge  及源码解析_ide_05

布局优化 include viewstub merge  及源码解析_ide_06

public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context);

        final TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.ViewStub, defStyleAttr, defStyleRes);
		//获取 inflatedId属性
        mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
		//获取目标布局
        mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
        mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
        a.recycle();

        setVisibility(GONE); //设置不可见
        setWillNotDraw(true); //设置不绘制内容
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(0, 0); //宽高都为0
    }


    private View inflateViewNoAdd(ViewGroup parent) {
        final LayoutInflater factory;
        if (mInflater != null) {
            factory = mInflater;
        } else {
            factory = LayoutInflater.from(mContext);
        }
		//1. 加载目标布局
        final View view = factory.inflate(mLayoutResource, parent, false);
		//2.  设置为目标布局根元素的id
        if (mInflatedId != NO_ID) {
            view.setId(mInflatedId);
        }
        return view;
    }

    private void replaceSelfWithView(View view, ViewGroup parent) {
        final int index = parent.indexOfChild(this);
		// 3. 将ViewStub 自身从父视图中移除
        parent.removeViewInLayout(this);

        final ViewGroup.LayoutParams layoutParams = getLayoutParams();
		// 4. 判断ViewStub是否设置了布局参数
		// 然后将目标布局的根元素添加到ViewStub的父控件中
        if (layoutParams != null) {
            parent.addView(view, index, layoutParams);
        } else {
            parent.addView(view, index);
        }
    }


    public View inflate() {
        final ViewParent viewParent = getParent();

        if (viewParent != null && viewParent instanceof ViewGroup) {
            if (mLayoutResource != 0) {
				// 将视图转为ViewGropu类型
                final ViewGroup parent = (ViewGroup) viewParent;
                final View view = inflateViewNoAdd(parent);
                replaceSelfWithView(view, parent);

                mInflatedViewRef = new WeakReference<>(view);
                if (mInflateListener != null) {
                    mInflateListener.onInflate(this, view);
                }

                return view;
            } else {
                throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
            }
        } else {
            throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
        }
    }


好博客就要一起分享哦!分享海报

此处可发布评论

评论(0展开评论

暂无评论,快来写一下吧

展开评论

您可能感兴趣的博客

客服QQ 1913284695