Android應用性能優化實踐

原文連接 : Speed up your app
原文做者 : UDI COHEN
譯文出自 : 開發技術前線 www.devtf.cn。未經容許,不得轉載!
譯者 : zijianwang90
校對者:
狀態 : 完成html

幾周以前,我在Droidcon NYC上有過一次關於Android性能優化的演講。java

我在這個演講中花費了大量的時間,由於我想經過真實的例子展示性能問題,以及我是經過什麼樣的工具去發掘這些問題的。由於時間緣由,在演講中我不得不捨棄一半的內容。在這篇文章中,我會總結在演講中我所討論的全部內容,而且給出實例android

點此連接進入演講視頻git

如今,咱們來逐一討論我在演講中說起的一些重點內容,但願個人闡述足夠的清晰。首先,在我進行性能優化的時候我遵循以下原則:github

原則

每當我遇到性能問題,或者嘗試發現性能問題的時候,我會遵循以下原則:web

  • 堅持性能測試 – 不要用你的眼睛去優化性能。也許在你盯着同一個動畫看了幾回以後,你會開始相信他運行的愈來愈流暢了。數據不會說謊。在你優化你的代碼以前以及以後,使用咱們將要介紹的一系列工具,去屢次的測試你的app到底性能幾何。
  • 使用低端設備 – 若是你想要你想暴露你應用的性能問題,低端設備每每會更加的容易。性能強大的設備每每不會太在乎你應用上面的一些優化問題,且不是全部用戶都在使用這些旗艦設備。
  • 權衡 – 性能的優化始終圍繞着權衡這兩個字。你在某一個點上的優化可能會形成另外一點上出現問題。在不少狀況下,你會花大量的時間尋找並解決這些問題,但形成這些問 題的緣由也可能使由於例如bitmaps的質量,或是你沒有使用正確的數據結構去存儲你的數據。因此你要時刻準備好做出必定的犧牲

Systrace

Systrace是一個很是好但卻有可能被你忽視的工具,這是由於開發者們每每不肯定Systrace可以爲他們提供什麼樣的信息。shell

Systrace會展現一個運行在手機上程序情況的概覽。這個工具提醒了咱們手機實際上是一個能夠在同一時間完成不少工做的電腦。在最近的一次SDK更新中,這個工具在數據分析能力上獲得了提高,用以幫助咱們尋找性能問題之所在。緩存

下面讓咱們來看看Systrace長什麼樣子:安全

systrace-overview

你能夠經過Android Device Monitor Tool或者是命令行來生成Systrace文件,想了解更多猛戳此處性能優化

在視頻中,我向你們介紹了Systrace中不一樣區域的功能。固然最有趣的仍是Alerts和Frames兩欄,它們展現了經過手機來的數據而生成出來的可視化分析結果。讓咱們來選擇最上方的alerts瞧瞧:

systrace-alert

這個警告指出了,有一個View#draw()方法執行了比較長的時間。咱們能夠在下面看到問題的描述,連接,甚至是相關的視頻。下面咱們看 Frames這一行,能夠看到這裏展現了被繪製出來的每一幀,而且用綠、黃、紅三顏色來區分它們在繪製時的性能。咱們選一個紅色幀來瞅瞅:

systrace-frame

在最下方,咱們看到了與這一幀所相關的一些警告。在這三個警告中,有一個是咱們上面所提到的(View#draw())。接下來咱們在這一幀處放大並在下方展開「Inflation during ListView recycling」這條警告:

systrace-frame-zoomin

咱們能夠看到警告部分的總耗時,32毫秒,遠高於了咱們對保障60fps所需的16毫秒繪製時間。同時還有更多的ListView每一個條目的繪製時 間,大約是6毫秒每一個條目,總共五個。而Description描述項中的內容會幫助咱們理解問題,甚至提供問題的解決方案。回到咱們上一張圖片,咱們可 以在「inflate」這一個塊區處放大,而且觀察究竟是哪些View在被填充過程當中耗時比較嚴重。

下面是另一個渲染過慢的實例:

systrace-2-frame

在選擇了某一幀以後,咱們能夠按「m」鍵來高亮這一幀,而且在上方看到了這一部分的耗時,如圖,咱們看到了這一陣的繪製總共耗時超過19毫秒。而當咱們展開這一幀惟一的一個警告時,咱們發現了「Scheduling delay」這條錯誤。

Scheduling delay(調度延遲)的意思就是一個線程在處理一塊運算的時候,在很長一段時間都沒有被分配到CPU上面作運算,從而致使這個線程在很長一段時間都沒有完成工做。咱們選擇這一幀中最長的一塊,從而獲得更加詳細的信息:

systrace-2-slice

在紅框區域內,咱們看到了「Wall duration」,他表明着這一區塊的開始到結束的耗時。之因此叫做「Wall duration」,是由於他就像是牆上的一個時鐘,從線程的一開始就爲你計時。

可是,CPU Duration一項中顯示了實際CPU在處理這一區塊所消耗的時間。

很顯然,兩個時間的差距仍是很是大的。整個區塊耗時18毫秒,而在這之中CPU只消耗了4毫秒的時間去運算。這就有點奇怪了,因此咱們應該看一下在這整個過程之中,CPU去幹嘛了。

systrace-2-cpu

能夠看到,全部四個線程都很是的繁忙。

選擇其中的一個線程會告訴咱們是哪一個程序在佔用他,在這裏是一個包名爲com.udinic.keepbusyapp的程序。在這裏,因爲另一個程序佔用CPU,致使了咱們的程序未能得到足夠的CPU資源。

可是這種狀況實際上是暫時的,由於被其餘後臺應用佔用CPU的狀況並很少見(- -),但仍有其餘應用的線程或是主線程佔用CPU。而Traceview也只能爲咱們提供一個概覽,他的深度是有限的。因此要找到咱們app中究竟是什麼 讓咱們的CPU繁忙,咱們還要藉助另外一個工具——Traceview。

Traceview

Traceview是一個性能測試工具,展現了全部方法的的運行時間。下面讓咱們來瞅瞅他是啥樣的:

traceview-overview

這個工具能夠從Android Device Monitor中打開也能夠經過代碼打開。更多的消息信息清看這裏

下面讓咱們來看看每一列的含義:

  • Name – 方法名,以及他們在上面圖表中所對應的顏色。
  • Inclusive CPU Time – CPU在處理這個方法以及全部子方法(如被他調用的全部方法)的總耗時。
  • Exclusive CPU Time – CPU在處理這一個單獨方法的總耗時。
  • Inclusive/Exlusive Real Time – 從方法的開始執行到執行結束的總耗時,和Systrace中的「Wall duration」相似
  • Calls+Recursion – 這個方法被調用的次數,以及被遞歸調用的次數。
  • CPU/Real time per Call – 在處理這個方法時的CPU耗時的平均值以及實際耗時的平均值。另外的列展現了這個方法全部調用的累計耗時

我打開一個滑動不太順滑的應用。開啓記錄,滑動一點後中止記錄。展開getView()方法,以下圖:

traceview-getview

這個方法被調用了12次,每次CPU會消耗3毫秒左右,可是每次調用的總耗時卻高達162毫秒!絕對有問題啊!

而看看這個方法的children,咱們能夠看到這其中的每一個方法在耗時方面是如何分佈的。Thread.join()方法戰局了98%的 inclusive real time。這個方法在等待另外一個線程結束的時候被調用。在Children中另一個方法就是Tread.start()方法,而之因此整個方法耗時很 長,我猜想是由於在getView()方法中啓動了線程而且在等待它的結束。

可是這個線程在哪兒?

咱們在getView()方法中並不能看到這個線程作了什麼,由於這段邏輯不在getView()方法之中。因而我找到了Thread.run()方法,就是在線程被建立出來時候所運行的方法。而跟隨這個方法一路向下,我找到了問題的元兇。

traceview-thread

我發現了BgService.doWork()方法的每次調用花費了將近14毫秒,而且有四十個這東西!並且getView()中還有可能調用屢次 這個方法,這就解釋了爲何getView()方法執行時間如此之長。這個方法讓CPU長時間的保持在了繁忙狀態。而看看Exclusive CPU time,咱們能夠看到他佔據了80%的CPU時間!此外,根據Exclusive CPU time排序,能夠幫咱們更好的定位那些耗時很長的方法,而他們頗有可能就是形成性能問題的罪魁禍首。

關注這些耗時方法,例如getView(),View#onDraw()等方法,能夠很好的幫助咱們尋找爲何應用運行緩慢的緣由。但有些時候,還 會有一些其餘的東西來佔用寶貴的CPU資源,而這些資源若是被運用在UI的繪製上,也許咱們的應用會更加流暢。Garbage Collector垃圾回收機制會不時的運行,回收那些沒用的對象,一般來說這不會影響咱們在前臺運行的程序。但若是GC被運行的過於頻繁,他一樣能夠影 響咱們應用的執行效率。而咱們該如何知道回收的是否過於頻繁了呢…

內存調優 Memory Profiling

Android Studio在最近的更新中給予了咱們更增強大的工具去分析性能問題。在底部Android選項中的Memory選項卡,會顯示有多大的數據在何時被分配到了堆內存之中,他是長成這個樣子的:

mem-graph

而當圖表中出現一個小的下滑的時候,說明GC回收發生了,他清除了沒必要要的對象而且騰出了必定的堆空間。而在這張圖表的左側有兩個工具供咱們使用,Head dump和Allocation Tracker。

Heap dump

爲了找出究竟是什麼正在佔用咱們的堆內存,咱們可使用左邊的heap dump按鈕。他會提供一個堆內存佔用狀況的快照,而且會在Android Studio中打開一個單獨的報告界面。

heap-overview

在左側,咱們看到一個圖標展現了堆中全部的實例,按照類進行分組。而對於每個實例,會展現有多少個實例的對象被分配到堆中,以及他們的所佔用的空 間(Shallow size淺尺寸),以及這些對象在內存中仍然佔用的空間,後者告訴了咱們多少的內存空間將會被釋放若是這些實例被釋放。這個工具可讓咱們直觀的觀察處內 存是被如何佔用的,幫助咱們分析咱們使用的數據結構和對象之間的關係,以便發現問題並使用更加高效的數據結構,解開和對象之間的關聯,而且下降 Ratained Memory的佔用。而最終目的,就是儘量的下降咱們的內存佔用。

回過頭來看圖表,咱們發現MemoryActivity存在39個實例,這對於一個Activity來講有點奇怪。在右邊選擇其中的一個實例,會在下方看到全部的對這個實例的引用樹狀列表。

heap-reftree

其中一個是ListenersManager對象中的一個集合。而觀察這個activity的其餘實例,就會他們都由於這個對象而被保留在了內存之中。這也解釋了爲何這些對象佔用瞭如此多的內存:

heap-retained

這個現象就叫作「內存泄露」,咱們的activity已經被銷燬,可是他們的對象卻由於始終被引用着而沒法被垃圾回收。咱們能夠避免這種狀況,例如 確保這些對象再被銷燬後不會被其餘對象一直引用着。在咱們這個例子中,在Activity被銷燬後,ListernesManager並不須要保持着對這 些對象的引用。因此解決辦法就是在onDestroy()回調方法中移除這些引用。

內存泄露以及其餘較大的對象會在堆中佔據不少的控件,它們減小着可用內存的同時也頻繁的形成垃圾回收。而垃圾回收又回形成CPU的繁忙,而堆內存並不會變得更大,最終就會致使更悲劇的結果發生:OutOfMemoryException內存溢出,並致使程序崩潰。

另一個更先進的工具就是Eclipse Memory Analyzer Tool (Eclipse MAT):

eclipse-mat

這個工具能夠作全部Android Studio能夠作的,而且辨別可能出現的內存泄露,以及提供更加高級的搜索功能,例如搜索全部大於2MB的Bitmap實例,或是搜索全部空的Rect對象

另一個很好的工具是LeakCanary,是一個第三方庫,能夠觀察應用中的對象而且確保它們沒有形成泄漏。而若是形成泄漏了,會有一個推送來提醒你在哪裏發生了什麼。

leakcanary

Allocation Tracker

咱們能夠在內存圖表的左側找到Allocation Tracker的啓動和中止按鈕。他會生成一個在必定時間內被生成的全部實例的報告,而且按照類分型分組:

alloc-class

或者按照方法分組:

alloc-method

同時它還能經過美觀的可視化界面,告訴咱們哪些方法或類擁有最多的實例。

利用這些信息,咱們能夠找到哪些佔用過多內存,引起過屢次垃圾回收且又對耗時很是敏感的方法。咱們也能夠利用這個工具找到不少短命的相同類的實例,從而能夠考慮使用對象池的思想去儘可能的減小過多的實例被建立。

常見內存小技巧

如下是一些我寫代碼時候遵循的規律或是技巧:

  • 枚舉在性能問題上一直是一個被常常討論的話題。這裏是一個討論枚舉的視頻,指出了枚舉所消耗的內存空間,這還有一段關於這個視頻的討論,固然其中存在着一些誤導。可是回過頭來,枚舉真的比通常的常量更加佔用空間嗎?確定的。可是這必定很差嗎?未必。若是你在編寫一個library庫而且須要很強的類型安全性,那麼也許可使用枚舉而非其餘辦法,例如@IntDef。而若是你只是有一堆的常量,使用枚舉也許就不能麼明智了。仍是那句話,在你作決定以前必定要權衡與取捨。
  • 自動裝箱 – 自動裝箱是一個從原始數據類型到對象型數據的裝箱過程(例如int到Integer)。每當一個原始類型數據被裝箱到一個對象類型數據,一個新的對象就產 生了(震驚吧。。)。因此若是發生了不少次的自動裝箱,勢必會加快GC的執行頻率,並且自動裝箱是很容易被咱們忽視的。而解決辦法,在使用這些類型的時候 儘可能一致,若是你在應用中徹底使用原始數據類型,那麼儘可能避免他被平白無故的自動封裝。你可使用咱們上面提到的memery profiling工具去尋找這些過於大量的對象類型數據,也能夠經過Traceview去尋找相似 Integer.valueOf(),Long.valueOf()這樣的方法來判斷是否發生了大量沒必要要的自動封裝。
  • HashMap vs ArrayMap / Sparse*Array – 既然提到了自動裝箱的問題,那麼使用HashMap的話,就須要咱們使用對象類型做爲鍵。而若是咱們在整個應用中使用的都是基本數據類型的「int」,那 麼在咱們使用HashMap時候就會發生自動裝箱,而這時也許咱們就能夠考慮使用SparseIntArray。而假如咱們仍然須要鍵爲對象類型,那麼我 們可使用ArrayMap。ArrayMap和HashMap很相似,可是在底層的實現原理卻不盡相同, 這也會讓咱們更加高效的使用內存,但要付出必定的性能代價。兩種方法都會比HashMap更加節省內存空間,可是相比於HashMap,查詢和增刪的速度 上會有必定的犧牲。固然,除非你具備至少1000條的數據源,不然在運行時也不會對速度形成太大的影響,這也是你使用他們替代HashMap的緣由之一。
  • 注意Context – 在咱們前面也看到了,Activity是很是容易形成內存泄露的。在Android中,最容易形成內存泄露的當屬Activity。而且這些內存泄露會浪 費大量的內存,由於他們持有着他們UI中全部的View,而這些View一般會佔據不少的控件。在開發過程當中的不少操做須要Context,而我一般也會 使用Activity來傳遞。因此必定要搞清楚你對這個Activity作了什麼。若是一個引用被緩存起來了,且這個對象的生命週期比你的 Activity還要長,那麼在咱們解除這個引用以前,就會形成內存泄露了。
  • 避免非靜態 – 當咱們建立非靜態內部類,而且初始化它的時候,在其內部會建立一個外部類的隱式引用。而若是內部類的生命週期比外部類還要長,那麼外部類也一樣會被保留在 內存之中,儘管咱們已經徹底不須要它了。例如,在Activity內建立了一個繼承自AsyncTask的內部類,完後在Activity運行的時候啓動 這個async task,再殺掉Activty。那麼這時候這個async task會保持着這個Activity直到執行結束。而解決辦法也很簡單,不要這麼作,儘可能使用靜態內部類。

監測GPU(GPU Profiling)

在Android 1.4中的一個全新工具,就是能夠查看GPU繪製。

gpu-overview

每一條線意味着一幀被繪製出來,而每條線中的不一樣顏色又表明着在繪製過程當中的不一樣階段:

  • Draw (藍色) 表明着View#onDraw()方法。在這個環節會建立/刷新DisplayList中的對象,這些對象在後面會被轉換成GPU能夠明白的OpenGL 命令。而這個值比較高多是由於view比較複雜,須要更多的時間去建立他們的display list,或者是由於有太多的view在很短的時間內被建立。
  • Prepare (紫色) – 在Lollipop版本中,一個新的線程被加入到了UI線程中來幫助UI的繪製。這個線程叫做RenderThread。它負責轉換display list到OpenGL命令而且送至GPU。在這過程當中,UI線程能夠繼續開始處理後面的幀。而在UI線程將全部資源傳遞給RenderThread過程 中所消耗的時間,就是紫色階段所消耗的時間。若是在這過程當中有不少的資源都要進行傳遞,display list會變得過多過於沉重,從而致使在這一階段過長的耗時。
  • Process (紅色) – 執行Display list中的內容並建立OpenGL命令。若是有過多或者過於複雜的display list須要執行的話,那麼這階段會消耗較長的時間,由於這樣的話會有不少的view被重繪。而重繪每每發生在界面的刷新或是被移動出了被覆蓋的區域。
  • Execute (黃色) – 發送OpenGL命令到GPU。這個階段是一個阻塞調用,由於CPU在這裏只會發送一個含有一些OpenGL命令的緩衝區給GPU,而且等待GPU返回空 的緩衝區以便再次傳遞下一幀的OpenGL命令。而這些緩衝區的總量是必定的,若是GPU太過於繁忙,那麼CPU則會去等待下一個空緩衝區。因此,若是我 們看到這一階段耗時比較長,那多是由於GPU過於繁忙的繪製UI,而形成這個的緣由則多是在短期內繪製了過於複雜的view。

在Marshmallow版本中,有更多的顏色被加了進來,例如Measure/Layout階段,input handling輸入處理,以及一些其餘的:

gpu-colors-marsh

在使用這些功能以前,你須要在開發者選項中開啓GPU rendering(GPU呈現模式分析):

gpu-settings2

接下來咱們就能夠經過如下這條adb命令獲得咱們想要獲得的全部信息:

咱們能夠本身收集這些信息並建立圖表。這個命令也會打印出一些其餘有用的信息,例如view層級中的層數,display lists的大小等等。在Marshmallow中,咱們也會獲得更多的信息:

gpu-adb

若是咱們須要自動化測試咱們的app,那麼咱們能夠本身建立服務器去運行在特定節點執行這些命令(如列表滾動,重度動畫等),並觀察這些數值的變 動。這能夠幫助咱們找出在哪裏出現了性能的降低,而且產品上線以前找到問題的所在。咱們也可以經過」framestats」關鍵字來找到更多更加精確的數 據,這裏有更詳盡的解釋

但這可不是獲取GPU Rendering數據的惟一方式!

咱們在開發者選項中看過了GPU呈現模式分析內的Profile GPU Rendering」選項後,還有另一個選項就是」On screen as bars」(在屏幕上顯示爲條形圖)。打開這個後,咱們就能夠直觀的看到每一幀在繪製過程當中所消耗的時間,綠色的橫線則表明16ms的60fps零界值。

gpu-onscreen

在右邊的例子中,咱們能夠看到不少幀都超出了綠線,這也意味着它花了多餘16毫秒的時間去繪製。而藍色佔據了這些線條的主體,咱們知道這多是由於 過多或是過於複雜的view在被繪製。在這種狀況下,當我滑動列表,由於列表中view的結構比較複雜,有一些view已經被繪製完成而一些由於過於複雜 還處於繪製階段,而這可能就是形成這些幀超過綠線的緣由——繪製起來實在太複雜了。

Hierarchy Viewer

我很是喜歡這個工具,同時也由於那麼多人徹底不用而感到一絲的悲涼。

使用Hierarchy Viewer,咱們能夠得到性能數據,觀察View層級中的每個View,而且能夠看到全部View的屬性。咱們一樣能夠導出theme數據,這樣能夠 看到每個style中的屬性值,可是咱們只能在單獨運行Hierarchy Viewer的時候才能這麼幹,而非經過Android Monitor。一般在我進行佈局設計以及佈局優化的時候,我會使用到這個工具。

hierview-overview

在正中間咱們看到的樹狀結構就表明了View的層級。View的層級能夠很寬,但若是太寬的話(10層左右),也許會在佈局和測量階段消耗大量的性 能。在每一次View經過View#onMeasure()方法測量的時候,或是經過View#onLayout()方法佈局他的全部子view的時候, 這些方法又回傳遞到它全部的子view上面而且重頭來過。有的佈局會將上述步驟作兩次,例如RelativeLayout以及某些經過配置的 LinearLayout,而若是它們又層層嵌套,那麼這些方法的傳遞會大量的增長。

在右下方,咱們看到了一個咱們佈局的「藍圖」,標註了每個view的位置。當咱們點擊這裏(或者從樹狀結構中),咱們會在左側看到他全部的屬性。 在設計佈局時候,有時候我不肯定爲何一個view被擺在那裏,而使用這個工具,我能夠在樹狀圖中找到這個view,選擇,並觀察他在預覽窗口中的位置。 我還經過view在屏幕上最終的繪製尺寸,來設計有趣的動畫,而且使用這些信息讓動畫或者View的位置更加的精準。我也能夠經過這個工具來尋找被其餘 View不當心蓋住從而找不到的View,等等等等。

hierview-colors

對於每個view咱們能夠得到他測量、佈局以及繪製的用是和它所包含的全部子view。在這裏顏色表明了這個view在繪製過程當中,相比樹中其餘 view的性能表現,這是咱們找到這些性能不足view的最佳途徑。鑑於咱們可以看到全部view的預覽,咱們能夠沿着樹狀圖,跟隨view被建立的順 序,找尋那些能夠被捨棄的多餘步驟。而其中之一,也是對性能影響很是大的,就是過分繪製。

過分繪製

正如咱們在GPU Profiling部分看到的,在Execute黃色階段,若是GPU有過多的東西要在屏幕上繪製,整個階段會消耗更多的時間,同事也增長了每一幀所消耗 的時間。過分繪製每每發生在咱們須要在一個東西上面繪製另一個東西,例如在一個紅色的背景上畫一個黃色的按鈕。那麼GPU就須要先畫出紅色背景,再在他 上面繪製黃色按鈕,此時過分繪製就是不可避免的了。若是咱們有太多層須要繪製,那麼則會過分的佔用GPU致使咱們每幀消耗的時間超過16毫秒。

overdraw-gif

使用「Debug GPU Overdraw」(調試過分繪製)功能,全部的過分繪製會以不一樣顏色的形式展現在屏幕上。1x或是2x的過分繪製沒啥問題,即使是一小塊淺紅色區域也不算太壞,但若是咱們看到太多的紅色區域在屏幕上,那可能就有問題了,讓咱們來看幾個例子:

overdraw-examples

在左邊的例子中,咱們看到列表部分是綠色的,一般還OK,可是在上方發生覆蓋的區域是一片紅色,那就有問題了。在右邊的例子中,整個列表都是淺紅 色。在兩個例子中,都各有一個不透明的列表存在2x或3x的過分繪製。這些過分繪製可能發生在咱們給Activity或Fragment設置了全屏的背 景,同時又給ListView以及ListView的條目設置了背景色。而經過只設置一次背景色便可解決這樣的問題。

注意:默認的主題會爲你指定一個默認的全屏背景色,若是你的activity又一個不透平的背景蓋住了默認的背景色,那麼你能夠移除主題默認的背景 色,這樣也會移除一層的過分繪製。這能夠經過配置主題配置或是經過代碼的方法,在onCreate()方法中調用 getWindow().setBackgroundDrawable(null)方法來實現。

而使用Hierarchy Viewer,你能夠導出一個全部view層級的PSD文件,在Photoshop中打開,而且調查不一樣的layout以及不一樣的層級,也可以發現一些在 佈局中存在的過分繪製。而使用這些信息能夠移除沒必要要的過分繪製。並且,不要看到綠色就知足了,衝着藍色去!

透明度

使用透明度可能會影響性能,可是要去理解爲何,讓咱們瞅瞅當咱們給view設置透明度的時候到底發生了什麼。咱們來看一下下面這個佈局:

alpha-before

咱們看到這個layout中又三個ImageView而且重疊擺放。在使用最常規的設置透明度的方法setAlpha()時,方法會傳遞到沒一個子 view上面,在這裏是每個ImageView。然後,這些ImageView會攜帶新的透明值被繪製入幀緩衝區。而結果就是:

alpha-direct

這並非咱們想要看到的結果。

由於每個ImageView都被賦予了一個透明值,致使了本應覆蓋的部分被融合在一塊兒。幸運的是,系統爲咱們解決了這個問題。佈局會被複制到一個非屏幕區域緩衝區中,而且以一個總體來接收透明度,其結果再被複制到幀緩衝區。結果就是:

alpha-complex

可是,咱們是要付出性能上面的代價的。

假如在幀緩衝區內繪製以前,還要在off-screen緩衝區中繪製一遍的話,至關於增長了一層不可見的繪製層。而系統並不知道咱們是但願這個透明 度以何種的形式去展示,因此係統一般會採用相對複雜的一種。可是也有不少設置透明度的方法可以避免在off-screen緩衝區中的複雜操做:

  • TextView – 使用setTextColor()方法替代setAlpha()。這種方法使用Alpha通道來改變字色,字也會直接使用它進行繪製。
  • ImageView – 使用setImageAlpha()方法替代setAlpha()。原理同上。
  • 自定義控件 – 若是你的自定義控件並不支持相互覆蓋,那就無所謂了。全部的子view並不會想上面的例子中同樣,由於覆蓋而相互融合。而經過複寫 hasOverlappingRendering()並將其返回false後,便會通知系統使用最直接的方式繪製view。同時咱們也能夠經過複寫 onSetAlpha()返回true來手動操控設置透明度後的邏輯。

硬件加速

在Honeycomb版本中引入了硬件加速(Hardware Accleration)後,咱們的應用在繪製的時候就有了全新的繪製模型。它引入了DisplayList結構,用來記錄View的繪製命令,以便更快的進行渲染。但還有一些很好的功能開發者們每每會忽略或者使用不當——View layers。

使用View layers(硬件層),咱們能夠將view渲染入一個非屏幕區域緩衝區(off-screen buffer,前面透明度部分提到過),而且根據咱們的需求來操控它。這個功能主要是針對動畫,由於它能讓複雜的動畫效果更加的流暢。而不使用硬件層的 話,View會在動畫屬性(例如coordinate, scale, alpha值等)改變以後進行一次刷新。而對於相對複雜的view,這一次刷新又會連帶它全部的子view進行刷新,並各自從新繪製,至關的耗費性能。使 用View layers,經過調用硬件層,GPU直接爲咱們的view建立一個結構,而且不會形成view的刷新。而咱們能夠在避免刷新的狀況下對這個結構進行進行 不少種的操做,例如x/y位置變換,旋轉,透明度等等。總之,這意味着咱們能夠對一個讓一個複雜view執行動畫的同時,又不會刷新!這會讓動畫看起來更 加的流暢。下面這段代碼咱們該如何操做:

很簡單,對吧?

是的,可是再使用硬件layers的時候仍是有幾點要牢記在心:

  • 回收 – 硬件層會佔用GPU中的一塊內存。只在必要的時候使用他們,好比動畫,而且過後注意回收。例如在上面ObjectAnimator的例子中,咱們增長了一 個動畫結束監聽以便在動畫結束後能夠移除硬件層。而在Property animator的例子中,咱們使用了withLayers(),這會在動畫開始時候自動建立硬件層而且在結束的時候自動移除。
  • 若是你在調用了硬件View layers後改變了View,那麼會形成硬件硬件層的刷新而且再次重頭渲染一 遍view到非屏幕區域緩存中。這種狀況一般發生在咱們使用了硬件層暫時還不支持的屬性(目前爲止,硬件層只針對如下幾種屬性作了優化:otation、 scale、x/y、translation、pivot和alpha)。例如,若是你另外一個view執行動畫,而且使用硬件層,在屏幕滑動他們的同時改 變他的背景顏色,這就會形成硬件層的持續刷新。而以硬件層的持續刷新所形成的性能消耗來講,可能讓它在這裏的使用變得並不那麼值。

而對於第二個問題,咱們也有一個可視化的辦法來觀察硬件層更新。使用開發者選項中的「Show hardware layers updates」(顯示硬件層更新)

hwl-devoptions2

當打開該選項後,View會在硬件層刷新的時候閃爍綠色。在好久之前我有一個ViewPager在滑動的時候有點不流暢。在開發者模式啓動這個選項後,我再次滑動ViewPager,發現了以下狀況:

hwl-calproblem

左右兩頁在滑動的時候徹底變成了綠色!

這意味着他們在建立的時候使用了硬件層,並且在滑動的時候也界面也進行了刷新。而當我在背景上面使用時差效果而且讓條目有一個動畫效果的時候,這些 處理確實會讓它進行刷新,可是我並無對ViewPager啓動硬件層。在閱讀了ViewPager的源碼後,我發現了在滑動的時候會自動爲左右兩頁啓動 一個硬件層,而且在滑動結束後移除掉。

在兩頁間滑動的時候建立硬件層也是能夠理解的,但對我來講小有不幸。一般來說加入硬件層是爲了讓ViewPager的滑動更加流暢,畢竟它們相對複雜。但這不是個人app所想要的,我不得不經過一些編碼來移除硬件層。

硬件層其實並非什麼酷炫的東西。重要的是咱們要理解他的原理而且合理的使用他們,要否則你確實會遇到一些麻煩。

DIY

在準備上述這一系列例子的過程當中,我進行了不少的編碼去模擬這些情景。你能夠在這個Github項目中找到這些代碼,同時也能夠在Google Play中找到。我用不一樣的Activity區分了不一樣的情景,而且儘可能將他們的用文檔解釋清楚,以便於幫助你們理解不一樣的Activity中是出現哪一種問題。你們能夠邊閱讀各個Activity的javadoc的同時,利用咱們前面講到的工具去玩兒這個App。

更多信息

隨着安卓系統的不斷進化,你有話你的應用的手段也在不斷變多。不少全新的工具被引入到了SDK中,以及一些新的特性被加入到了系統中(比如硬件層這東西)。因此與時俱進和懂得取捨是很是重要的。

這是一個很是棒的油管播放列表,叫Android Performance Patterns,一些谷歌出品的短視頻,講解了不少與性能相關的話題。你能夠找到不一樣數據結構之間的對比(HashMap vs ArrayMap),Bitmap的優化,網絡優化等等,吐血推薦!

加入Android Performance Patterns的G+社羣,和你們一塊兒討論,分享心得,提出問題!

更多有意思的連接:

  • 瞭解安卓中的圖形結構(Graphics Architecture)。例如關於UI的渲染,不一樣的系統組件,好比SurfaceFlinger,以及他們之間是如何交互的。比較長,可是值得一看!
  • Google IO 2012上的一段演講,展現了繪製模型(Drawing model)是如何工做的。
  • 一段來自Devoxx 2013的關於Anrdroid性能的研討,展現了一些在Anrdroid 4.4對繪製模型的一些優化,而且經過demo的形式展現了對不一樣優化工具的使用(Systrace,Overdraw等等)。
  • 一篇很是好的關於「預防性優化」(Preventative Optimizations)的文章,闡述了他和「不成熟的優化」有和區別。不少的開發者並不優化他們的代碼,由於他們認爲這些影響並不明顯。可是記住, 問題也是聚沙成塔的。若是你有機會去優化很小的一點,即使是很是微不足道的一點,也是應該的。
  • 安卓中的內存管理 – 一個2011年的Google IO視頻,仍然值得一看。視頻展現了安卓是如何管理不一樣app的內存的,以及如何使用Eclipse MAT去發現問題。
  • 一個叫作Romain Guy的谷歌工程師的案例研究,經過優化一個第三方的推特客戶端。在這個研究中,Romain展現了他是如何發現問題的,而且建議了相應的解決方案。另外一篇文章跟進了這個問題,展現了這個app在從新制做後的一些其餘問題。

我真心但願你經過這篇文章得到到了足夠豐富的信息和信心,從今天開始優化你的應用吧!

嘗試用工具去記錄,並經過一些開發者選項中的選項,開搞吧。歡迎來G+上分享你在安卓性能優化上面的心得!

相關文章
相關標籤/搜索