Android渲染機制和丟幀分析

http://blog.csdn.net/bd_zengxinxin/article/details/52525781java

本身編寫App的時候,有時會感受界面卡頓,尤爲是自定義View的時候,大多數是由於佈局的層次過多,存在沒必要要的繪製, 或者onDraw等方法中過於耗時。那麼究竟須要多快,才能給用戶一個流暢的體驗呢?那麼就須要簡單瞭解下Android的渲染機制:android

Android系統每隔16ms發出VSYNC信號,觸發對UI進行渲染,那麼整個過程若是保證在16ms之內就能達到一個流暢的畫面。 那麼若是操做超過了16ms就會發生下面的狀況:canvas

若是系統發生的VSYNC信號,而此時沒法進行渲染,還在作別的操做,那麼就會致使丟幀的現象, (你們在察覺到APP卡頓的時候,能夠看看logcat控制檯,會有drop frames相似的警告)。這樣的話,繪製就會在下一個16ms的時候才進行繪製, 即便只丟一幀,用戶也會發現卡頓的。ide

那爲何是16ms,16ms意味着着1000/60hz,至關於60fps,那麼只要解釋爲何是60fps。工具

這是由於人眼與大腦之間的協做沒法感知超過60fps的畫面更新。12fps大概相似手動快速翻動書籍的幀率, 這明顯是能夠感知到不夠順滑的。24fps使得人眼感知的是連續線性的運動,這實際上是歸功於運動模糊的 效果。 24fps是電影膠圈一般使用的幀率,由於這個幀率已經足夠支撐大部分電影畫面須要表達的內容,同時可以最大的減小費用支出。 可是低於30fps是 沒法順暢表現絢麗的畫面內容的,此時就須要用到60fps來達到想要的效果,固然超過60fps是沒有必要的。佈局

有了對Android渲染機制基本的認識之後,那麼咱們的卡頓的緣由就在於沒有辦法在16ms內完成該完成的操做, 而主要因素是在於沒有必要的layouts、invalidations、Overdraw。渲染的過程是由CPU與GPU協做完成, 下面一張圖很好的展現出了CPU和GPU的工做,以及潛在的問題,檢測的工具和解決方案。性能

咱們須要知道: 1.經過Hierarchy Viewer去檢測渲染效率,去除沒必要要的嵌套 2.經過Show GPU Overdraw去檢測Overdraw,最終能夠經過移除沒必要要的背景以及使用canvas.clipRect解決大多數問題。優化

Overdraw

Overdraw(過分繪製)描述的是屏幕上的某個像素在同一幀的時間內被繪製了屢次。在多層次的UI結構裏面, 若是不可見的UI也在作繪製的操做,這就會致使某些像素區域被繪製了屢次。這就浪費大量的CPU以及GPU資源。this

當設計上追求更華麗的視覺效果的時候,咱們就容易陷入採用愈來愈多的層疊組件來實現這種視覺效果的怪圈。這很容易致使大量的性能問題, 爲了得到最佳的性能,咱們必須儘可能減小Overdraw的狀況發生。spa

咱們能夠經過手機設置裏面的開發者選項,打開Show GPU Overdraw的選項,能夠觀察UI上的Overdraw狀況。

藍色,淡綠,淡紅,深紅表明了4種不一樣程度的Overdraw狀況,咱們的目標就是儘可能減小紅色Overdraw,看到更多的藍色區域。

Overdraw有時候是由於你的UI佈局存在大量重疊的部分,還有的時候是由於非必須的重疊背景。例如某個Activity有一個背景, 而後裏面 的Layout又有本身的背景,同時子View又分別有本身的背景。僅僅是經過移除非必須的背景圖片,這就可以減小大量的紅色Overdraw區域, 增長 藍色區域的佔比。這一措施可以顯著提高程序性能。

Overdraw 的處理方案一:移除沒必要要的background

下面看一個簡單的展現ListView的例子: activity_main

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:background="@android:color/white" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" android:orientation="vertical" > <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="@dimen/narrow_space" android:textSize="@dimen/large_text_size" android:layout_marginBottom="@dimen/wide_space" android:text="@string/header_text"/> <ListView android:id="@+id/id_listview_chats" android:layout_width="match_parent" android:background="@android:color/white" android:layout_height="wrap_content" android:divider="@android:color/transparent" android:dividerHeight="@dimen/divider_height"/> </LinearLayout> 

item的佈局文件

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" android:paddingBottom="@dimen/chat_padding_bottom"> <ImageView android:id="@+id/id_chat_icon" android:layout_width="@dimen/avatar_dimen" android:layout_height="@dimen/avatar_dimen" android:src="@drawable/joanna" android:layout_margin="@dimen/avatar_layout_margin" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/darker_gray" android:orientation="vertical"> <RelativeLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@android:color/white" android:textColor="#78A" android:orientation="horizontal"> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:padding="@dimen/narrow_space" android:text="@string/hello_world" android:gravity="bottom" android:id="@+id/id_chat_name" /> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:textStyle="italic" android:text="@string/hello_world" android:padding="@dimen/narrow_space" android:id="@+id/id_chat_date" /> </RelativeLayout> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="@dimen/narrow_space" android:background="@android:color/white" android:text="@string/hello_world" android:id="@+id/id_chat_msg" /> </LinearLayout> </LinearLayout> 

Activity的代碼

public class OverDrawActivity01 extends AppCompatActivity { private ListView mListView; private LayoutInflater mInflater ; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_overdraw_01); mInflater = LayoutInflater.from(this); mListView = (ListView) findViewById(R.id.id_listview_chats); mListView.setAdapter(new ArrayAdapter<Droid>(this, -1, Droid.generateDatas()) { @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder = null ; if(convertView == null) { convertView = mInflater.inflate(R.layout.chat_item,parent,false); holder = new ViewHolder(); holder.icon = (ImageView) convertView.findViewById(R.id.id_chat_icon); holder.name = (TextView) convertView.findViewById(R.id.id_chat_name); holder.date = (TextView) convertView.findViewById(R.id.id_chat_date); holder.msg = (TextView) convertView.findViewById(R.id.id_chat_msg); convertView.setTag(holder); }else { holder = (ViewHolder) convertView.getTag(); } Droid droid = getItem(position); holder.icon.setBackgroundColor(0x44ff0000); holder.icon.setImageResource(droid.imageId); holder.date.setText(droid.date); holder.msg.setText(droid.msg); holder.name.setText(droid.name); return convertView; } class ViewHolder { ImageView icon; TextView name; TextView date; TextView msg; } }); } } 

如今的效果是:

注意,咱們的需求是總體是Activity是個白色的背景。

開啓Show GPU Overdraw之後:

對比上面的參照圖,能夠發現一個簡單的ListView展現Item,居然不少地方被過分繪製了4X 。 那麼,其實主要緣由是因爲該佈局文件中存在不少沒必要要的背景,仔細看上述的佈局文件,那麼開始移除吧。

  • 沒必要要的Background 1

咱們主佈局的文件已是background爲white了,那麼能夠移除ListView的白色背景

  • 沒必要要的Background 2

Item佈局中的LinearLayout的android:background=」@android:color/darker_gray」

  • 沒必要要的Background 3

Item佈局中的RelativeLayout的android:background=」@android:color/white」

  • 沒必要要的Background 4

Item佈局中id爲id_msg的TextView的android:background=」@android:color/white」

這四個沒必要要的背景也比較好找,那麼移除後的效果是:

對比以前的是否是好多了,接下來還存在一些沒必要要的背景.

  • 沒必要要的Background 5

這個背景比較難發現,主要須要看Adapter的getView的代碼,上述代碼你會發現,首先爲每一個icon設置了背景色 (主要是當沒有icon圖的時候去顯示),而後又設置了一個頭像。那麼就形成了overdraw,有頭像的徹底不必去繪製背景,全部修改代碼:

Droid droid = getItem(position); if(droid.imageId ==-1) { holder.icon.setBackgroundColor(0x4400ff00); holder.icon.setImageResource(android.R.color.transparent); }else { holder.icon.setImageResource(droid.imageId); holder.icon.setBackgroundResource(android.R.color.transparent); } 

ok,還有最後一個,這個也是很是容易被忽略的。

  • 沒必要要的Background 6

記得咱們以前說,咱們的這個Activity要求背景色是白色,咱們的確在layout中去設置了背景色白色,那麼這裏注意下, 咱們的Activity的佈局最終會添加在DecorView中,這個View會中的背景是否是就沒有必要了, 因此咱們但願調用mDecor.setWindowBackground(drawable);,那麼能夠在Activity調用getWindow().setBackgroundDrawable(null);

setContentView(R.layout.activity_overdraw_01); getWindow().setBackgroundDrawable(null); 

ok,一個簡單的listview顯示item,咱們一共找出了6個沒必要要的背景,如今再看最後的Show GPU Overdraw 與最初的比較。

對比參照圖,基本已經達到了最優的狀態。

Overdraw 的處理方案二:clipRect的妙用

咱們在自定義View的時候,常常會因爲疏忽形成不少沒必要要的繪製,好比你們看下面這樣的圖:

多張卡片疊加,那麼若是你是一張一張卡片從左到右的繪製,效果確定沒問題,可是疊加的區域確定是過分繪製了。 而且material design對於界面設計的新的風格更容易形成上述的問題。那麼有什麼好的方法去改善呢? 答案是有的,android的Canvas對象給咱們提供了很便利的方法clipRect就能夠很好的去解決這類問題。

下面經過一個實例來展現,那麼首先看一個效果圖:

左邊顯示的時效果圖,右邊顯示的是開啓Show Override GPU以後的效果,能夠看到,卡片疊加處明顯的過分渲染了。

上面已經說了使用cliprect方法,那麼咱們目標直指自定義View的onDraw方法。修改後的代碼:

@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.save(); canvas.translate(20, 120); for (int i = 0; i < mCards.length; i++) { canvas.translate(120, 0); canvas.save(); if (i < mCards.length - 1) { canvas.clipRect(0, 0, 120, mCards[i].getHeight()); } canvas.drawBitmap(mCards[i], 0, 0, null); canvas.restore(); } canvas.restore(); } 

分析得出,除了最後一張須要完整的繪製,其餘的都只須要繪製部分;因此咱們在循環的時候,給i到n-1都添加了clipRect的代碼。

最後的效果圖:

能夠看到,全部卡片變爲了淡紫色,對比參照圖,都是1X過分繪製.

若是你按照上面的修改,會發現最終效果圖不是淡紫色,而是青色(2X),那是爲何呢?由於你還忽略了一個優化的地方, 本View已經有了不透明的背景,徹底能夠移除Window的背景了,即在Activity中,添加getWindow().setBackgroundDrawable(null);代碼。

VSYNC(Vertical Synchronization 垂直同步)

爲了理解App是如何進行渲染的,咱們必須瞭解手機硬件是如何工做,那麼就必須理解什麼是VSYNC。

在講解VSYNC以前,咱們須要瞭解兩個相關的概念:

1.Refresh Rate:表明了屏幕在一秒內刷新屏幕的次數,這取決於硬件的固定參數,例如60Hz。 2.Frame Rate:表明了GPU在一秒內繪製操做的幀數,例如30fps,60fps。

GPU會獲取圖形數據進行渲染,而後硬件負責把渲染後的內容呈現到屏幕上,他們二者不停的進行協做。

不幸的是,刷新頻率和幀率並非總可以保持相同的節奏。若是發生幀率與刷新頻率不一致的狀況, 就會容易出現Tearing的現象(畫面上下兩部分顯示內容發生斷裂,來自不一樣的兩幀數據發生重疊)。

一般來講,幀率超過刷新頻率只是一種理想的情況,在超過60fps的狀況下,GPU所產生的幀數據會由於等待VSYNC的刷新信息而被Hold住, 這樣可以保持每次刷新都有實際的新的數據能夠顯示。可是咱們遇到更多的狀況是幀率小於刷新頻率。

在這種狀況下,某些幀顯示的畫面內容就會與上一幀的畫面相同。糟糕的事情是,幀率從超過60fps忽然掉到60fps如下, 這樣就會發生LAG,JANK,HITCHING等卡頓掉幀的不順滑的狀況。這也是用戶感覺很差的緣由所在。

相關文章
相關標籤/搜索