Android App 優化之消除卡頓

anly_jun發表於2016-11-16

為了便於閱讀, 應邀將Android App效能優化系列, 轉移到掘金原創上來.
掘金的新出的"收藏集"功能可以用來做系列文集了.

這節我們就來聊聊App中的卡頓現象.

1, 感知卡頓

使用者對卡頓的感知, 主要來源於介面的重新整理. 而介面的效能主要是依賴於裝置的UI渲染效能. 如果我們的UI設計過於複雜, 或是實現不夠好, 裝置又不給力, 介面就會像卡住了一樣, 給使用者卡頓的感覺.

1.1 16ms原則

在剖析卡頓的原因之前, 我們先來了解下Android中著名的"16ms"原則:

Android系統每隔16ms會發出VSYNC訊號重繪我們的介面(Activity).
為什麼是16ms, 因為Android設定的重新整理率是60FPS(Frame Per Second), 也就是每秒60幀的重新整理率, 約合16ms重新整理一次.

就像是這樣的:

Android App 優化之消除卡頓

這就意味著, 我們需要在16ms內完成下一次要重新整理的介面的相關運算, 以便介面重新整理更新. 然而, 如果我們無法在16ms內完成此次運算會怎樣呢?

例如, 假設我們更新螢幕的背景圖片, 需要24ms來做這次運算. 當系統在第一個16ms時重新整理介面, 然而我們的運算還沒有結束, 無法繪出圖片. 當系統隔16ms再發一次VSYNC資訊重繪介面時, 使用者才會看到更新後的圖片. 也就是說使用者是32ms後看到了這次重新整理(注意, 並不是24ms). 這就是傳說中的丟幀(dropped frame):

Android App 優化之消除卡頓
dropped frame

丟幀給使用者的感覺就是卡頓, 而且如果運算過於複雜, 丟幀會更多, 導致介面常常處於停滯狀態, 卡到爆.

那麼會有哪些常見的情況會導致運算超過16ms, 進而丟幀, 讓使用者覺得卡頓呢?

2, 卡頓原因分析及處理

一般來說, 會有以下幾種情況導致卡頓這種效能問題, 我們逐一看下:

2.1 過於複雜的佈局

上節有說, 介面效能取決於UI渲染效能. 我們可以理解為UI渲染的整個過程是由CPU和GPU兩個部分協同完成的.

其中, CPU負責UI佈局元素的Measure, Layout, Draw等相關運算執行. GPU負責柵格化(rasterization), 將UI元素繪製到螢幕上.

如果我們的UI佈局層次太深, 或是自定義控制元件的onDraw中有複雜運算, CPU的相關運算就可能大於16ms, 導致卡頓.

這個時候, 我們需要藉助Hierarchy Viewer這個工具來幫我們分析佈局了. Hierarchy Viewer不僅可以以圖形化樹狀結構的形式展示出UI層級, 還對每個節點給出了三個小圓點, 以指示該元素Measure, Layout, Draw的耗時及效能.

具體請參考App優化之Layout怎麼擺.

2.2 過度繪製(Overdraw)

上節說的CPU方面的, 關於GPU的繪製, 如果我們的介面存在Overdraw, 也可能導致卡頓.

Overdraw: 用來描述一個畫素在螢幕上多少次被重繪在一幀上.
通俗的說: 理想情況下, 每屏每幀上, 每個畫素點應該只被繪製一次, 如果有多次繪製, 就是Overdraw, 過度繪製了.

2.2.1 除錯Overdraw

Android系統提供了視覺化的方案來讓我們很方便的檢視overdraw的現象:
在"系統設定"-->"開發者選項"-->"除錯GPU過度繪製"中開啟除錯:

Android App 優化之消除卡頓

此時介面可能會有五種顏色標識:

Android App 優化之消除卡頓

  • 原色: 沒有overdraw
  • 藍色: 1次overdraw
  • 綠色: 2次overdraw
  • 粉色: 3次overdraw
  • 紅色: 4次及4次以上的overdraw

一般來說, 藍色是可接受的, 是效能優的.

2.2.2 Overdraw的分析處理

上面有言, 所謂Overdraw, 就是在一個畫素點上繪製了多次. 常見的就是:

  1. 繪製了多重背景.
  2. 繪製了不可見的UI元素.

還是以GithubApp這個App的程式碼為例除錯, 開啟應用, 展示是這樣的:

Android App 優化之消除卡頓

可以看到是中間列表這塊overdraw比較嚴重. 檢視程式碼發現:

fragment_trending_container.xml中ViewPager設定了背景:

複製程式碼

而ViewPager中的fragment又設定了背景:




    ...
複製程式碼

完整程式碼請檢視Github上原始碼, 本文分析時commit截止到b01b5793.

刪除外層ViewPager的背景再看:

Android App 優化之消除卡頓

可以發現中間列表區域已經不再是紅色了, 但是也沒有達到藍色這個可以接受的層級. 這是因為我們的Activity預設情況下, theme會給window設定一個純色的背景. 因為我們這裡不想使用這個預設的背景,故而給layout加了一層背景, 導致了多重繪製背景.

當然我們也可以自定義主題, 將theme的window background設定成我們想要的, 而不在佈局中設定.

可以通過如下方式去掉window的背景.

設定主題:

@null複製程式碼

或是程式碼設定, 在onCreate中:

getWindow().setBackgroundDrawable(null);複製程式碼

此時我們看到的效果:

Android App 優化之消除卡頓

已基本達到優化水平.

以上旨在提供分析方法和思路.
Overdraw主要原因是背景的多重繪製, 或是不可見的View在背後繪製等, 但不僅限於此.

2.3 UI執行緒的複雜運算

上文ANR相關分析中就說到UI執行緒的複雜運算會造成UI無響應, 當然更多的是造成UI響應停滯, 卡頓.

產生ANR已經是卡頓的極致了, 具體分析可以參看App優化之ANR詳解一文.

關於運算阻塞導致的卡頓的分析, 可以使用Traceview這個工具.
具體Traceview的介紹, 以及實戰分析, 可以參考App優化之提升你的App啟動速度之理論基礎App優化之提升你的App啟動速度之例項挑戰.

在這裡需要提下我們在效能分析工具中提到的StrictMode.

2.3.1 StrictMode的使用

StrictMode用來基於執行緒或VM設定一些策略, 一旦檢測到策略違例, 控制檯將輸出一些警告,包含一個trace資訊展示你的應用在何處出現問題.

通常用來檢測主執行緒中的磁碟讀寫或網路訪問等耗時操作.

在Application或是Activity的onCreate中開啟StrictMode:

 public void onCreate() {
     if (BuildConfig.DEBUG) {
         // 針對執行緒的相關策略
         StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                 .detectDiskReads()
                 .detectDiskWrites()
                 .detectNetwork()   // or .detectAll() for all detectable problems
                 .penaltyLog()
                 .build());

         // 針對VM的相關策略
         StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                 .detectLeakedSqlLiteObjects()
                 .detectLeakedClosableObjects()
                 .penaltyLog()
                 .penaltyDeath()
                 .build());
     }
     super.onCreate();
 }複製程式碼

如果你的執行緒出了問題, 控制檯會有警告輸出, 可以定位到程式碼.
相對簡單, 在此就不多廢話了.
解決UI執行緒的耗時操作方案, 可以參考ANR詳解裡面說到的那些執行緒模式.

2.4 頻繁的GC

上面說的都是處理上的, CPU, GPU相關的. 實際上記憶體原因也可能會造成應用不流暢, 卡頓的.

說到這, 想起當年配桌上型電腦的三大件(CPU, 記憶體, 顯示器)了. 貌似分析App效能也是這幾大件啊 :)

為什麼說頻繁的GC會導致卡頓呢?
簡而言之, 就是執行GC操作的時候,任何執行緒的任何操作都會需要暫停,等待GC操作完成之後,其他操作才能夠繼續執行, 故而如果程式頻繁GC, 自然會導致介面卡頓.

以下內容參考自Android Performance Patterns:Memory Churn and Performance. 需翻牆

導致頻繁GC有兩個原因:

  • 記憶體抖動(Memory Churn), 即大量的物件被建立又在短時間內馬上被釋放.
  • 瞬間產生大量的物件會嚴重佔用Young Generation的記憶體區域, 當達到閥值, 剩餘空間不夠的時候, 也會觸發GC. 即使每次分配的物件需要佔用很少的記憶體,但是他們疊加在一起會增加Heap的壓力, 從而觸發更多的GC.

這些GC操作可能會造成上面說到的丟幀, 如下:

Android App 優化之消除卡頓

就會讓使用者感知到卡頓了.

一般來說瞬間大量產生物件一般是因為我們在程式碼的迴圈中new物件, 或是在onDraw中建立物件等. 所以說這些地方是我們尤其需要注意的...

關於記憶體的分析, 我們在後續的記憶體優化中細聊吧.

相關文章