我在幾周前的 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 命令以獲取它需要的所有資訊,當然對我們也很有用!使用如下命令:
1 |
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等等。這意味著我們可以在螢幕上動畫繪製一個複雜檢視而不用在過程中讓它失效!這使得動畫更加流暢。這裡有一段示例程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 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 效能模式社群上分享你發現的東西。
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式