佈局優化 include viewstub merge 及源碼解析

在這裏插入圖片描述

我只是一個無情的搬運工

佈局是我們再開發應用時必不可少的工作,通常情況下,佈局並不會成爲工作中的難點。但是,當你的應用變得越來越富咱,頁面越來越多時,佈局上的優化工作就成了性能優化的第一步。因爲佈局上的優化並不像其他優化方式那麼複雜,通過Android Sdk提供的HierarchyView可以很直接地看到冗餘的層級,去除這些多次與的層級將使我們的UI變得更流暢。本小結我們就來學習一些常用的佈局優化方式。

1.1 include佈局

在這裏插入圖片描述

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佈局

在這裏插入圖片描述

    /**
     * Inflate a new view hierarchy from the specified XML node. Throws
     * {@link InflateException} if there is an error.
     * <p>
     * <em><strong>Important</strong></em>&nbsp;&nbsp;&nbsp;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就成了我們實現這個功能的重要手段。

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

    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");
        }
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章