Android性能優化典范-底層工作原理

60fps的由來:

作為程序員,我們經常會聽到60fps和16ms這兩個重要值,同時我們會將程序是否達到60fps來作為App性能的衡量標準,這是因為人眼與大腦之間的協作無法感知超過60fps的畫面更新。

單純的列出數據,可能無法幫助大家進行理解,這里我們舉幾個實際生活中常用的例子:

a.   12fps類似于手動快速翻書的頻率

b.   24fps則可以滿足人眼感知的連續線性的運動,這歸功于運用模糊的效果,電影膠圈就是以這個幀率作為破防幀率的。

c.  但是30fps但是低于30fps是無法順暢表現絢麗的畫面內容的,此時就需要用到60fps來達到想要的效果。我們在開發app的時候需要保持的性能目標就為60fps,換算為時間值也就是:1000ms/60大概為16ms,也就意味著我們需要在16ms內處理完所有的任務。

卡頓的由來:

大多數用戶感知到的卡頓等性能問題的最主要根源都是因為渲染性能。從設計師的角度,他們希望App能夠有更多的動畫,圖片等時尚元素來實現流暢的用戶體驗。但是Android系統很有可能無法及時在16ms內完成那些復雜的界面渲染操作。舉個例子來說,理想情況下,Android系統每隔16ms發出VSYNC信號,觸發對UI進行渲染,如果每次渲染都成功,這樣就能夠達到流暢的畫面所需要的60fps:

理想情況下的渲染

而現實情況下,總會因為各種原因造成16ms內無法運算出繪制時所需要的運算結果,這樣就會造成以下的情況:

假如一個操作花費時間為24ms,則此時在16ms(即系統在接收到VSYNC信號指令)時,無法進行渲染,則這種情況下,只能在接收到下一個VSYNC信號時才進行渲染。這樣,在中間的16ms區間內只能顯示之前的一幀,這樣就意味著在32ms內看到的是同一個畫面。這種現象就是我們熟知的卡頓。

卡頓情況下的渲染

VSYNC的由來:

在上面的知識基礎上,我們知道了卡頓的產生原因,針對上面提到的渲染以及VSYNC信號,我們來詳細說明一下其中的運作原理:

在講解渲染的原理之前,我們需要先了解兩個基本概念:

1.  Refresh Rate:

概念:       代表了屏幕在一秒內刷新屏幕的次數

決定因素:這取決于硬件的固定參數,

舉例:       60Hz

2.   Frame Rate:

概念:       代表了GPU在一秒內繪制操作的幀數

決定因素:GPU

舉例:       60fps

GPU負責計算和獲取數據進行渲染,硬件方面負責將畫面展示在頻幕上面,兩者完美的配合就可以達到最佳的情況,然而不幸的事,兩者在實際情況下并不能總是保持步調一致,如果發生的幀率(fps)與刷新率(Hz)不一致的情況下。就會出現Tearing(圖片撕裂)活著卡頓的情況。

A.   當GPU計算速度(fps)快于硬件刷新率(Hz)時:

Tearing(圖片撕裂)的由來:

首先我們先介紹一下上面提到的Tearing,我們都知道,在繪制每一幀的時候,顯示屏(硬件)會從圖形芯片(內存)中取出GPU計算好的數據,并繪制該幀上的像素點,繪制方式為從上到下,一行一行的繪制。

理想情況下:期望顯示屏在繪制完一幀之后,圖形芯片整好能提供新幀的數據。

特殊情況下:當fps高于hz時,也就是GPU計算速度快于顯示屏的繪制速度,這樣就會導致在突破沒有繪制完成時,就載入了新一幀的數據,由于存儲幀數據的內存空間為同一塊內存,新的數據就會將老的數據進行覆蓋,這就導致最終繪制出來的幀是半個幀的新數據和半個幀的老數據。

發生Tearing現象的圖片

為了避免這種情況的發生,Android中引入了雙緩沖機制和VSYNC(垂直同步)機制進行優化:

1.  VSYNC(垂直同步)

垂直同步是為了保證當前幀可以完整顯示而提出的機制,它會告知GPU在載入新幀之前,要等待屏幕繪制完成前一幀。這樣就可以避免數據的覆蓋,但是從機制中可以發現明顯的漏洞,在等待硬件繪制完成的時間內,GPU是并不進行數據計算的,作為在資本主義社會中誕生的Android系統,這種現象是無法容忍的,為了保證“榨取每一分勞動力”的無上原則,引入了雙緩沖機制。

在加入了垂直同步機制以后,系統由“固定16ms進行繪制”變成了“根據VSYNC信號”來進行繪制,也就是說,我們的優化目標變成了:保證VSYNC信號在16ms內發出。為了保證這個目標,Android系統默認的將幀率設定在60fps,也就是說Android系統每隔16ms就會發出VSYNC信號,來通知硬件進行刷新。

2.   雙緩沖機制

正如其字面的理解,雙緩沖就是采用兩塊內存區域作為存儲。Android中所采用的雙緩沖機制,意味著它可以在顯示一幀的同時進行另一幀的處理。當顯示緩沖A時,系統在緩沖B中構建新的幀。完成后,則交換緩沖。顯示緩沖B,而A則被清空,繼續下一幀的繪制(這里很像我們通常寫代碼時候用到的懶加載機制),保證了GPU的勞動力充分榨取。

但是這種雙緩沖機制同樣會有問題,當遇到上面我們提到的卡頓現象時,也就是某幀的繪制時間超過16毫秒時,雙緩沖的問題就暴露出來了。如圖所示,當緩沖B超時后,一個卡頓發生了。卡頓總是不好的,但更嚴重的問題在于圖中代表CPU和GPU的圖片之間的空白,這是對時間的浪費。在第一幀中,緩沖B超時,則在它被顯示之前,都處于使用中的狀態,而緩沖A因為為了填補空白,仍在顯示,則也處于使用中的狀態。我們知道,僅在收到垂直同步脈沖時,才會進行幀(緩沖)切換。則CPU和GPU受限于可以使用的緩沖數,不得不消極怠工(好像我們可以說:不是我不干活哦,是程序在編譯哦)。

當然,針對這種現象,前輩們也提出了相應的解決方案,例如:三倍緩沖(也就是在A,B兩塊緩沖區域都處于使用狀態時,創建C快緩沖區,這樣就可以保證GPU在持續工作過程中)。

B.  當GPU計算速度(fps)慢于硬件刷新率(Hz)時:

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

fps小于Hz的情況

在這種情況下,某些幀顯示的畫面內容就會與上一幀的畫面相同,也就是我們常說的“丟幀”。又或者另外一種情況,用戶在使用過程中幀率從超過60fps突然掉到60fps以下,這樣就會發生LAG,JANK,HITCHING等卡頓掉幀的不順滑的情況。這也是用戶感受不好的原因所在。

導致卡頓的原因:

在了解繪制以及卡頓的原因以后,我們就可以結合一些性能優化工具來對我們的APP進行優化,從而使我們的APP具備潤滑流暢的快感:

A.   性能監測工具

在優化性能之前,首要的是需要在App中找到需要優化的點,進行逐一優化,各個擊破,這時就需要用到一些的性能監測工具,從哪里找這些工具呢?谷歌為我們這群懶人們提供了一個高效且易用的可視化工具:Profile GPU Render。

位置:

開發者選項--->Profile GPU Render--->On Screen as Bars

界面介紹:

這樣就可以在屏幕下面看到如心電圖一樣的動態性能監視圖,當在使用APP的過程中,下部的監視圖會從左至右進行繪制。整個屏幕可以分為三個部分,頭頂部的心電圖代表的是,通知欄的繪制性能情況(一般不咋用),下部是我們著重需要重點關注的,也就是當前激活的Application(我們需要優化的app)的性能心電圖。

性能監視圖

在下部的監視圖中,除了那根明顯的綠色線條用于標注16ms以外(每個程序員在優化的過程中需要保證的就是將在整個APP運行過程中,整個心電圖中的每一根線都在這條綠線以下),其下部的心電圖根據顏色分為三個部分:

A.1   藍色代表隊:在該幀的計算過程中CPU進行轉換及緩存(創建和展示displaylist中的內容)所消耗掉時間。而繪制時間則是由轉換成GPU可以識別的格式所消耗掉時間(由drawable圖片通過命令或者通過canvas繪制兩種方式)以及將格式緩沖進Displaylist所消耗掉時間所構成。

如果這個部分的消耗很大的話,說明當前幀中可能有很多的用到圖片資源的控件需要繪制,活著說當前頁面中有一個自定義控件非常復雜,需要消耗很多的邏輯計算時間。

格式轉換及緩存兩部分

A.2   紅色代表隊:在該幀的計算過程中將結果存儲進DisplayList的集合中,然后利用該集合匯總的信息,轉換成圖像,也就是繪制圖像執行所消耗掉時間。通常Android中的會通過Open GL ES API來將集合中的信息傳入到GPU當中,隨后通過GPU將信息轉化成像素點,繪制在屏幕上。

也就是說,如果我們需要繪制的信息越多的情況下,比如說華麗復雜的自定義時,就會消耗很多時間,同時還有重要的一點就是:如果圖片中出現重復繪制的情況,也會相應增加GPU的無謂消耗時間。這部分的內容也會增加紅色代表隊的時間消耗。

A.3   橙色代表隊:在該幀的計算過程中所CPU在完成邏輯計算后,但需要等待GPU繪制完成,才能進行下一步工作,所消耗掉的時間。

這里如果出現增長的話,就是意味著你的GPU上承載著高負荷的工作,也就是說你當前有非常多的Open GL ES API命令需要執行。

補充:

在上面提到的諸如Open GL ES API,CPU進行轉換及緩存等一些名詞是不是很懵逼?(別裝,肯定的),在這里我繼續補充一下從谷歌提供的視頻中針對這些步驟的詳細解釋:

1.   XML布局中的文弱少女是如何轉換成的頻幕上的妖艷賤貨的:

首先需要通過resterization(柵格化)將圖片,圖形或者文字一類轉化成頻幕可以展示的像素的過程。 當然,這個過程絕逼是非常消耗時間的, 這里的GPU就是將這個漫長的過程變得飛快。

由于GPU對柵格化的基本格式有特定的要求,主要格式為:polygons,textures和images。CPU在這里負責將圖片,文字首先轉化成成 polygons,textures和images格式,然后傳遞給GPU進行柵格化處理。

例如一個button需要先在CPU中進行格式轉換(polygons等),然后傳遞給GPU進行柵格化

2.   看似平整的地面,總有暗坑等你踩

但是也許你可以發現這里有一個坑比的地方,button轉換成特定的polygons是一個時間消耗過程,再由polygons傳遞給GPU又是一個時間消耗過程,而由CPU傳遞給GPU同樣是一個非常耗時的過程。這樣就意味著,GPU中進行柵格化所節省下來的時間,可能在這里被消耗大半。

幸運的是Open GL考慮到了這一點,它提供了一個類似緩存到機制:CPU上傳到GPU中的資源,可以作為緩沖保存在GPU當中,在下次再次利用的過程中,就省去了CPU的格式轉換和CPU上傳到GPU的過程消耗。Android系統就靈活利用到了這一點,它在系統啟動過程中,就將主題中的系統資源以一個單一polygons的形式上傳至GPU,以后在調用系統資源時,就可以直接在GPU中取到相應資源,而不需要轉換和傳遞。這就是加載Android系統圖片為啥這么快的原因。

然而,有了這個機制就可以萬事大吉了?并不,隨著UI畫的圖越來越詭異,產品設計的動畫越來越彪悍,GPU的緩沖機制變得幾乎形同虛設,因為每一個圖片都是不同的,都無法服用,因此GPU中的緩存資源只能通過不斷被覆蓋來達到相應效果,謝天謝地,Android系統提供了差異化繪制機制,簡單來說就是緩存的舊資源與即將寫入的新資源進行對比,只對發生了改變的部分進行重新處理。以此緩解GPU的壓力。

3.   媽媽說,破掉的東東要扔掉

在上面,我們有提到,有DisplayList對CPU處理好的格式資源以及需要進行的相應的繪制指令,進行接收。這里的DispalyList在特殊情況下,可以對其接收的信息進行復用,舉例來說:

如果一個button改變了其位置:GPU可以將DisplayList中的信息可以進行復用。

如果一個button改變了其大小或者其形狀,表面顏色發生改變(視覺上的形體,色彩改變 ):GPU就無法使用之前CPU傳遞來的DisplayList,需要通過CPU進行重新格式轉換,然后將命令和轉換好的資源存入一個新的DisplayList當中。

4.   Android系統都是強迫癥!

我們需要注意的是,我們的每一個小小的改變,都會引起整個view的大量工作,舉例來說:

4.a  改變界面中一個view或viewgroup中的子view的尺寸大小,原先的measure流程測過的所有尺寸就無效了,系統會遍歷整個view hierarchy,對每個view進行重新measure。

4.b  改變界面中一個view或者一個viewgroup中的子view的位置,原先界面中的所有view位置信息就全部無效了,系統會通過requestLayout來重新確定每個view的位置。

這些個步驟都是非常耗時的,當界面中有龐大且復雜的view出現時,就會嚴重影響程序的性能。這里我們可以通過將整個view hierarchy扁平化處理,這樣可以有效的降低改動部分view,所造成的整個view hierarchy重新測量或重新定位所消耗掉時間。

Hierarchy Viewer視圖

B.  Overdraw

在上面的介紹中,我們得知,在CPU通過Open GL將轉換后的資源交給GPU進行繪制時,如果出現大量重復繪制的情況,會加重GPU的負擔,那么降低重復繪制的內容,就是我們需要關注的其中一個重點。那么,問題來了,重復繪制查找哪家強?Android系統中給我們同樣提供了一個用于監測重復繪制的工具---Overdraw。

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

當設計上追求更華麗的視覺效果的時候,我們就容易陷入采用越來越多的層疊組件來實現這種視覺效果的怪圈。這很容易導致大量的性能問題,為了獲得最佳的性能,我們必須盡量減少Overdraw的情況發生。

幸運的是,谷歌為我們考慮到了這一點,在系統中的手機設置里面的開發者選項,步驟為:"系統設置"-->"開發者選項"-->"調試GPU過度繪制"打開Show GPU Overdraw的選項,可以觀察UI上的Overdraw情況。在勾選該選項后,頻幕中會出現四種色調:

Overdraw四種繪制情況

其中,四種顏色:藍色,綠色,粉色,紅色分別代表:1次繪制,2次繪制,3次繪制,四次繪制。還有一種是原色,代表的是無重復繪制。

多次繪制的原因有很多,其中有可能是因為你UI布局存在大量重疊的部分,還有的時候是因為非必須的重疊背景活著繪制了不可見的UI元素。這里需要提醒的是,通常情況下,我們創建的Activity在默認情況下,theme會給window設置一個純色的背景. 因為我們這里不想使用這個默認的背景,故而給layout加了一層背景, 導致了多重繪制背景.可以通過:

getWindow().setBackgroundDrawable(null);

將這一層默認添加的背景消除。

到此為止,是不是發現,隨著科技和優化機制的革新,更漂亮,更華麗的效果也得以實現,兩者相互推進,使得我們的應用越來越完美,這就是我們為何需要優化我們APP的根本原因。本篇文章只是向程序優化的第一步,隨后,我會繼續學習視頻和相關文檔,推出新的文章,與大家一起分享和討論。

 

來自:http://www.jianshu.com/p/5a2fb652cc1e

 

 本文由用戶 s345129564 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!