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