過度繪制
繪制原理
Android系統(tǒng)要求每一幀都要在 16ms 內(nèi)繪制完成厢拭,平滑的完成一幀意味著任何特殊的幀需要執(zhí)行所有的渲染代碼(包括 framework 發(fā)送給 GPU 和CPU 繪制到緩沖區(qū)的命令)都要在 16ms 內(nèi)完成吹菱,保持流暢的體驗羔沙。這個速度允許系統(tǒng)在動畫和輸入事件的過程中以約 60 幀每秒( 1秒 / 0.016幀每秒 = 62.5幀/秒 )的平滑幀率來渲染谍倦。
如果應(yīng)用沒有在 16ms 內(nèi)完成這一幀的繪制仔掸,假設(shè)你花了 24ms 來繪制這一幀春叫,那么就會出現(xiàn)掉幀的情況残家。
系統(tǒng)準備將新的一幀繪制到屏幕上,但是這一幀并沒有準備好骑歹,所有就不會有繪制操作预烙,畫面也就不會刷新。反饋到用戶身上道媚,就是用戶盯著同一張
圖看了 32ms 而不是 16ms 扁掸,也就是說掉幀發(fā)生了。
掉幀
掉幀是用戶體驗中一個非常核心的問題最域。丟棄了當前幀谴分,并且之后不能夠延續(xù)之前的幀率,這種不連續(xù)的間隔會容易會引起用戶的注意镀脂,也就是我們
常說的卡頓牺蹄、不流暢。
掉幀的原因很多薄翅,比如:
-
ViewTree非常龐大沙兰,花了很多時間重新繪制界面中的控件,這樣非常浪費CPU周期:
-
過度繪制嚴重翘魄,在繪制用戶看不到的對象上花費了太多的時間:
- 大量動畫多次重復鼎天,消耗CPU和GPU
- 頻繁地觸發(fā)GC機制
目前我們的項目的 APP 卡頓現(xiàn)象主要是由于 ViewTree 過于龐大和過度繪制嚴重造成
UI繪制機制
在現(xiàn)在的設(shè)備上,UI繪制主要由CPU和GPU協(xié)作完成熟丸,其工作原理如下圖:
其實這圖我也看得不太懂训措,但知道那么兩個解決問題的方法:
- 利用 Android Studio 自帶的 Hierarchy Viewer 去檢測各 View 層的繪制時間,刪除或合并圖層
- 打開手機的 ShowGPUOverdraw去檢測Overdraw,移除不必要的background
Hierarchy Viewer 的使用
Hierarchy Viewer
Hierarchy Viewer工具在Android device monitor中
在Mac的Android Studio中:
圖片來源http://blog.csdn.net/lmj623565791/article/details/45556391/
在windows的Android Studio中:
那么如何使用呢绩鸣?
圖片來源http://blog.csdn.net/lmj623565791/article/details/45556391/
簡單使用
打開ViewTree視圖后怀大,點擊任意一個view,然后點擊Profile Node即可展示每個view在各個階段的耗時情況呀闻,如:
圖中可以看到化借,該view在Measure、Layout和Draw階段都比其它view耗時要多(下面的點變成紅色了)捡多,圖中看出該view節(jié)點后有72個子view蓖康,還可以讀出數(shù)據(jù):
階段 | 耗時 |
---|---|
Measure | 0.028ms |
Layout | 0.434ms |
Draw | 9.312ms |
在ViewTree中查找可刪減或合并的view,找到耗時嚴重的view加以改良垒手,可以減輕過度繪制現(xiàn)象蒜焊,例如:
例如圖中的兩個LinearLayout只要保留一個就夠了,而前面這個RecyclerView的item只有一個科贬,沒必要使用RecyclerView泳梆,可以考慮用其它view來替代
用Show GPU Overdraw方法來檢測
過度繪制的檢測
按照以下步驟打開ShowGPUOverrdraw的選項:
設(shè)置 -> 開發(fā)者選項 -> 調(diào)試GPU過度繪制 -> 顯示GPU過度繪制
打開后,屏幕會有多種顏色榜掌,切換到需要檢測的應(yīng)用程序优妙,對于各個色塊,有一張參考圖:
其中藍色部分表示1層過度繪制憎账,紅色表示4層過度繪制套硼。
解決方案
移除不必要的background
下面舉個簡單的例子:
- activity_main 布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical"
tools:context="com.example.erkang.overdraw.MainActivity">
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
card_view:cardBackgroundColor="@color/white">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white">
<TextView
android:id="@+id/title_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingBottom="5dp"
android:paddingTop="5dp"
android:text="OverDraw展示樣式" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_below="@+id/title_tv"
android:layout_marginBottom="5dp"
android:scaleType="fitCenter"
android:src="@drawable/infernal_affairs_0" />
</RelativeLayout>
</android.support.v7.widget.CardView>
<View
android:layout_width="match_parent"
android:layout_height="20dp" />
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/gray" />
</LinearLayout>
- RecyclerView的item的布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<ImageView
android:layout_marginLeft="10dp"
android:id="@+id/item_iv"
android:layout_width="100dp"
android:layout_height="100dp"
tools:src="@drawable/infernal_affairs_1" />
<TextView
android:layout_marginLeft="10dp"
android:id="@+id/item_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="對唔住,我喺差人胞皱。" />
</LinearLayout>
- Activity代碼:
public class MainActivity extends AppCompatActivity {
private MyAdapter myAdapter;
private RecyclerView recyclerView;
private static final int ITEM_COUNT = 20;
private static final int ITEM_DISTANCE = 40;
private LinearLayoutManager layoutManager;
private MyItemDecoration myItemDecoration;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportActionBar().hide();
setContentView(R.layout.activity_main);
init();
}
private void init() {
recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
myAdapter = new MyAdapter(MainActivity.this, ITEM_COUNT);
layoutManager = new LinearLayoutManager(MainActivity.this, LinearLayoutManager.VERTICAL, false);
myItemDecoration = new MyItemDecoration(ITEM_DISTANCE);
recyclerView.addItemDecoration(myItemDecoration);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(myAdapter);
}
}
- ItemDecoration代碼:
public class MyItemDecoration extends RecyclerView.ItemDecoration{
protected int halfSpace;
/**
* @param space item之間的間隙
*/
public MyItemDecoration(int space){
setSpace(space);
}
public void setSpace(int space) {
this.halfSpace = space / 2;
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
outRect.top = halfSpace;
outRect.bottom = halfSpace;
outRect.left = halfSpace;
outRect.right = halfSpace;
}
}
現(xiàn)在看起來的效果是這樣的:
圖中邪意,我們需要上方展示的部分背景為白色,而下方列表Item之間的顏色為灰色朴恳,item的背景為白色抄罕。
打開顯示過度繪制功能后是這樣的:
圖中可以看到很多區(qū)域出現(xiàn)了三重或四重的過度繪制現(xiàn)象。那么我們開始去掉不必要的background于颖。
- 不必要的background 1:
總布局LinearLayout中的 android:background="@color/white" 可以去掉;
- 不必要的background 2:
上方布局RelativeLayout中的android:background="@color/white"可以去掉;
去掉這兩個background后嚷兔,我們重新安裝一下應(yīng)用程序森渐,發(fā)現(xiàn)界面上方過度繪制現(xiàn)象明顯改善:
![](http://i.imgur.com/jWAaJ8w.png)
但界面下方仍存在過度繪制現(xiàn)象,若把RecyclerView中的background值的灰色去掉冒晰,則下方列表Item之間的就會變成白色同衣,顯然不是我們想要的效
果:
于是,我們需要對RecyclerView的ItemDecoration類進行改造壶运,改造成如下:
public class MyItemDecoration extends RecyclerView.ItemDecoration{
protected int halfSpace;
private Paint paint;
/**
* @param space item之間的間隙
*/
public MyItemDecoration(int space, Context context) {
setSpace(space);
paint = new Paint();
paint.setAntiAlias(true);//抗鋸齒
paint.setColor(context.getResources().getColor(R.color.gray));//設(shè)置背景色
}
public void setSpace(int space) {
this.halfSpace = space / 2;
}
/**
*
* 重寫onDraw 方法以實現(xiàn)recyclerview的item之間的間隙的背景
* @param c 畫布
* @param parent 使用該 ItemDecoration 的 RecyclerView 對象實例
* @param state 使用該 ItemDecoration 的 RecyclerView 對象實例的狀態(tài)
*/
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
int outLeft, outTop, outRight, outBottom,viewLeft,viewTop,viewRight,viewBottom;
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View view = parent.getChildAt(i);
viewLeft = view.getLeft();
viewTop = view.getTop();
viewRight = view.getRight();
viewBottom = view.getBottom();
// item外層的rect在RecyclerView中的坐標
outLeft = viewLeft - halfSpace;
outTop = viewTop - halfSpace;
outRight = viewRight + halfSpace;
outBottom = viewBottom + halfSpace;
//item 上方的矩形
c.drawRect(outLeft, outTop, outRight,viewTop, paint);
//item 左邊的矩形
c.drawRect(outLeft,viewTop,viewLeft,viewBottom,paint);
//item 右邊的矩形
c.drawRect(viewRight,viewTop,outRight,viewBottom,paint);
//item 下方的矩形
c.drawRect(outLeft,viewBottom,outRight,outBottom,paint);
}
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
outRect.top = halfSpace;
outRect.bottom = halfSpace;
outRect.left = halfSpace;
outRect.right = halfSpace;
}
}
其實就是增加了onDraw方法耐齐,在item的間隙畫上了帶背景色的矩形,于是我們想要的效果又回來了:
這時打開“顯示過度繪制”功能:
已經(jīng)是可以接受的效果了。
總結(jié)
解決過度繪制現(xiàn)象埠况,可以從這兩個方法入手:
- 利用 Hierarchy Viewer 觀察整個界面的ViewTree耸携,刪掉無用的圖層,找到能合并的view合并辕翰,找到紅點圖層分析原因夺衍;
- 查看各圖層的background,去掉不必要的background喜命;
- 對于列表中Item之間的間隙顏色沟沙,不要在列表的 background 設(shè)置,應(yīng)該在列表應(yīng)用的 ItemDecoration 中設(shè)置
后記
本文已上傳至GitHub https://github.com/EKwongChum/OverDraw
歡迎指出問題壁榕,謝謝矛紫。