AndroidUI優化實踐
前言
當項目發展到一定規模后,我們都會遇到性能瓶頸,黑屏、卡頓等,其中一個原因就是我們App某些頁面布局過于復雜,重繪嚴重。為了解決此問題,我們需要利用某些UI調試工具,優化布局。
相關知識點
1.Android系統每隔16ms發出VSYNC信號,觸發對UI進行渲染,如果每次渲染都成功,這樣就能夠達到流暢的畫面所需要的60fps,為了能夠實現60fps,這意味著程序的大多數操作都必須在16ms內完成。
在此,我們需要明白一個概念。如果一次UI渲染不能在16ms內完成,那么將會出現丟幀的現象,從而產生卡頓。譬如,ListView的item布局很復雜,當滑動ListView時,就可能發生卡頓的情況。
2.Overdraw(過度繪制)描述的是屏幕上的某個像素在同一幀的時間內被繪制了多次。在多層次的UI結構里面,如果不可見的UI也在做繪制的操作,這就會導致某些像素區域被繪制了多次。這就浪費大量的CPU以及GPU資源。
我們知道手機屏幕是有X軸和Y軸的概念,其實在系統中還有一個Z軸的概念。由WindowManagerService來控制View在Z軸的位置,Z軸“位置高”的View顯示在“位置低”的View之上。如果某一個像素點有很多View都“路過”,那么就會導致過度繪制的發生。
UI優化工具
1.hierarchyviewer
hierarchyviewer是android sdk提供的UI分析工具,該工具在sdk目錄下的tools文件夾內。使用該工具可以查看某Activity的布局層次,也可以觀察每個view的measure、layout、draw所需時間。該工具是UI優化一大利器。關于hierarchyviewer的介紹和使用,網上有大量資料可以查看。所以本文就不再啰嗦了。
2.?調試GPU過度渲染
該工具是android手機開發者選項中提供的一個UI調試工具,不同手機ROM該工具的名字可能不一樣,但一般都會帶有“過度”的字樣。如下圖是三星某款手機界面。
過度渲染工具使用
接著我們來使用該工具看看QQ首頁的情況。
QQ界面
打開該工具后,我們會發現應用界面都變的花花綠綠了。通過觀察這些顏色,我們就可以知道哪些區域出現過度繪制。那么不同的顏色分別代表什么呢。我們來看下圖。
摘自Android性能優化典范
淺藍色代表屏幕上一個像素只被繪制了一次,此為最優解。
薄荷綠代表屏幕上一個像素被繪制了兩次,這種情況可以接受。
淺粉色代表屏幕上一個像素被繪制了三次,這種情況還可以忍受。
紅色代表屏幕上一個像素被繪制了四次及以上,這種情況就不能忍了。
如果App某個界面大部分區域都是紅色,那我們就得好好優化該界面了。
?3.GPU呈現模式分析
該工具是android手機開發者選項中提供的一個UI調試工具,不同手機ROM該工具的名字可能不一樣,但一般都會帶有“GPU”的字樣。如下圖是三星某款手機界面。
GPU呈現模式
我們看到屏幕上有很多不同顏色的“柱子”,不同的顏色代表不同的操作。具體是什么,感興趣的可以查下資料。另外還有一條綠色的水平線,這條線是一條基準線,代表60fps,即VSYNC 信號時間間隔16ms。
上圖兩個區域都有柱狀圖,和兩條綠色水平線。這是因為每個Window都會有自己GPU呈現模式分析,Activity是一個Window,Dialog也是一個Window。
如果大部分“柱子”都超過綠色水平線,說明此頁面出現嚴重丟幀現象。所以為了避免卡頓的情況,我們應該盡量維持在綠色水平線之下。
該工具還有一個作用,當界面正在繪制的時候,柱狀圖是不會不停的往前走。如果柱狀圖在動,我們就知道此時正在發生UI繪制。
有一次我在利用該工具優化UI的時候,發現我們應用首頁每個一段時間都會發生UI繪制。但是肉眼沒有看到View在“動”。后來通過打印堆棧信息發現,是某一個View的動畫導致的,因為該View在布局最底部,當該View滑出屏幕的時候,其實動畫還是繼續執行。為了解決該問題,我們應該做一個判斷,當View為不可見的時候,停止動畫。
UI優化情景分析
減少布局層次
1.merge標簽的使用。
使用merge可以減少不必要的層級。
比如,自定義一個控件且繼承LinearLayout,若該自定義控件需要填充布局,那么此時,我們就可以使用merge標簽。
public class TestLinearLayout extends LinearLayout {
public TestLinearLayout(Context context) {
super(context);
init();
}
public TestLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public TestLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
setOrientation(VERTICAL);
View.inflate(getContext(), R.layout.test_layout, this);
}
}</code></pre>
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</merge></code></pre>
上述例子中,使用merge標簽將上述布局填充到TestLinearLayout中,因為TestLinearLayout本身就是繼承LinearLayout,擁有LinearLayout的屬性。如果我們將布局文件修改為:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout></code></pre>
然后再填充到TestLinearLayout中,這時候就相當于多了一層無用的LinearLayout。
2.ViewStub標簽的使用
ViewStub標簽使我們很少使用,但又是很重要的一個標簽,該標簽的作用是用于懶加載布局,當系統碰到ViewStub標簽的時候是不進行任何處理(measure、layout等),比設置View隱藏、不可見更高效。當我們真正需要顯示某一個布局的時候才去渲染。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
上述布局是系統Activity中某種布局。該布局文件對Activity布局中的actionBar進行了懶加載處理,畢竟不是每個Activity都需要actionBar。
3.利用高級View的特殊屬性
1)利用TextView滑動屬性來避免嵌套一層ScrollView。
在xml中添加TextView的屬性:
android:scrollbars = "vertical"
然后在代碼中添加:
textView.setMovementMethod(new ScrollingMovementMethod())
2)利用TextView的drawableEnd屬性等設置icon。
3)在TextView設置文本中利用換行符達到上下標簽的功能。
4)利用GridLayout來完成宮格布局,GridLayout是非常好使的一個View,多多使用吧。
4.自定義View來減少布局層級
這方面的優化一般優先級較低,隨著大家修為的提升,這方面的工作也越來越得心應手。
減少過渡繪制
1.謹慎設置View的背景

帶分割線的布局
很多情況下,我們會有上圖的布局需求。要求每個條目之間有分割線。
很多情況下我們都是直接給XXLayout設置背景顏色,然后通過margin來產生分割線。然后在加上Activity的背景顏色和TextView背景顏色,這樣就出現過度繪制的情況。
該情況我們可以通過LinearLayout的分割線屬性來解決,同時我們也可以自定義一個ILinearLayout并集成LinearLayout來完善分割線的需求。如下代碼。
public class ILinearLayout extends LinearLayout {
private Paint mPaint;
private boolean mHeaderDividersEnable = false;
private boolean mFooterDividersEnable = false;
private int mLineWidth = DensityUtil.dip2px(getContext(), 0.5f);
public ILinearLayout(Context context) {
super(context);
initResource();
}
/**
* add custom attributeSet
**/
public ILinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
String nameSpace = "http://schemas.android.com/apk/res-auto";
mHeaderDividersEnable = attrs.getAttributeBooleanValue(nameSpace, "headDividerEnable", false);
mFooterDividersEnable = attrs.getAttributeBooleanValue(nameSpace, "footerDividerEnable", false);
initResource();
}
public ILinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initResource();
}
private void initResource() {
if (mPaint == null) {
mPaint = new Paint();
mPaint.setColor(Color.parseColor("#cccccc"));
mPaint.setStrokeWidth(mLineWidth);
}
setShowDividers(SHOW_DIVIDER_MIDDLE);
GradientDrawable gd = new GradientDrawable();
Drawable drawable = getResources().getDrawable(R.drawable.linearlayout_divider);
setDividerDrawable(drawable);
}
/**
* 設置頭部線是否可見
*/
public void setShowHeaderDividers(boolean enable) {
mHeaderDividersEnable = enable;
}
/**
* 設置尾部線是否可見
*/
public void setShowFooterDividers(boolean enable) {
mFooterDividersEnable = enable;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mHeaderDividersEnable) {
canvas.drawLine(0, 0, getMeasuredWidth(), 0, mPaint);
}
if (mFooterDividersEnable) {
canvas.drawLine(0, getMeasuredHeight(), getMeasuredWidth(), getMeasuredHeight(), mPaint);
}
}
}</code></pre>
強烈建議:很多情況的過度繪制都是我們設置大片的背景顏色,所以在給布局設置背景顏色上,我們一定要慎重。
2.大面積不可見區域盡量不要繪制。

不可見區域不要繪制!
如上圖,是我們app中碰到的一個優化場景(我見過很多App都有這種界面需求,淘寶、QQ、攜程等),XListView設置了一張背景圖片。在下拉的時候,圖片會慢慢出現。這種方式會出現兩個問題:
第一、過度繪制。
第二、圖片過大,導致內存消耗過大。
所以我們要把背景圖片不可見的部分不進行繪制。
實現步驟其實很簡單。
在onDraw方法,畫布canvas的clipRect(RectF rect)方法可以只繪制規定矩形內的區域。當下拉刷新的時候,下拉刷新的頭高度是在變化的。所以我們只需要計算出任意時刻下拉刷新頭的矩形大小就可以動態設置背景。
寫在最后
以上介紹,是我們優化UI的時候碰到的一些常見問題。當App達到一定用戶規模的時候,UI優化是一個不可避免的任務。當然隨著我們能力提升,在寫布局的時候都會盡量把布局寫的簡單。另外強大的自定義View能力會對我們UI優化大有裨益。
來自:http://www.jianshu.com/p/2e0a2787e0c0