我只是一个无情的搬运工
布局是我们再开发应用时必不可少的工作,通常情况下,布局并不会成为工作中的难点。但是,当你的应用变得越来越富咱,页面越来越多时,布局上的优化工作就成了性能优化的第一步。因为布局上的优化并不像其他优化方式那么复杂,通过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> 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");
}
}