Android 卡頓優化 1 卡頓解析


1, 感知卡頓

用戶對卡頓的感知, 主要來源於界面的刷新. 而界面的性能主要是依賴於設備的UI渲染性能. 如果我們的UI設計過於復雜, 或是實現不夠好, 設備又不給力, 界面就會像卡住了一樣, 給用戶卡頓的感覺.

1.1 16ms原則

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

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

就像是這樣的:

16ms

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

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


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過度繪制"中開啟調試:

toggle GPU overdraw

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

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

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

2.2.2 Overdraw的分析處理

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

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

還是以GithubApp這個App的代碼為例調試, 打開應用, 展示是這樣的:

example-1

可以看到是中間列表這塊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 

完整代碼請查看Github上源碼, 本文分析時commit截止到b01b5793.

刪除外層ViewPager的背景再看:

example-2

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

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

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

設置主題:

<item name="android:windowBackground">@null</item> 

或是代碼設置, 在onCreate中:

getWindow().setBackgroundDrawable(null); 

此時我們看到的效果:

example-3

已基本達到優化水平.

以上旨在提供分析方法和思路.
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操作可能會造成上面說到的丟幀, 如下:

gc dropped frame

就會讓用戶感知到卡頓了.

一般來說瞬間大量產生對象一般是因為我們在代碼的循環中new對象, 或是在onDraw中創建對象等. 所以說這些地方是我們尤其需要注意的...


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM