我只是一個無情的搬運工
佈局是我們再開發應用時必不可少的工作,通常情況下,佈局並不會成爲工作中的難點。但是,當你的應用變得越來越富咱,頁面越來越多時,佈局上的優化工作就成了性能優化的第一步。因爲佈局上的優化並不像其他優化方式那麼複雜,通過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");
}
}