布局优化是我们 app 优化的第一步,通过Android studio 提供的 Layout Inspector 可以很直接的看到冗余层级,去除这些冗余层级将使我们的 UI 变的更流畅。另外官方不推荐继续使用**** hierarchy Viewer****查看视图层级了。
include 布局
随着开发的页面越来越多,难免会有部分页面的布局中的部分布局重复,比如每个页面的titleView,这些重复的布局不仅仅是冗余代码,后续需要更改调整的时候,需要每个页面都进行调整,极容易出错。
Android 提供的 include 标签可以将一个布局加入到另外一个布局中,这样,重复的布局就可以单独写入一个布局 XML 文件中,方便管理、修改,提高了复用性。
使用示例:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/app_bg"
tools:showIn="@layout/activity_main" // showIn 标签在编译过程中会被删除,仅供开发过程中预览使用
android:gravity="center_horizontal">
<include layout="@layout/titlebar"/>
// include 标签可以包含完整的 layout_* 属性
<include android:id="@+id/news_title"
android:layout_width="match_parent"
android:layout_height="match_parent"
layout="@layout/title"/>
<TextView android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="@string/hello" // 文字仅预览时可见,编译过程中会删除
android:padding="10dp" />
...
</LinearLayout>
include 标签的原理很简单,就是在 解析布局的时候,如果检测到 include 标签,就把该布局下的跟视图添加到 include 所在的父视图中。对于 xml 的解析,最终都会调用 LayoutInflater 的 inflate 方法,该方法最终又会调用到 rInflate 方法,我们看看这个方法:
// 省略其他代码
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
}
从这个 if 条件判断汇总,我们能看到, include 标签不能作为跟标签,否则会抛出异常,继续看看 parseInclude 方法:
private void parseInclude(XmlPullParser parser, Context context, View parent,
AttributeSet attrs) throws XmlPullParserException, IOException {
int type;
if (parent instanceof ViewGroup) {
// 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);
if (layout == 0) { // include 标签必须包含 layout 属性。
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 {
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!");
}
final String childName = childParser.getName();
// merge 标签
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 {
final View view = createViewFromTag(parent, childName,
context, childAttrs, hasThemeOverride);
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 {
params = group.generateLayoutParams(attrs);
} catch (RuntimeException e) {
// Ignore, just fail over to child attrs.
}
if (params == null) {
params = group.generateLayoutParams(childAttrs);
}
view.setLayoutParams(params);
// Inflate all children.
rInflateChildren(childParser, view, childAttrs, true);
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;
}
group.addView(view);
}
} finally {
childParser.close();
}
}
} else {
throw new InflateException("<include /> can only be used inside of a ViewGroup");
}
LayoutInflater.consumeChildElements(parser);
}
merge 标签
如果父视图布局和子视图的根布局一样,那么可以使用 merge
标签来减少视图嵌套层级。在解析 xml 的时候,会自动忽略 merge 标签,直接加载 merge 中的视图。这样,我们就少了一个层级的嵌套,减少了层级,提升了布局性能。
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/add"/>
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/delete"/>
</merge>
merge 标签原理也比较简单,看看 LayoutInflater 的 inflate 方法:
else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
}
在 while 循环中遍历子视图的时候,如果遍历到了 merge 标签会报错,因为 merge 标签只能作为跟标签。
else {
// 根据 tag 创建视图
final View view = createViewFromTag(parent, name, context, attrs);
// 获取 merge 父视图
final ViewGroup viewGroup = (ViewGroup) parent;
// 获取params
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
// 加载子视图
rInflateChildren(parser, view, attrs, true);
// 添加到父视图
viewGroup.addView(view, params);
}
ViewStub 视图
ViewStub是一个不可见的和能在运行时期间延迟加载视图的,宽高都为 0 的视图。当调用 ViewStub 的 inflate 方法 或者是 setVisiable 以后才加载视图,变的可见。这对于某些需要根据特性情况来进行加载的视图比较有效,可以在正常初始化视图的时候节省部分系统资源。
<ViewStub
android:id="@+id/view_stub"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout="@layout/view_stub_main" /> // 注意这里有 android 标签
看看 ViewStub 源码:
public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context);
final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.ViewStub, defStyleAttr, defStyleRes);
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);
}
public void setVisibility(int visibility) {
if (mInflatedViewRef != null) {
View view = mInflatedViewRef.get();
if (view != null) {
view.setVisibility(visibility);
} else {
throw new IllegalStateException("setVisibility called on un-referenced view");
}
} else {
super.setVisibility(visibility);
// 如果还没有加载过,那么加载视图
if (visibility == VISIBLE || visibility == INVISIBLE) {
inflate();
}
}
}
public View inflate() {
final ViewParent viewParent = getParent();
if (viewParent != null && viewParent instanceof ViewGroup) {
if (mLayoutResource != 0) {
final ViewGroup parent = (ViewGroup) viewParent;
// 加载布局
final View view = inflateViewNoAdd(parent);
// 使用自身布局替换父布局中的 ViewStub 标签
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");
}
}
private View inflateViewNoAdd(ViewGroup parent) {
final LayoutInflater factory;
if (mInflater != null) {
factory = mInflater;
} else {
factory = LayoutInflater.from(mContext);
}
// 加载布局
final View view = factory.inflate(mLayoutResource, parent, false);
if (mInflatedId != NO_ID) {
view.setId(mInflatedId);
}
return view;
}
private void replaceSelfWithView(View view, ViewGroup parent) {
final int index = parent.indexOfChild(this);
// 从父布局中删除 ViewStub
parent.removeViewInLayout(this);
// 将自身布局添加到父布局
final ViewGroup.LayoutParams layoutParams = getLayoutParams();
if (layoutParams != null) {
parent.addView(view, index, layoutParams);
} else {
parent.addView(view, index);
}
}
总结
- 尽量使用 RelativeLayout 避免视图嵌套
- 使用
<merge>
标签减少视图嵌套层数 - 使用 ViewStub 加载需要延迟加载的视图
- 在 RecyclerView 等列表视图中,尽量少使用 Lyout_weight 属性
- 将可以复用的视图抽取出来使用
<include>
标签加载
网友评论