給 App 提速:Android 性能優化總結

英文:Udi Cohen

譯者:伯樂在線 - 至秦

網址:http://android.jobbole.com/81944/


我在幾周前的 Droidcon NYC 會議上,做了一個關於 Android 性能優化的報告。


我花了很多時間準備這個報告,因爲我想要展示實際例子中的性能問題,以及如何使用適合的工具去確認它們 。但由於沒有足夠時間來展示所有的一切,我不得不將幻燈片的內容減半。在本文中,將總結所有我談到的東西,並展示那些我沒有時間討論的例子。


你可以在這裏觀看報告視頻。

幻燈片在這裏可以看到。

現在,讓我們仔細查看一些我之前談過的重要內容 ,但願我可以非常深入地解釋一切。那就先從我在優化時遵循的基本原則開始:


我的原則


每當處理或者排查性能問題的時候,我都遵循這些原則:


  • 持續測量: 用你的眼睛做優化從來就不是一個好主意。同一個動畫看了幾遍之後,你會開始想像它運行地越來越快。數字從來都不說謊。使用我們即將討論的工具,在你做改動的前後多次測量應用程序的性能。

  • 使用慢速設備:如果你真的想讓所有的薄弱環節都暴露出來,慢速設備會給你更多幫助。有的性能問題也許不會出現在更新更強大的設備上,但不是所有的用戶都會使用最新和最好的設備。

  • 權衡利弊 :性能優化完全是權衡的問題。你優化了一個東西 —— 往往是以損害另一個東西爲代價的。很多情況下,損害的另一個東西可能是查找和修復問題的時間,也可能是位圖的質量,或者是應該在一個特定數據結構中存儲的大量數據。你要隨時做好取捨的準備。


Systrace


Systrace 是一個你可能沒有用過的好工具。因爲開發者不知道要如何利用它提供的信息。


Systrace 告訴我們當前大致有哪些程序運行在手機上。這個工具提醒我們,手中的電話實際上是一個功能強大的計算機,它可以同時做很多事情。在SDK 工具的最近的更新中,這個工具增強了從數據生成波形圖的功能,這個功能可以幫助我們找到問題。讓我們觀察一下,一個記錄文件長什麼樣子:



你可以用 Android Device Monitor 工具或者用命令行方式產生一個記錄文件。在這裏可以找到更多的信息。


我在視頻中解釋了不同的部分。其中最有意思的就是警報(Alert)和幀(Frame),展示了對蒐集數據的分析。讓我們觀察一個採集到的記錄文件,在頂部選擇一個警報:



這個警報報告了有一個 View#draw() 調用費時較多。我們得到關於告警的描述,其中包含了關於這個主題的文檔鏈接甚至是視頻鏈接。檢查幀下面那一行,我們看到繪製的每一幀都有一個標識,被標成爲綠色、黃色或者紅色。如果標識是紅色,就說明這幀在繪製時有一個性能問題。讓我們選取一個紅色的幀:




我們在底部看到所有這幀相關的警報。一共有三個,其中之一是我們之前看到的。讓我們放大這個幀並在底部把 “Inflation during ListView recycling” 這個警報報展開:



我們看到這部分一共耗時 32 毫秒,超出了每分鐘 60 幀的要求,這種情況下繪製每一幀的時間不能超過 16 毫秒。幀中 ListView 的每一項都有更多的時間信息 —— 每一項耗時 6 毫秒,我們一共有 5 項。其中的描述幫助我們理解這個問題,甚至還提供了一個解決方案。從上面的圖中,我們看到所有內容都是可視化的,甚至可以放大“擴展”(“inflate”)片,來觀察佈局中的哪個視圖(“View”)擴展時花了更久的時間。


另一個幀繪製較慢的例子:



選擇一幀後,我們可以按下“m” 鍵來高亮並觀察這部分花了多久。上圖中,我們觀察到繪製這幀花費了 19 毫秒。展開這幀對應的唯一警報,它告訴我們有一個“調度延遲”。


調度延遲說明這個處理特定時間片的線程有很長時間沒有被 CPU 調度。因此這個線程花了很長時間才完成。選擇幀中最長的時間片以便獲取更多的詳細信息:




牆上時間(Wall Duration)是指從時間片開始到結束所花費的時間。它被稱爲“牆上時間”,這是因爲線程啓動後就像觀察一個掛鐘(去記錄這個時間)。


CPU時間是 CPU 處理這個時間片所花費的實際時間。


值得注意的是這兩個時間有很大的不同。完成這個時間片花了 18 毫秒,但是 CPU 卻只花費了 4 毫秒。這有點奇怪,現在是個好機會來擡頭看看這整段時間裏 CPU 都做了什麼:




CPU 的 4 個核心都相當忙碌。


選擇一個com.udinic.keepbusyapp 應用程序中的線程。這個例子中,一個不同的應用程序讓 CPU 更加忙碌,而不是爲我們的應用程序貢獻資源。


這種特殊場景通常是暫時的,因爲其它的應用程序不會總是在後臺獨佔 CPU(對嗎?)。這些線程可能出自你應用程序中的其它進程或者甚至來自主進程。因爲 Systrace 是一個總覽工具,有一些限制條件讓我們不能深入下去。我們需要使用另外一個叫做 Traceview 工具,來找出是什麼讓 CPU 一直忙碌。


Traceview


Traceview 是一個性能分析工具,告訴我們每一個方法執行了多長時間。讓我們看一個跟蹤文件:




這個工具可以通過 Android Device Monitor 或者從代碼中啓動。更多信息請參考這裏。


讓我們仔細查看這些不同的列:


  • 名稱:此方法的名字,上圖中用不同的顏色加以標識。

  • CPU非獨佔時間:此方法及其子方法所佔用的 CPU 時間(即所有調用到的方法)。

  • CPU獨佔時間:此方法單獨佔用 CPU 的時間。

  • 非獨佔和獨佔的實際時間 :此方法從啓動那一刻直到完成的時間。和 Systrace 中的“牆上時間”一樣。

  • 調用和遞歸 :此方法被調用的次數以及遞歸調用的數量。

  • 每次調用的 CPU 時間和實際時間 :平均每次調用此方法的 CPU 時間和實際時間。另一個時間字段顯示了所有調用這個方法的時間總和。


我打開一個滑動不流暢的應用程序。我啓動追蹤,滑動了一會然後關掉追蹤。找到 getView() 這個方法然後把它展開,我看到下面的結果:



此方法被調用了 12 次,每次調用 CPU 花費的時間是 3 毫秒,但每次調用實際花費的時間是 162 毫秒!這一定有問題……


查看了這個方法的子方法,可以看到總體時間都花費在哪些方法上。Thread.join() 佔了 98% 左右的非獨佔實際時間。此方法用在等待其他線程結束。另一個子方法是 Thread.start(),我猜想 getView() 方法啓動了一個線程然後等着它執行結束。


但這個線程在哪裏呢?


因爲 getView() 不直接做這件事情,所以 getView() 沒有這樣的子線程。爲找到它 ,我查找一個 Thread.run() 方法,這是生成一個新線程所調用的方法。我追蹤這個方法直至找到元兇:




我發現每次調用 BgService.doWork() 方法大約花費 14 毫秒,一共調用了 40 次 。每次 getView() 都有可能不止一次調用它,這就可以解釋爲什麼每次調用 getView() 需要花費這麼長時間。此方法讓 CPU 長時間處於忙碌狀態。再查看一下 CPU 獨佔 時間,我們看到它在整個記錄中佔用了 80% 的 CPU 時間!在追蹤記錄中排序 CPU 獨佔時間也是找到費時函數的最佳方法,因爲很有可能就是它們造成了你所遇到的性能問題。


追蹤對時間敏感的方法,比如 getView()、View#onDraw()和其它的方法,會幫助我們找到應用程序變慢的原因。但有時候還會有其他東西讓 CPU 很忙,佔用了寶貴的 CPU 週期,而這些原本可以用於繪製 UI 讓應用更加流暢。垃圾收集器偶爾會運行清除不再使用的對象,它通常不會對運行在前臺的應用程序造成很大的影響。但如果 GC 執行得過於頻繁,就會讓應用程序變慢,這可能讓我們受到指責……


內存性能分析


Android Studio 最近改進了很多,有越來越多的工具可以幫助我們找出和分析性能問題。Android 窗口中的內存頁告訴我們,隨着時間的推移有多少數據在棧上分配。它看上去像這樣:



我們在圖中看到一個小的下降,這裏發生了一次 GC 事件 ,移除了堆上不需要的對象和釋放了空間。


圖中的左邊有兩個工具可用:堆轉儲和分配跟蹤器。


堆轉儲


爲了調查堆上分配了什麼,我們可以使用左邊的堆轉儲按鈕。這將對當前堆上分配的東西進行快照,在 Android Studio中作爲一個單獨報告呈現在屏幕上:



我們在左邊看到堆上實例的一張柱狀圖,按照它們的類名字進行分組。每一個實例都有分配對象的數量,實例的大小(淺尺寸)和保留在內存中的對象大小。後者告訴我們,如果這些實例被釋放,可以釋放多少內存。這個視圖非常重要,它讓我們看到應用程序中內存佔用的情況,幫助我們確認大型數據結構和對象關係。這些信息幫助我們構建更多高效的數據結構,解開對象連接以減少保留的內存,並最終儘可能地減少佔用的內存。


查看柱狀圖,我們看到 MemoryActivity 有 39 個實例,對一個 Activity 而言這顯得很奇怪。我們在右邊選擇其中一個實例,底部的引用樹裏會顯示這個實例所有的引用。




其中一個是 ListenersManager 對象中數組的一部分。查看這個Activity 的其它實例,顯示出它們都被這個對象保留下來。這就解釋了爲什麼只有這個類的對象會佔用這麼多內存。



這種情況就是衆所周知的“內存泄漏”。因爲這些 Activity 被徹底銷燬後,由於引用的關係,這些無用的內存不能作爲垃圾被收集掉。避免這種情況的方法,就是確保對象不被比其它生命週期更長的其它對象引用。這種情況下,ListenManager 不應該在這個 Activity 被銷燬後還保留這個引用。一種解決方法就是在 onDestory() 回調函數中,在這個Activity 被銷燬時刪除這個引用。


內存泄漏和其它在堆中佔用大量空間的大型對象,會減少可用內存並頻繁觸發GC 事件嘗試釋放更多的空間。這些 GC 事件會讓 CPU 很忙,結果降低了應用程序的性能。如果對應用程序而言沒有足夠數量的可用內存,而且堆也不能再增長,就會產生一個更爲嚴重的後果 ——OutofMemoryException,會導致應用程序崩潰。


Eclipse 內存分析工具(Elicpse MAT)是一個更高級的工具:



這個工具可以做到 Android Studio 能做到的所有功能,還可以識別可能出現的內存泄漏,而且提供了更高級的實例查找方法,比如查找所有大於 2 MB 的 Bitmap 實例或者所有 Rect 空對象。


LeakCanary 函數庫也是一個很好的工具,它可以追蹤對象並確保它們不會泄漏。如果內存泄露了 —— 你將收到一個通知告訴你在哪裏發生了什麼。




分配跟蹤器


在內存圖中,可以通過左邊的其它按鈕來啓動或停止分配跟蹤器。它會生成當時所有被分配實例的報告,可以按照類分組:



或者按照方法分組:



它有很好的可視化效果,告訴我們最大的分配實例是什麼。


通過這個信息,我們可以找到佔用大量內存且對時序要求嚴格的方法。它可能會頻繁觸發 GC 事件。我們還可以找到大量生命週期很短的同一類型實例,這樣可以考慮使用一個對象池來減少分配的數量。


常用的內存技巧


這裏有一些我在編寫代碼時常用的小技巧和準則:


  • 枚舉是性能討論中的熱門主題。這裏有一個相關視頻,告訴我們枚舉類型的大小,還有一個針對這個視頻和其中一些誤導的討論。枚舉是否會比常量更加佔用空間?當然會。這很糟嗎?未必。如果你正在編寫一個函數庫,需要強類型安全,使用這種方法會比其他方法好,比如@IntDef。如果你只是需要把一堆常量需要彙總起來 —— 這種情況下使用枚舉就不太明智。通常情況下,你在做決定的時候需要權衡利弊。

  • 自動裝箱 —— 自動裝箱會自動把原始類型轉換成它們的對象表示(比如int->Integer)。每當一個原始類型被“裝箱”成一個對象,就創建了一個新的對象(我知道這讓人很震驚)。如果我們有很多這樣的情況 —— GC 就會頻繁地運行。要留意到這些自動裝箱的數量並不容易,因爲每當一個原始類型給一個對象賦值時,這就自動發生了。嘗試保持這些類型的一致性是一個解決方法。如果你在應用程序中使用這些原始類型,避免不要讓它們無故被自動裝箱。你可以使用內存性能分析工具找到表示一個原始類型的衆多對象。你也可以使用Traceview 來查找 Interger.valueOf()、Long.valueOf()等。

  • HashMap 與 ArrayMap 或 Sparse*Array 比較 —— HashMap 要求使用對象作爲鍵值,就和自動裝箱問題有關 。如果在應用程序中使用了原始的“int” 類型,它在和 HashMap 交互時會被自動裝箱成 Interger,這種情況下其實只要使用 SparseIntArray 就好了。如果只是想用對象作爲鍵值,可以使用 ArrayMap 類型。它和 HashMap 非常類似,但是底層的工作機理完全不同。它會更高效地使用內存,代價是速度比較慢。同 HashMap 相比上面兩種替代方案佔用的內存都比較小,但是花在檢索項目和分配空間上的時間會比 HashMap 多一些。除非有 1000 個以上的項目,它們在執行時間上幾乎沒有什麼差別,它們是你實現映射的可行選擇。

  • 上下文感知 —— 像前面看到的,Activity 中更有可能發生內存泄漏。Activity 是 Android 中最常見的內存泄漏(!),對此你可能並不會感到意外。它們也是非常昂貴的泄漏,因爲它們裏面包括了 UI 中所有的視圖層級,這佔用了很多的空間。平臺上的很多操作都需要一個 Context 對象,通常用一個 Activity 來傳遞這些信息。要確保你理解了那個 Activity 上發生了什麼。如果一個指向它的引用被緩存了,而且這個對象要比 Activity 生存時間長,若不清除這個引用,就會造成一個內存泄漏。

  • 避免非靜態內部類 —— 當你創建並實例化了一個非靜態內部類,你就建造了一個指向外部類型的隱含引用。如果這個內部類的實例比外部類型存活的時間還要長,那即使不需要這個外部類型,它還是會被保存在內存中。例如,在Activity 類中創建了一個擴展 AsynTask 的非靜態類型,開始處理異步任務,在運行過程中殺掉這個 Activity。只要這個異步任務運行時,它就會讓這個 Activity 一直活着。解決方案 —— 請不要這樣做,如果實在需要的話,就聲明一個靜態內部類。


GPU 性能分析


Android Studio 1.4 增加了一個新工具,可以對 GPU 渲染進行性能分析。


在 Android 窗口下進入 GPU 頁面,你會看到一張圖表,上面顯示了繪製屏幕上每一幀所花費的時間:



圖中的每一條線代表被繪製的一幀,不同顏色表示處理過程中的不同階段:


  • 繪圖(藍色)—— 代表 View#onDraw() 方法。這部分創建和更新了 DisplayList 對象,這些對象後續會被轉換成 GPU 可以理解的 OpenGL 命令。比較高的值是由於複雜視圖需要更多的時間來創建顯示列表,或者有很多視圖在很短的時間內失效了。

  • 準備(紫色)—— 在 Lollipop (譯者注:Android 的一個版本,也被簡稱爲 Android L) 中,增加了另外一個線程來讓 UI Thread 可以更快地繪製 UI。這個線程被稱爲 RenderThread。它負責將顯示列表轉換成 OpenGL 命令再發給 GPU。當處理這些的時候,UI 線程可以開始處理下一幀。這個步驟 UI 線程需要花時間把相關資源傳遞給 RenderThread。如果有很多資源需要傳遞,例如很多和大量的顯示列表,這個步驟就會比較耗時。

  • 處理(紅色)—— 執行顯示列表來創建 OpenGL 命令。如果需要執行很多和複雜的顯示列表,這個步驟會花費較長的時間,因爲有很多視圖需要被重新繪製。當這個視圖失效了,或者它被暴露在移動的重疊視圖下,它都要被重繪。

  • 執行(黃色)—— 發送 OpenGL 命令給 GPU。這部分是一個阻塞調用,因爲 CPU 發送一個包含命令的緩存給 GPU,預期 GPU 返回一個乾淨的緩存用來處理下一幀。這些緩存的數量是有限的,如果 GPU 很忙—— CPU 會發現它要等待一個緩存被釋放掉。因此如果我們看到在這個步驟看到較高的值,很可能說明 GPU 在忙着繪製 UI,這個 UI 太複雜很難在短時間完成。


在 Marshmallow中(譯者注:Android 的一個版本,也被簡稱爲 Android M),增加了更多顏色可以代表更多步驟,比如量測和佈局,輸入處理和其它的功能:



編輯於2015/09/29:一位來自 Google 的框架工程師,John Reck,增加了新顏色的相關信息:


“動畫” 的確切定義是指每一個向 Choreographer 註冊爲 CALLBACK_ANIMATION 的東西。包含 Choreographer#postFrameCallback and View#postOnAnimation,它們被用在 view.animate()、ObjectAnimator、Transition等等上面,它和 systrace 的“動畫”標籤是同一個東西。


“misc”是指 vsync 和當前時間標籤的延遲。如果你曾經從 Choreographer 的日誌中看到過類似“錯過 vsync 多少多少毫秒跳過了多少多少幀” 的信息,這些現在都被標記爲“misc”。在統計幀的轉儲中 INTENDED_VSYNC 和 VSYNC 是不同的。(https://developer.android.com/preview/testing/performance.html#timing-info)


但使用這個功能前,你需要先在開發者選項中打開 GPU 渲染這個選項:



這個工具被允許使用 ADB 命令以獲取它需要的所有信息,當然對我們也很有用!使用如下命令:


adb shell dumpsys gfxinfo <PACKAGE_NAME>


我們可以收到這些數據並創建下面這張圖表。這個命令還會打印其它有用的信息,比如層級中有多少視圖,整個顯示列表的大小等等。在 Marshmallow 中我們可以獲得更多的統計信息。



如果應用程序有相應的自動化 UI 測試,可以在某些交互操作後(列表滑動和大量的動畫等),在服務器上運行這個命令來觀察這些值是否會隨着時間而變化,比如“Janky Frames”。這會幫助我們在某些提交推入後定位一個性能下降的問題,讓我們有時間在應用程序面世前解決掉這個問題。使用“framestats”這個關鍵詞,我們可以獲得更加詳細的繪製信息,可以參考這裏。


但不是隻有觀察圖表這樣一種方式!


在“Profile GPU Rendering” 開發者選項中,還有一個“On Screen as bars”選項。打開這個選項後,屏幕上每個窗口都顯示圖表,上面有一個綠線代表 16 毫秒的門限值。




在右面的例子裏,我們看到有些幀超出了綠線,這說明繪製這些幀的時間超過了 16 毫秒。因爲這些線條中的大部分是藍色,我們認爲有很多或複雜的視圖需要繪製。在這個場景下,我滑動新聞供應列表,它裏面有不同類型的視圖。有些視圖已經失效,有些繪製時會更加複雜。有些幀超過門限值可能因爲是這個時間內正好有一個複雜的視圖要繪製。


層級觀察器


我愛死這個工具了,但讓我失望的是大部分人根本不使用它!


使用層級觀察器,我們可以得到性能的統計信息、觀察屏幕上完整的視圖層級和訪問所有這些視圖的屬性。單獨使用層級觀察器,你還可以轉儲主題的數據,觀察每一個樣式的屬性值,但 Android Monitor 上做不到這一點。我在進行佈局設計和優化時會使用這個工具。




在中間,我們看到一個代表視圖層級的樹。這個視圖層級可以很寬,但如果它太深(大概 10 個層級),在佈局和量測階段就會花費很多時間。每次用 View#onMeasure() 中測量一個視圖,或者在 View#onLayout() 中定位所有的子視圖,這些命令都會傳遞給子視圖,子視圖也會做同樣的事情。有些佈局的每個步驟會執行兩次,比如 RelativeLayout 和一些 LinearLayout 配置,如果它們是嵌套的,傳遞次數就會呈指數增加。


在底部右側,我們看到一個佈局的“設計圖”,上面顯示了每個視圖的位置。我們在這裏或者在上面的樹中選擇一個視圖,可以在左邊觀察它的屬性。設計佈局時,我有時候不確定爲什麼一個特定的視圖會在那裏結束。通過這個工具,我可以在樹中追蹤它,選擇它並觀察它在前面窗口的位置。通過查看視圖在屏幕上的最終尺寸,我可以設計有趣的動畫,還可以使用這些信息準確地移動東西。我可以找到那些被其它視圖無意覆蓋而看不到的視圖,以及更多信息。




對每一個視圖和它的子視圖,我們都有量測、佈局和繪製它們所花費的時間。顏色表明了這個視圖和其他視圖相比性能如何,很容易通過這個方式找到最薄弱的環節。因爲我們還看到這個視圖的預覽,可以仔細檢查這個樹並按照創建它的步驟,找到多餘的步驟並移除掉。這其中有一個東西對於性能會有很大的影響,那就是過度繪製(Overdraw)。


過度繪製


如同在 GPU 性能分析部分所看到的 —— 如果 GPU 有很多東西要畫到屏幕上,增加了繪製每一幀的時間,這樣圖表中黃色所代表的執行階段就要花費較長時間才能完成。當我們在其它東西的上面畫東西時,就會出現過度繪製,比如說一個紅色背景上的黃色按鍵。GPU 需要先繪製紅色背景然後在上面畫黃色按鍵,這樣過度繪製就不可避免了。如果有很多過度繪製的層,它會造成 GPU 很忙併且很難達成 16 毫秒的要求。



通過使用開發選項中的 “Debug GPU Overdraw”設定,所有過度繪製會變成不同的顏色來表明這個區域過度繪製的嚴重程度。有 1 倍或 2 倍的過度繪製還好,甚至有些小的淺紅色區域也不算太壞,但是如果我們在屏幕上看到很多紅色 —— 那可能就有麻煩了。讓我們看些例子:



左邊的例子裏,有一個被畫成綠色的列表,這通常還好,但是頂部有一個覆蓋把它變成紅色,這就開始有問題了。右邊的例子裏,整個列表都是淺紅色。這兩個例子中都有一個不透明列表,存在2倍或3倍的過度繪製。如果在 Activity 或者 Fragment 的窗口中有一個全屏的背景色,列表和其中每一個欄位的視圖都可能會出現過度繪製。我們可以通過只爲它們中的一個設置背景色來解決這個問題。


注意:默認主題爲窗口聲明瞭一個全屏的背景色。如果一個 Activity 上有一個不透明的佈局覆蓋了整個屏幕,去除這個窗口的背景就可以減少一層過度繪製。這可以在主題或代碼中通過在 onCreate() 中調用 getWindow().setBackgroundDrawable(null)實現。


使用層級觀察器,你可以將層級中的所有分層導出來,並生成一個可用 Photoshop 打開的 PSD 文件。在 Photeshop 中查看不同層級,就可以展示出佈局中所有的過度繪製。通過這些信息可以減少多餘的過度繪製,不要在綠色上止步不前,爭取做到藍色界面的效果!


Alpha


使用透明特性可能會有隱含的性能問題,想要理解爲什麼 —— 讓我們看一下給一個視圖設定 alpha 值會發生什麼。考慮下面這個佈局:



這個佈局中有三個 ImageView,一個疊在另一個上面。通過 setAlpha() 可以直接和簡單地設定一個 alpha值,這個命令會在傳遞到 ImageView的子視圖上。後面這些 ImageView 被設置成那個 alpha 值繪製到幀緩存上。結果就是:




這不是我們想看到的。


因爲每個 ImageView 都用一個 alpha 值繪製,所有重疊的圖像都混在一起。幸運的是,操作系統有方法解決這個問題。佈局會被複制到一個離屏緩存,這個 alpha 值會被應用到整個緩存上,然後再把它複製到幀緩存。結果是:



但是……我們還是付出了代價。


在把視圖繪製到幀緩存之前,先在離屏緩存上繪製這個視圖,實際上是增加了另一個未被發現的過度繪製分層。操作系統不確認什麼時侯使用這種方法,或者之前展示的直接方法,所以總是默認選擇複雜的那個。但是還是有些方法設置 alpha 值並避免加入複雜的離屏緩存。


  • 文本視圖(TextView)—— 用 setTextColor() 代替 setAlpha() 方法。文本顏色如果使用 alpha 通道,會導致直接用 alpha 繪製文本。

  • 圖像視圖(ImageView)—— 用 setImageAlpha() 代替 setAlpha() 方法。原因同文本視圖。

  • 自定義視圖 —— 如果你自定義的視圖不支持視圖重疊,這個複雜的行爲就和我們無關。那就沒有辦法,如上面例子所示,子視圖會混在一起。通過重載 hasOverlappingRendering() 方法並返回錯誤,我們可以通知系統對視圖採用直接和簡單的通道。通過重載 onSetAlpha() 方法並返回正確,我們就有一個選擇來手動處理設定一個 alpha 值會發生什麼。


硬件加速


硬件加速在 Honeycomb (譯者注:Android H版本)上被引入,我們有一個新的繪製模型用來在屏幕上呈現應用程序。DisplayList 這個數據結構被引入,它用來記錄視圖繪製的命令以便快速呈現。但是有另外一個很好的特性,開發者沒有留意或者沒有正確地使用 —— 就是視圖分層。


使用視圖分層,我們可以在一個離屏緩存上繪製視圖(之前看到過,應用在一個 Alpha 通道上)並且可以隨意處理。這個特性主要用於動畫,因爲我們可以快速地動畫繪製複雜的視圖。沒有分層,動畫繪製一個視圖需要在改變動畫屬性(比如,X 座標、縮放和 alpha 值等)後,讓這個視圖失效。對於複雜的視圖,這個失效會傳播到所有的子視圖,它們也會被重繪,這個操作的開銷很大。通過使用硬件支持的視圖分層,GPU 會創建視圖的一個紋理。有一些可以應用在紋理上的操作,不需要讓它失效,比如 X 和 Y座標位置、旋轉、alpha等等。這意味着我們可以在屏幕上動畫繪製一個複雜視圖而不用在過程中讓它失效!這使得動畫更加流暢。這裏有一段示例代碼:


// 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();


很簡單,對嗎?


當然,但是在使用硬件層時需要牢記一些事情:


  • 使用視圖後清理 —— 硬件層在內存有限的模組(GPU)上佔用了一定的空間。只在需要的時候去嘗試使用它們,好比動畫,使用完後再把它們清理掉。在上面的 ObjectAnimator例子中,我添加了一個監聽程序用來在動畫結束後移除分層。在 Property 動畫的例子中,我使用了 withLayers 方法,這個方法在開始時自動創建分層,動畫結束後就移除掉。

  • 如果你在採用一個硬件層改變視圖,這樣會讓硬件層失效並在離屏緩存上全部重繪這個視圖。當改變一個不能被硬件層優化的屬性時,就會發生這些(目前,如下這些是可以優化的:旋轉、縮放、X/Y轉換、旋轉運動和 alpha)。例如,你正在利用硬件層動畫繪製一個視圖,在屏幕上移動它的同時更新視圖的背景色,這會導致硬件層的持續更新。更新硬件層的開銷很大,這種情況下使用它划不來。


第二個問題是讓這些硬件層的更新變得可視化。使用開發者選項,我們可以打開 “Show hardware layers updates” 選項。



打開這個選項後,當視圖更新硬件層時,視圖會變成綠色閃一下。我之前曾經用過它,當時有一個 ViewPage 滑動得不如我期望得流暢。打開這個選項後,我繼續滑動 ViewPager,看到下面這些:



在整個滑動中兩個頁面都變綠了!


這說明這兩個頁面創建了一個硬件層,當滑動 ViewPager 時,它們都失效了。當滑動這些頁面時,我通過在背景上運用視差效果和逐漸動畫繪製頁面上的項目來更新頁面。我並沒有爲 ViewPager 頁面創建一個硬件層。閱讀 ViewPager 源代碼後,我發現當用戶開始滑動時,就爲兩個頁面創建了一個硬件層並在滑動停止後移除了這個分層。


它在滑動頁面時理所當然地創建了硬件層,我認爲這樣做很糟。通常當我們滑動 ViewPager 時 ,這些頁面不會改變,而且他們相當複雜 —— 硬件層可以很快地繪製它們。我開發的應用程序不是這樣情況,我不得不通過一些小技巧來移除這些硬件分層。


硬件層不是銀彈(譯者注:歐美古老傳說中使用銀子彈(silver bullet)可以殺死吸血鬼、狼人或怪獸;銀子彈引申爲解決問題的有效方法)。理解並正確地使用它們是相當重要的,否則你會陷入一個大麻煩。


自己動手


我在準備所有這些演示例子的時候 ,編寫了大量代碼來模擬這些情況。你可以在這個 Github 倉庫中和 Google Play 上找到所有這些。我把不同的場景拆分到不同的 Activity 上,並嘗試寫出文檔讓你們理解使用某個 Activity 會造成什麼問題。閱讀這些 Activity 的 Javadoc,打開工具並在應用程序上玩一玩吧。


更多信息


隨着 Android 操作系統的演進,你會有更多的方法來優化你的應用程序。Android SDK引入了新的工具,系統也加入了新的特性(比如硬件層)。你應該與時俱進,並在做改動前權衡利弊。


YouTube 上有一個很棒的播放列表,叫做 Android 性能模式,裏面有很多來自 Google 的小視頻,解釋了性能方面的不同主題。你可以找到不同數據結構的比較(HashMap 對比 ArrayMap)、位圖優化、甚至還有如何優化網絡請求。我強烈建議把它們都看一遍。


加入 Google+ 的 Android 性能模式社羣,和 Google 工程師在內的其他人討論性能問題,大家一起分享想法、文章和問題。


更多有趣的鏈接:


  • 瞭解 Android 圖像架構是如何工作的。這裏有你需要知道的一切,包含 Android 如何繪製 UI,解釋不同的系統組件,比如 SurfaceFlinger,以及它們之間是如何通信的。這篇很長,但是值得讀一下。

  • Google IO 2012 上的一個演講,展示了繪製模型是如何工作的,以及如何和爲什麼在繪製 UI 時會出現卡頓。

  • Devoxx 2013 上的一個 Android 性能主題演講,展示了在 Android 4.4 上對繪製模型的一些優化,並演示了優化性能的不同工具(Systrace、過度繪製等等)。

  • 這是一篇介紹預防性優化的好文章,裏面也說明了與過度優化的差異。很多開發者不去優化他們的代碼,因爲他們覺得這個影響沒什麼大不了的。請記住一件事情,那就是所有小問題加起來就是一個大問題。如果你有機會優化一小部分,看上去可能沒什麼,但不要排除這種可能性。

  • Android 上的內存管理 —— Google IO 2011 上的一個老視頻,但還是一定相關性。展示了 Android 上如何管理應用程序的內存,以及如何使用類似 Eclipse MAT 之類的工具來查找問題。

  • Google 工程師 Romain Guy 做的一個案例分析,即如何優化一個常見的 twitter 客戶端。這個案例中,Romain 告訴我們他如何找到應用程序的性能問題,以及建議如何修改。後續文章還介紹了這個程序優化後的其它問題。


我希望你現在已經有了足夠的信息,從今天開始,更加有信心開始優化你的應用程序!


就從打開記錄和一些相關的開發者選項開始吧。歡迎你在評論中,或在 Google+ 的Android 性能模式社羣上分享你發現的東西。

發佈了28 篇原創文章 · 獲贊 1 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章