來源:http://hukai.me/android-performance-patterns/#jtss-tsinahtml
大多數用戶感知到的卡頓等性能問題的最主要根源都是由於渲染性能。從設計師的角度,他們但願App可以有更多的動畫,圖片等時尚元素來實現流暢的用戶體驗。可是Android系統頗有可能沒法及時完成那些複雜的界面渲染操做。Android系統每隔16ms發出VSYNC信號,觸發對UI進行渲染,若是每次渲染都成功,這樣就可以達到流暢的畫面所須要的60fps,爲了可以實現60fps,這意味着程序的大多數操做都必須在16ms內完成。android
若是你的某個操做花費時間是24ms,系統在獲得VSYNC信號的時候就沒法進行正常渲染,這樣就發生了丟幀現象。那麼用戶在32ms內看到的會是同一幀畫面。算法
用戶容易在UI執行動畫或者滑動ListView的時候感知到卡頓不流暢,是由於這裏的操做相對複雜,容易發生丟幀的現象,從而感受卡頓。有不少緣由能夠致使丟幀,也許是由於你的layout太過複雜,沒法在16ms內完成渲染,有多是由於你的UI上有層疊太多的繪製單元,還有多是由於動畫執行的次數過多。這些都會致使CPU或者GPU負載太重。canvas
咱們能夠經過一些工具來定位問題,好比可使用HierarchyViewer來查找Activity中的佈局是否過於複雜,也可使用手機設置裏面的開發者選項,打開Show GPU Overdraw等選項進行觀察。你還可使用TraceView來觀察CPU的執行狀況,更加快捷的找到性能瓶頸。緩存
Overdraw(過分繪製)描述的是屏幕上的某個像素在同一幀的時間內被繪製了屢次。在多層次的UI結構裏面,若是不可見的UI也在作繪製的操做,這就會致使某些像素區域被繪製了屢次。這就浪費大量的CPU以及GPU資源。網絡
當設計上追求更華麗的視覺效果的時候,咱們就容易陷入採用愈來愈多的層疊組件來實現這種視覺效果的怪圈。這很容易致使大量的性能問題,爲了得到最佳的性能,咱們必須儘可能減小Overdraw的狀況發生。app
幸運的是,咱們能夠經過手機設置裏面的開發者選項,打開Show GPU Overdraw的選項,能夠觀察UI上的Overdraw狀況。工具
藍色,淡綠,淡紅,深紅表明了4種不一樣程度的Overdraw狀況,咱們的目標就是儘可能減小紅色Overdraw,看到更多的藍色區域。佈局
Overdraw有時候是由於你的UI佈局存在大量重疊的部分,還有的時候是由於非必須的重疊背景。例如某個Activity有一個背景,而後裏面的Layout又有本身的背景,同時子View又分別有本身的背景。僅僅是經過移除非必須的背景圖片,這就可以減小大量的紅色Overdraw區域,增長藍色區域的佔比。這一措施可以顯著提高程序性能。性能
爲了理解App是如何進行渲染的,咱們必須瞭解手機硬件是如何工做,那麼就必須理解什麼是VSYNC。
在講解VSYNC以前,咱們須要瞭解兩個相關的概念:
GPU會獲取圖形數據進行渲染,而後硬件負責把渲染後的內容呈現到屏幕上,他們二者不停的進行協做。
不幸的是,刷新頻率和幀率並非總可以保持相同的節奏。若是發生幀率與刷新頻率不一致的狀況,就會容易出現Tearing的現象(畫面上下兩部分顯示內容發生斷裂,來自不一樣的兩幀數據發生重疊)。
理解圖像渲染裏面的雙重與三重緩存機制,這個概念比較複雜,請移步查看這裏:http://source.android.com/devices/graphics/index.html,還有這裏http://article.yeeyan.org/view/37503/304664。
一般來講,幀率超過刷新頻率只是一種理想的情況,在超過60fps的狀況下,GPU所產生的幀數據會由於等待VSYNC的刷新信息而被Hold住,這樣可以保持每次刷新都有實際的新的數據能夠顯示。可是咱們遇到更多的狀況是幀率小於刷新頻率。
在這種狀況下,某些幀顯示的畫面內容就會與上一幀的畫面相同。糟糕的事情是,幀率從超過60fps忽然掉到60fps如下,這樣就會發生LAG,JANK,HITCHING等卡頓掉幀的不順滑的狀況。這也是用戶感覺很差的緣由所在。
性能問題如此的麻煩,幸虧咱們能夠有工具來進行調試。打開手機裏面的開發者選項,選擇Profile GPU Rendering,選中On screen as bars的選項。
選擇了這樣之後,咱們能夠在手機畫面上看到豐富的GPU繪製圖形信息,分別關於StatusBar,NavBar,激活的程序Activity區域的GPU Rending信息。
隨着界面的刷新,界面上會滾動顯示垂直的柱狀圖來表示每幀畫面所須要渲染的時間,柱狀圖越高表示花費的渲染時間越長。
中間有一根綠色的橫線,表明16ms,咱們須要確保每一幀花費的總時間都低於這條橫線,這樣纔可以避免出現卡頓的問題。
每一條柱狀線都包含三部分,藍色表明測量繪製Display List的時間,紅色表明OpenGL渲染Display List所須要的時間,黃色表明CPU等待GPU處理的時間。
咱們一般都會提到60fps與16ms,但是知道爲什麼會是以程序是否達到60fps來做爲App性能的衡量標準嗎?這是由於人眼與大腦之間的協做沒法感知超過60fps的畫面更新。
12fps大概相似手動快速翻動書籍的幀率,這明顯是能夠感知到不夠順滑的。24fps使得人眼感知的是連續線性的運動,這實際上是歸功於運動模糊的效果。24fps是電影膠圈一般使用的幀率,由於這個幀率已經足夠支撐大部分電影畫面須要表達的內容,同時可以最大的減小費用支出。可是低於30fps是沒法順暢表現絢麗的畫面內容的,此時就須要用到60fps來達到想要的效果,固然超過60fps是沒有必要的。
開發app的性能目標就是保持60fps,這意味着每一幀你只有16ms=1000/60的時間來處理全部的任務。
瞭解Android是如何利用GPU進行畫面渲染有助於咱們更好的理解性能問題。那麼一個最實際的問題是:activity的畫面是如何繪製到屏幕上的?那些複雜的XML佈局文件又是如何可以被識別並繪製出來的?
Resterization柵格化是繪製那些Button,Shape,Path,String,Bitmap等組件最基礎的操做。它把那些組件拆分到不一樣的像素上進行顯示。這是一個很費時的操做,GPU的引入就是爲了加快柵格化的操做。
CPU負責把UI組件計算成Polygons,Texture紋理,而後交給GPU進行柵格化渲染。
然而每次從CPU轉移到GPU是一件很麻煩的事情,所幸的是OpenGL ES能夠把那些須要渲染的紋理Hold在GPU Memory裏面,在下次須要渲染的時候直接進行操做。因此若是你更新了GPU所hold住的紋理內容,那麼以前保存的狀態就丟失了。
在Android裏面那些由主題所提供的資源,例如Bitmaps,Drawables都是一塊兒打包到統一的Texture紋理當中,而後再傳遞到GPU裏面,這意味着每次你須要使用這些資源的時候,都是直接從紋理裏面進行獲取渲染的。固然隨着UI組件的愈來愈豐富,有了更多演變的形態。例如顯示圖片的時候,須要先通過CPU的計算加載到內存中,而後傳遞給GPU進行渲染。文字的顯示更加複雜,須要先通過CPU換算成紋理,而後再交給GPU進行渲染,回到CPU繪製單個字符的時候,再從新引用通過GPU渲染的內容。動畫則是一個更加複雜的操做流程。
爲了可以使得App流暢,咱們須要在每一幀16ms之內處理完全部的CPU與GPU計算,繪製,渲染等等操做。
順滑精妙的動畫是app設計裏面最重要的元素之一,這些動畫可以顯著提高用戶體驗。下面會講解Android系統是如何處理UI組件的更新操做的。
一般來講,Android須要把XML佈局文件轉換成GPU可以識別並繪製的對象。這個操做是在DisplayList的幫助下完成的。DisplayList持有全部將要交給GPU繪製到屏幕上的數據信息。
在某個View第一次須要被渲染時,DisplayList會所以而被建立,當這個View要顯示到屏幕上時,咱們會執行GPU的繪製指令來進行渲染。若是你在後續有執行相似移動這個View的位置等操做而須要再次渲染這個View時,咱們就僅僅須要額外操做一次渲染指令就夠了。然而若是你修改了View中的某些可見組件,那麼以前的DisplayList就沒法繼續使用了,咱們須要回頭從新建立一個DisplayList而且從新執行渲染指令並更新到屏幕上。
須要注意的是:任什麼時候候View中的繪製內容發生變化時,都會從新執行建立DisplayList,渲染DisplayList,更新到屏幕上等一系列操做。這個流程的表現性能取決於你的View的複雜程度,View的狀態變化以及渲染管道的執行性能。舉個例子,假設某個Button的大小須要增大到目前的兩倍,在增大Button大小以前,須要經過父View從新計算並擺放其餘子View的位置。修改View的大小會觸發整個HierarcyView的從新計算大小的操做。若是是修改View的位置則會觸發HierarchView從新計算其餘View的位置。若是佈局很複雜,這就會很容易致使嚴重的性能問題。咱們須要儘可能減小Overdraw。
咱們能夠經過前面介紹的Monitor GPU Rendering來查看渲染的表現性能如何,另外也能夠經過開發者選項裏面的Show GPU view updates來查看視圖更新的操做,最後咱們還能夠經過HierarchyViewer這個工具來查看佈局,使得佈局儘可能扁平化,移除非必需的UI組件,這些操做可以減小Measure,Layout的計算時間。
引發性能問題的一個很重要的方面是由於過多複雜的繪製操做。咱們能夠經過工具來檢測並修復標準UI組件的Overdraw問題,可是針對高度自定義的UI組件則顯得有些力不從心。
有一個竅門是咱們能夠經過執行幾個APIs方法來顯著提高繪製操做的性能。前面有提到過,非可見的UI組件進行繪製更新會致使Overdraw。例如Nav Drawer從前置可見的Activity滑出以後,若是還繼續繪製那些在Nav Drawer裏面不可見的UI組件,這就致使了Overdraw。爲了解決這個問題,Android系統會經過避免繪製那些徹底不可見的組件來儘可能減小Overdraw。那些Nav Drawer裏面不可見的View就不會被執行浪費資源。
可是不幸的是,對於那些過於複雜的自定義的View(重寫了onDraw方法),Android系統沒法檢測具體在onDraw裏面會執行什麼操做,系統沒法監控並自動優化,也就沒法避免Overdraw了。可是咱們能夠經過canvas.clipRect()來幫助系統識別那些可見的區域。這個方法能夠指定一塊矩形區域,只有在這個區域內纔會被繪製,其餘的區域會被忽視。這個API能夠很好的幫助那些有多組重疊組件的自定義View來控制顯示的區域。同時clipRect方法還能夠幫助節約CPU與GPU資源,在clipRect區域以外的繪製指令都不會被執行,那些部份內容在矩形區域內的組件,仍然會獲得繪製。
除了clipRect方法以外,咱們還可使用canvas.quickreject()來判斷是否沒和某個矩形相交,從而跳過那些非矩形區域內的繪製操做。作了那些優化以後,咱們能夠經過上面介紹的Show GPU Overdraw來查看效果。
雖然Android有自動管理內存的機制,可是對內存的不恰當使用仍然容易引發嚴重的性能問題。在同一幀裏面建立過多的對象是件須要特別引發注意的事情。
Android系統裏面有一個Generational Heap Memory的模型,系統會根據內存中不一樣的內存數據類型分別執行不一樣的GC操做。例如,最近剛分配的對象會放在Young Generation區域,這個區域的對象一般都是會快速被建立而且很快被銷燬回收的,同時這個區域的GC操做速度也是比Old Generation區域的GC操做速度更快的。
除了速度差別以外,執行GC操做的時候,任何線程的任何操做都會須要暫停,等待GC操做完成以後,其餘操做纔可以繼續運行。
一般來講,單個的GC並不會佔用太多時間,可是大量不停的GC操做則會顯著佔用幀間隔時間(16ms)。若是在幀間隔時間裏面作了過多的GC操做,那麼天然其餘相似計算,渲染等操做的可用時間就變得少了。
致使GC頻繁執行有兩個緣由:
解決上面的問題有簡潔直觀方法,若是你在Memory Monitor裏面查看到短期發生了屢次內存的漲跌,這意味着頗有可能發生了內存抖動。
同時咱們還能夠經過Allocation Tracker來查看在短期內,同一個棧中不斷進出的相同對象。這是內存抖動的典型信號之一。
當你大體定位問題以後,接下去的問題修復也就顯得相對直接簡單了。例如,你須要避免在for循環裏面分配對象佔用內存,須要嘗試把對象的建立移到循環體以外,自定義View中的onDraw方法也須要引發注意,每次屏幕發生繪製以及動畫執行過程當中,onDraw方法都會被調用到,避免在onDraw方法裏面執行復雜的操做,避免建立對象。對於那些沒法避免須要建立對象的狀況,咱們能夠考慮對象池模型,經過對象池來解決頻繁建立與銷燬的問題,可是這裏須要注意結束使用以後,須要手動釋放對象池中的對象。
JVM的回收機制給開發人員帶來很大的好處,不用時刻處理對象的分配與回收,能夠更加專一於更加高級的代碼實現。相比起Java,C與C++等語言具有更高的執行效率,他們須要開發人員本身關注對象的分配與回收,可是在一個龐大的系統當中,仍是免不了常常發生部分對象忘記回收的狀況,這就是內存泄漏。
原始JVM中的GC機制在Android中獲得了很大程度上的優化。Android裏面是一個三級Generation的內存模型,最近分配的對象會存放在Young Generation區域,當這個對象在這個區域停留的時間達到必定程度,它會被移動到Old Generation,最後到Permanent Generation區域。
每個級別的內存區域都有固定的大小,此後不斷有新的對象被分配到此區域,當這些對象總的大小快達到這一級別內存區域的閥值時,會觸發GC的操做,以便騰出空間來存放其餘新的對象。
前面提到過每次GC發生的時候,全部的線程都是暫停狀態的。GC所佔用的時間和它是哪個Generation也有關係,Young Generation的每次GC操做時間是最短的,Old Generation其次,Permanent Generation最長。執行時間的長短也和當前Generation中的對象數量有關,遍歷查找20000個對象比起遍歷50個對象天然是要慢不少的。
雖然Google的工程師在儘可能縮短每次GC所花費的時間,可是特別注意GC引發的性能問題仍是頗有必要。若是不當心在最小的for循環單元裏面執行了建立對象的操做,這將很容易引發GC並致使性能問題。經過Memory Monitor咱們能夠查看到內存的佔用狀況,每一次瞬間的內存下降都是由於此時發生了GC操做,若是在短期內發生大量的內存上漲與下降的事件,這說明頗有可能這裏有性能問題。咱們還能夠經過Heap and Allocation Tracker工具來查看此時內存中分配的到底有哪些對象。
雖然Java有自動回收的機制,但是這不意味着Java中不存在內存泄漏的問題,而內存泄漏會很容易致使嚴重的性能問題。
內存泄漏指的是那些程序再也不使用的對象沒法被GC識別,這樣就致使這個對象一直留在內存當中,佔用了寶貴的內存空間。顯然,這還使得每級Generation的內存區域可用空間變小,GC就會更容易被觸發,從而引發性能問題。
尋找內存泄漏並修復這個漏洞是件很棘手的事情,你須要對執行的代碼很熟悉,清楚的知道在特定環境下是如何運行的,而後仔細排查。例如,你想知道程序中的某個activity退出的時候,它以前所佔用的內存是否有完整的釋放乾淨了?首先你須要在activity處於前臺的時候使用Heap Tool獲取一份當前狀態的內存快照,而後你須要建立一個幾乎不這麼佔用內存的空白activity用來給前一個Activity進行跳轉,其次在跳轉到這個空白的activity的時候主動調用System.gc()方法來確保觸發一個GC操做。最後,若是前面這個activity的內存都有所有正確釋放,那麼在空白activity被啓動以後的內存快照中應該不會有前面那個activity中的任何對象了。
若是你發如今空白activity的內存快照中有一些可疑的沒有被釋放的對象存在,那麼接下去就應該使用Alocation Track Tool來仔細查找具體的可疑對象。咱們能夠從空白activity開始監聽,啓動到觀察activity,而後再回到空白activity結束監聽。這樣操做之後,咱們能夠仔細觀察那些對象,找出內存泄漏的真兇。
一般來講,Android對GC作了大量的優化操做,雖然執行GC操做的時候會暫停其餘任務,但是大多數狀況下,GC操做仍是相對很安靜而且高效的。可是若是咱們對內存的使用不恰當,致使GC頻繁執行,這樣就會引發不小的性能問題。
爲了尋找內存的性能問題,Android Studio提供了工具來幫助開發者。
Android Studio中的Memory Monitor能夠很好的幫組咱們查看程序的內存使用狀況。
電量實際上是目前手持設備最寶貴的資源之一,大多數設備都須要不斷的充電來維持繼續使用。不幸的是,對於開發者來講,電量優化是他們最後纔會考慮的的事情。可是能夠肯定的是,千萬不能讓你的應用成爲消耗電量的大戶。
Purdue University研究了最受歡迎的一些應用的電量消耗,平均只有30%左右的電量是被程序最核心的方法例如繪製圖片,擺放佈局等等所使用掉的,剩下的70%左右的電量是被上報數據,檢查位置信息,定時檢索後臺廣告信息所使用掉的。如何平衡這二者的電量消耗,就顯得很是重要了。
有下面一些措施可以顯著減小電量的消耗:
咱們能夠經過手機設置選項找到對應App的電量消耗統計數據。咱們還能夠經過Battery Historian Tool來查看詳細的電量消耗。
若是發現咱們的App有電量消耗過多的問題,咱們可使用JobScheduler API來對一些任務進行定時處理,例如咱們能夠把那些任務重的操做等到手機處於充電狀態,或者是鏈接到WiFi的時候來處理。 關於JobScheduler的更多知識能夠參考http://hukai.me/android-training-course-in-chinese/background-jobs/scheduling/index.html
電量消耗的計算與統計是一件麻煩並且矛盾的事情,記錄電量消耗自己也是一個費電量的事情。惟一可行的方案是使用第三方監測電量的設備,這樣纔可以獲取到真實的電量消耗。
當設備處於待機狀態時消耗的電量是極少的,以N5爲例,打開飛行模式,能夠待機接近1個月。但是點亮屏幕,硬件各個模塊就須要開始工做,這會須要消耗不少電量。
使用WakeLock或者JobScheduler喚醒設備處理定時的任務以後,必定要及時讓設備回到初始狀態。每次喚醒無線信號進行數據傳遞,都會消耗不少電量,它比WiFi等操做更加的耗電,詳情請關注http://hukai.me/android-training-course-in-chinese/connectivity/efficient-downloads/efficient-network-access.html
修復電量的消耗是另一個很大的課題,這裏就不展開繼續了。
高效的保留更多的電量與不斷促使用戶使用你的App來消耗電量,這是矛盾的選擇題。不過咱們可使用一些更好的辦法來平衡二者。
假設你的手機裏面裝了大量的社交類應用,即便手機處於待機狀態,也會常常被這些應用喚醒用來檢查同步新的數據信息。Android會不斷關閉各類硬件來延長手機的待機時間,首先屏幕會逐漸變暗直相當閉,而後CPU進入睡眠,這一切操做都是爲了節約寶貴的電量資源。可是即便在這種睡眠狀態下,大多數應用仍是會嘗試進行工做,他們將不斷的喚醒手機。一個最簡單的喚醒手機的方法是使用PowerManager.WakeLock的API來保持CPU工做並防止屏幕變暗關閉。這使得手機能夠被喚醒,執行工做,而後回到睡眠狀態。知道如何獲取WakeLock是簡單的,但是及時釋放WakeLock也是很是重要的,不恰當的使用WakeLock會致使嚴重錯誤。例如網絡請求的數據返回時間不肯定,致使原本只須要10s的事情一直等待了1個小時,這樣會使得電量白白浪費了。這也是爲什麼使用帶超時參數的wakelock.acquice()方法是很關鍵的。可是僅僅設置超時並不足夠解決問題,例如設置多長的超時比較合適?何時進行重試等等?
解決上面的問題,正確的方式多是使用非精準定時器。一般狀況下,咱們會設定一個時間進行某個操做,可是動態修改這個時間也許會更好。例如,若是有另一個程序須要比你設定的時間晚5分鐘喚醒,最好可以等到那個時候,兩個任務捆綁一塊兒同時進行,這就是非精肯定時器的核心工做原理。咱們能夠定製計劃的任務,但是系統若是檢測到一個更好的時間,它能夠推遲你的任務,以節省電量消耗。
這正是JobScheduler API所作的事情。它會根據當前的狀況與任務,組合出理想的喚醒時間,例如等到正在充電或者鏈接到WiFi的時候,或者集中任務一塊兒執行。咱們能夠經過這個API實現不少免費的調度算法。
從Android 5.0開始發佈了Battery History Tool,它能夠查看程序被喚醒的頻率,又誰喚醒的,持續了多長的時間,這些信息均可以獲取到。
請關注程序的電量消耗,用戶能夠經過手機的設置選項觀察到那些耗電量大戶,並可能決定卸載他們。因此儘可能減小程序的電量消耗是很是有必要的。