爲你的應用加速 - 安卓優化指南

爲你的應用加速 - 安卓優化指南

原則

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

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

Systrace

Systrace是一個非常好但卻有可能被你忽視的工具,這是因爲開發者們往往不確定Systrace能夠爲他們提供什麼樣的信息。

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命令得到我們想要得到的所有信息:

adb shell dumpsys gfxinfo <PACKAGE_NAME>

我們可以自己收集這些信息並創建圖表。這個命令也會打印出一些其他有用的信息,例如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執行動畫的同時,又不會刷新!這會讓動畫看起來更加的流暢。下面這段代碼我們該如何操作:

// Using the Object animator
view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 20f);
objectAnimator.addListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
        view.setLayerType(View.LAYER_TYPE_NONE, null);
    }
});
objectAnimator.start();

// Using the Property animator
view.animate().translationX(20f).withLayer().start();

很簡單,對吧?

是的,但是再使用硬件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在重新制作後的一些其他問題。

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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章