總要說兩句
前段時間在開發項目時搞了一下過渡繪製的問題,一個比較複雜的界面剛開發出來,過渡繪製95%紅,調了半天后,基本只有5%的紅,效果不錯,準備找個時間總結一下,今天剛好看到網上以爲大神發了一篇文章,總結的不錯,順手牽過來啦,哈哈哈~
1、感知卡頓
用戶對卡頓的感知, 主要來源於界面的刷新. 而界面的性能主要是依賴於設備的UI渲染性能. 如果我們的UI設計過於複雜, 或是實現不夠好, 設備又不給力, 界面就會像卡住了一樣, 給用戶卡頓的感覺.
1.1 16ms原則
在剖析卡頓的原因之前, 我們先來了解下Android中著名的”16ms”原則:
- Android系統每隔16ms會發出VSYNC信號重繪我們的界面(Activity).
爲什麼是16ms, 因爲Android設定的刷新率是60FPS(Frame Per Second), 也就是每秒60幀的刷新率, 約合16ms刷新一次.
這就意味着, 我們需要在16ms內完成下一次要刷新的界面的相關運算, 以便界面刷新更新. 然而, 如果我們無法在16ms內完成此次運算會怎樣呢?
例如, 假設我們更新屏幕的背景圖片, 需要24ms來做這次運算. 當系統在第一個16ms時刷新界面, 然而我們的運算還沒有結束, 無法繪出圖片. 當系統隔16ms再發一次VSYNC信息重繪界面時, 用戶纔會看到更新後的圖片. 也就是說用戶是32ms後看到了這次刷新(注意, 並不是24ms). 這就是傳說中的丟幀(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的耗時及性能.
2.2 過度繪製(Overdraw)
上節說的CPU方面的, 關於GPU的繪製, 如果我們的界面存在Overdraw, 也可能導致卡頓.
Overdraw: 用來描述一個像素在屏幕上多少次被重繪在一幀上.
通俗的說: 理想情況下, 每屏每幀上, 每個像素點應該只被繪製一次, 如果有多次繪製, 就是Overdraw, 過度繪製了.
2.2.1 調試Overdraw
Android系統提供了可視化的方案來讓我們很方便的查看overdraw的現象:
在”系統設置”–>”開發者選項”–>”調試GPU過度繪製”中開啓調試:
此時界面可能會有五種顏色標識:
- 原色: 沒有overdraw
- 藍色: 1次overdraw
- 綠色: 2次overdraw
- 粉色: 3次overdraw
- 紅色: 4次及4次以上的overdraw
(一般來說, 藍色是可接受的, 是性能優的)
上面有言, 所謂Overdraw, 就是在一個像素點上繪製了多次. 常見的就是:
繪製了多重背景.
繪製了不可見的UI元素.
打開應用, 展示是這樣的:
可以看到是中間列表這塊overdraw比較嚴重. 查看代碼發現:
fragment_trending_container.xml中ViewPager設置了背景:
<android.support.v4.view.ViewPager
android:id="@+id/view_pager"
android:background="@color/md_white_1000"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
而ViewPager中的fragment又設置了背景:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/refresh_layout"
android:background="@color/md_white_1000"
android:layout_width="match_parent"
android:layout_height="match_parent">
...
</android.support.v4.widget.SwipeRefreshLayout
刪除外層ViewPager的背景再看:
可以發現中間列表區域已經不再是紅色了, 但是也沒有達到藍色這個可以接受的層級. 這是因爲我們的Activity默認情況下, theme會給window設置一個純色的背景. 因爲我們這裏不想使用這個默認的背景,故而給layout加了一層背景, 導致了多重繪製背景.
(當然我們也可以自定義主題, 將theme的window background設置成我們想要的, 而不在佈局中設置.)
可以通過如下方式去掉window的背景.
設置主題:
<item name="android:windowBackground">@null</item>
或是代碼設置, 在onCreate中:
getWindow().setBackgroundDrawable(null);
此時我們看到的效果:
已基本達到優化水平.
2.3 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();
}
2.4 頻繁的GC
上面說的都是處理上的, CPU, GPU相關的. 實際上內存原因也可能會造成應用不流暢, 卡頓的.
簡而言之, 就是執行GC操作的時候,任何線程的任何操作都會需要暫停,等待GC操作完成之後,其他操作才能夠繼續運行, 故而如果程序頻繁GC, 自然會導致界面卡頓.
導致頻繁GC有兩個原因:
- 內存抖動(Memory Churn), 即大量的對象被創建又在短時間內馬上被釋放.
- 瞬間產生大量的對象會嚴重佔用Young Generation的內存區域, 當達到閥值, 剩餘空間不夠的時候, 也會觸發GC. 即使每次分配的對象需要佔用很少的內存,但是他們疊加在一起會增加Heap的壓力, 從而觸發更多的GC.
這些GC操作可能會造成上面說到的丟幀, 如下:
一般來說瞬間大量產生對象一般是因爲我們在代碼的循環中new對象, 或是在onDraw中創建對象,或者是在ListView的getView中創建對象等. 所以說這些地方是我們尤其需要注意的…