Android APP 卡頓問題分析及解決方案
使用者對卡頓的感知, 主要來源於介面的重新整理. 而介面的效能主要是依賴於裝置的UI渲染效能. 如果我們的UI設計過於複雜, 或是實現不夠友好,計算繪製演算法不夠優化, 裝置又不給力, 介面就會像卡住了一樣, 給使用者卡頓的感覺.
如果你的應用介面出現卡頓不流暢的情況,不用懷疑,這很大原因是你沒有在16ms完成你的工作。沒錯,16ms要完成你的工作,再慢點,使用者就會感覺到卡頓,也許就會在螢幕對面開始吐槽你的APP,然後狠心把你辛辛苦苦開發出來的APP給解除安裝掉,打住,跑偏了!
1、16ms原則
Android 在不同的版本都會優化“UI的流暢性”問題,但是直到在android 4.1版本中做了有效的優化,這就是Project Butter。
Project Butter 加入了三個核心元素: VSYNC、Triple Buffer 和 Choreographer。其中,VSYNC
VSYNC:產生一箇中斷訊號
Triple Buffer:當雙 Buffer 不夠使用時,該系統可分配第三塊 Buffer
Choreographer:這個用來接受一個 VSYNC 訊號來統一協調UI更新
接下來我們就逐個去解析這3個核心元素:
在瞭解VSYNC之前,我們首先來了解一下我們在 xml 寫的一個佈局是如何載入到Acitivty/Fragment中並最終 display 呢?,我相信這個過程大部分程式猿也並不是很關心,因為 Android 底層都為為我們搞定這一部分的處理。但是如果要了解16ms原則,我們簡單瞭解下這個過程是非常有必要的。先看我簡單畫的一個圖:
從上面的圖可以看出,CPU 會先把 Layout 中的 UI 元件計算成 polygons(多邊形)和 textures(紋理),然後經過 OpenGL ES 處理(這個處理過程非常複雜,感興趣的童鞋可以繼續耕耘)。OpenGL ES處理完後再交給 GPU 進行柵格化渲染,渲染後 GPU 再將資料傳送給螢幕,由螢幕進行繪製顯示。
Activity 的介面之所以可以被繪製到螢幕上其中有一個很重要的過程就是 柵格化(Resterization),柵格化簡單來說就是將向量圖轉化為機器可以識別的點陣圖的一個過程。其中很複雜也比較很耗時,GPU 就是用來加快柵格化的速度。瞭解了這個過程後,我們在來理解 VSYNC
1、1 關於VSYNC
VSYNC 這個概念出來很久了,Vertical Synchronization,就是所謂的“垂直同步”。在 Android 中也沿用了這個概念,我們也可以把它理解為“幀同步”。這個用來幹嘛的呢,就是為了保證 CPU、GPU 生成幀的速度和 Display 重新整理的速度保持一致。
Android 系統每 16ms(更準確的是大概16.6ms) 就會發出一次 VSYNC訊號觸發 UI 渲染更新。大約螢幕一秒重新整理60次,也就是說要求 CPU 和 GPU 每秒要有處理 60 幀的能力,一幀花費的時間在 16ms 內。
這個方案的原理主要是通過 Choreographer 類設定它的 FrameCallback 函式,當每一幀被渲染時會觸發回撥 FrameCallback, FrameCallback 回撥 void doFrame (long frameTimeNanos) 函式。一次介面渲染會回撥 doFrame 方法,如果兩次 doFrame 之間的間隔大於 16.6ms 說明發生了卡頓。
如果你平時注意卡頓的日誌資訊,那麼下面這個段log就不會陌生了
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
SKIPPED_FRAME_WARNING_LIMIT 的預設值是 30,也就說當我們的程式卡頓大於 30 時會列印這條 log 資訊
那麼在 Android系統中,是如何利用 VSYNC 工作的呢?
一句話總結:在 VSYNC 開始發出訊號時,CPU和GPU已經就開始準備下一幀的資料了,趕在下個 VSYNC 訊號到來時,GPU 渲染完成,及時傳送資料給螢幕,Display 繪製顯示完成。不出什麼意外的話,每一幀都會這麼井然有序進行著,在這種理想狀態下,使用者就會體驗到如絲般順滑的感覺了。當然你也不會看到這篇部落格了,囧!
上面總結的一句話,如果用更專業的術語來說就是一個名詞,雙緩衝機制
1、2 雙緩衝機制
雙緩衝技術一直貫穿整個 Android 系統。因為實際上幀的資料就是儲存在兩個 Buffer 緩衝區中,A 緩衝用來顯示當前幀,那麼 B 緩衝就用來快取下一幀的資料,同理,B顯示時,A就用來緩衝!這樣就可以做到一邊顯示一邊處理下一幀的資料。
這樣看起來貌似沒什麼問題,一切都是我們的掌控中。但是,由於某些原因,比如我們應用程式碼上邏輯處理過於負責或者過於複雜的佈局,過度繪製(Overdraw),UI執行緒的複雜運算,頻繁的GC等,導致下一幀繪製的時間超過了16ms,那麼問題就來了,這時候使用者就不爽了,因為使用者很明顯感知到了卡頓的出現,也就是所謂的丟幀情況。如下圖所示:
ok,下面我們來認真分析一下為什麼會出現丟幀的情況:
1、當 Display 顯示第 0 幀資料時,此時 CPU 和 GPU 已經開始渲染第 1 幀畫面,並將資料快取在緩衝 B 中。但是由於某些原因,就好像上面說的,導致系統處理該幀資料耗時過長或者未能及時處理該幀資料。
2、當 VSYNC 訊號來時,Display 向 B 緩衝要資料,這時候 B 就藍瘦香菇了,因為緩衝 B 的資料還沒準備好,B緩衝區這時候是被鎖定的,Display 表示你沒準備好,我咋辦呢,無奈,只能繼續顯示之前緩衝 A 的那一幀,此時緩衝 A 的資料也不能被清空和交換資料。這種情況就是所謂的“丟幀”,也被稱作“廢幀”;當第 1 幀資料(即緩衝 B 資料)準備完成後,它並不會馬上被顯示,而是要等待下一個 VSYNC,Display 重新整理後,這時使用者才看到畫面的更新。
3、當某一處丟幀後,大概率會影響後面的繪製也出現丟幀,最走給使用者感覺就是卡頓了。最嚴重的直接造成ANR。
2、Triple Buffer
既然丟幀的情況不可避免,Android 團隊從未放棄對這塊的優化處理,於是便出現了Triple Buffer(三緩衝機制)
在三倍緩衝機制中,系統這個時候會建立一個緩衝 C,用來緩衝下一幀的資料。也就是說在顯示完緩衝B中那一幀後,下一幀就是顯示緩衝 C 中的了。這樣雖然還是不能避免會出現卡頓的情況,但是 Android 系統還是盡力去彌補這種缺陷,最終儘可能給用平滑的動效體驗。
3、卡頓處理
下面我們就以下幾種情況導致卡頓問題進行分析處理。
3.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 的耗時及效能。
2.2 過度繪製( Overdraw )
Overdraw: 用來描述一個畫素在螢幕上多少次被重繪在一幀上.
通俗的說: 理想情況下, 每屏每幀上, 每個畫素點應該只被繪製一次, 如果有多次繪製, 就是 Overdraw, 過度繪製了。 常見的就是:繪製了多重背景或者繪製了不可見的UI元素.
解決方案:
Android系統提供了視覺化的方案來讓我們很方便的檢視overdraw的現象:
在”系統設定”–>”開發者選項”–>”除錯GPU過度繪製”中開啟除錯:
此時介面可能會有五種顏色標識:
overdraw indicator
- 原色: 沒有overdraw
- 藍色: 1次overdraw
- 綠色: 2次overdraw
- 粉色: 3次overdraw
- 紅色: 4次及4次以上的overdraw
一般來說, 藍色是可接受的, 是效能優的.
2.3 UI 執行緒的複雜運算
UI執行緒的複雜運算會造成UI無響應, 當然更多的是造成UI響應停滯, 卡頓。產生ANR已經是卡頓的極致了。
解決方案:
關於運算阻塞導致的卡頓的分析, 可以使用 Traceview 這個工具。
2.4 頻繁的 GC
上面說的都是處理上的CPU, GPU 相關的. 實際上記憶體原因也可能會造成應用不流暢, 卡頓的。
為什麼說頻繁的 GC 會導致卡頓呢?
簡而言之, 就是執行 GC 操作的時候,任何執行緒的任何操作都會需要暫停,等待 GC 操作完成之後,其他操作才能夠繼續執行, 故而如果程式頻繁 GC, 自然會導致介面卡頓。
導致頻繁GC有兩個原因:
- 記憶體抖動(Memory Churn), 即大量的物件被建立又在短時間內馬上被釋放。
- 瞬間產生大量的物件會嚴重佔用 Young Generation 的記憶體區域, 當達到閥值, 剩餘空間不夠的時候, 也會觸發 GC。即使每次分配的物件需要佔用很少的記憶體,但是他們疊加在一起會增加 Heap 的壓力, 從而觸發更多的 GC。
解決方案:
一般來說瞬間大量產生物件一般是因為我們在程式碼的迴圈中 new 物件, 或是在 onDraw 中建立物件等。
還是是儘量不要在迴圈中大量的使用區域性變數。所以說這些地方是我們尤其需要注意的。