關於React Native專案在android上UI效能除錯實踐
我們盡最大的努力來爭取使UI元件的效能如絲般順滑,但有的時候這根本不可能做到。要知道,Android有超過一萬種不同型號的手機,而在框架底層進行軟體渲染的時候是統一處理的,這意味著你沒辦法像iOS那樣自由。不過有些時候,你還是可以想辦法提升應用的效能(有的時候問題根本不是出在原生程式碼上!)
要想解決應用的效能問題,第一步就是搞明白在每個16毫秒的幀中,時間都去哪兒了。為此,我們會使用一個標準的Android效能分析工具systrace
,不過在此之前……
請先確定JS的開發者模式已經關閉!
你應該在應用的日誌裡看到
__DEV__ === false, development-level warning are OFF, performance optimizations are ON
等字樣(你可以通過adb logcat來檢視應用日誌)
使用Systrace進行效能分析
Systrace是一個標準的基於標記的Android效能分析工具(如果你安裝了Android platform-tool包,它也會一同安裝)。被除錯的程式碼段在開始和結束處加上標記,在執行的過程中標記會被記錄,最後會以圖表形式展現統計結果。包括Android SDK自己和React Native框架都已經提供了標準的標記供你檢視。
收集一次資料
注意:
Systrace從React Native
v0.15
版本開始支援。你需要在此版本下構建專案才能收集相應的效能資料。
首先,把你想分析的、執行不流暢的裝置使用USB線連結到電腦上,然後操作應用來到你想分析的導航/動畫之前,接著這樣執行systrace:
$ <AndroidSDK所在目錄>/platform-tools/systrace/systrace.py --time=10 -o trace.html sched gfx view -a <你的應用包名>
對於此命令做一個簡單的說明:
time
引數控制本次資料收集的持續時間,單位是秒。schd
,gfx
, 和view
是我們所關心的Android SDK內建的tag(標記的集合):schd
提供了你的裝置的每個CPU核心正在做什麼的資訊,gfx
提供了你的圖形相關資訊,譬如每幀的時間範圍,而view
提供了一些關於檢視佈局和渲染相關效能的資訊。-a <你的應用包名>
啟用了針對應用的過濾。在這裡填寫你用React Native建立的應用包名。你的應用包名
AndroidManifest.xml
裡找到,形如com.example.app
譯註:實際上,AndroidManifest.xml裡的應用包名會被app/build.gradle
裡的applicationId
取代。如果二者不一致,應當以app/build.gradle
裡的為準。
一旦systrace開始收集資料,你可以操作應用執行你所關心的動畫和操作。在收集結束後,systrace會給你提供一個連結,你可以在瀏覽器中開啟這個連結來檢視資料收集的結果。
檢視效能資料
在瀏覽器中開啟資料頁面(建議使用Chrome),你應該能看到類似這樣的結果:
提示: 你可以使用WSAD鍵來滾動和縮放效能資料圖表。
啟用垂直同步高亮
接下來你首先應該啟用16毫秒幀區間的高亮。在螢幕頂端點選對應的複選框:
然後你應該能在螢幕上看到類似上圖的斑馬狀條紋。如果你無法看到這樣的條紋,可以嘗試換一臺裝置來進行分析:部分三星手機顯示垂直同步高亮存在已知問題,而Nexus系列大部分情況都相當可靠。
找到你的程序
滾動圖表直到你找到你的應用包名。在上面的例子裡,我正在分析com.facebook.adsmanager
,由於核心的執行緒名字長度限制,它會顯示成book.adsmanager
。
在左側,你應該能看到一系列執行緒對應著右邊的時間軸。有3到4個執行緒是我們必須關注的:UI執行緒(名字可能是UI Thread
或者是你的包名), mqt_js
和mqt_native_modules
。如果你在Android 5.0以上版本執行,我們還需要關注Render
(渲染)執行緒。
UI 執行緒
標準的Android佈局和繪製都在UI執行緒裡發生。右側顯示的執行緒名字會是你的包名(在我的例子裡是book.adsmanager)或者UI Thread.你在這個執行緒裡看到的事件可能會是一些Choreographer
, traversals
或者DispatchUI
:
JS執行緒
這是用於執行JavaScript程式碼的執行緒。根據Android系統版本或者裝置的不同,執行緒名可能是mqt_js
或者<...>
。如果看不到對應的名字的話,尋找類似JSCall
,Bridge.executeJSCall
這樣的事件。
原生模組執行緒
這裡是用於原生模組執行程式碼(譬如UIManager
)的執行緒,執行緒名可能是mqt_native_modules
或<...>
。在後一種情況下,尋找類似NativeCall
, CallJavaModuleMethod
, 還有onBatchComplete
這樣的事件名:
額外的:渲染執行緒
如果你在使用Android L(5.0)或者更高版本,你應該還會在你的應用裡看到一個渲染執行緒。這個執行緒真正生成OpenGL渲染序列來渲染你的UI。這個執行緒的名字可能為RenderThread
或者<...>
,在後一種情況下,尋找類似DrawFrame
或queueBuffer
這樣的事件:
尋找導致卡頓的罪魁禍首
一個流暢的動畫應該看起來像這樣:
每個背景顏色不同的部分我們稱作“一幀”——記住要渲染一個流暢的幀,我們所有的介面工作都需要在16毫秒內完成。注意沒有任何一個執行緒在靠近幀的邊界處工作。類似這樣的一個應用程式就正在60FPS(幀每秒)的情況下流暢表現。
如果你發現一些起伏的地方,譬如這樣:
注意在上圖中JS執行緒基本上一直在執行,並且超越了幀的邊界。這個應用就沒法以60FPS渲染了。在這種情況下,問題出在JS中。
你還有可能會看到一些類似這樣的東西:
在這種情況下,UI和渲染執行緒有一些重負荷的工作,以至於超越了幀的邊界。這可能是由於我們每幀試圖渲染的UI太多了導致的。在這種情況下,問題出在需要渲染的原生檢視上。
並且,你還應該能看到一些可以指導接下來優化工作的有用的資訊。
JS的問題
如果你發現問題出在JS上,在你正在執行的JS程式碼中尋找線索。在上面的圖中,我們會發現RCTEventEmitter
每幀被執行了很多次。這是上面的資料統計放大後的內容:
這看起來不是很正常,為什麼事件被呼叫的如此頻繁?它們是不同的事件嗎?具體的答案取決於你的產品的程式碼。在許多情況下,你可能需要看看shouldComponentUpdate的介紹。
TODO: 我們還在準備更多的JS效能分析的工具,會在將來的版本中加入。
原生UI問題
如果你發現問題出在原生UI上,有兩種常見的情況:
- 你每幀在渲染的UI給GPU帶來了太重的負載,或者:
- 你在動畫、互動的過程中不斷建立新的UI物件(譬如在scroll的過程中載入新的內容)
GPU負擔過重
在第一種情況下,你應該能看到UI執行緒的圖表類似這樣:
注意DrawFrame
花費了很多時間,超越了幀的邊界。這些時間用來等待GPU獲取它的操作快取。
要緩解這個問題,你應該:
- 檢查
renderToHardwareTextureAndroid
的使用,有這個屬性的View的子節點正在進行動畫或變形會導致效能大幅下降(譬如Navigator
提供的滑動、淡入淡出動畫)。 - 確保你沒有使用
needsOffscreenAlphaCompositing
,這個預設是關閉的,因為它在大部分情況下都會帶來GPU消耗的大幅提升。
如果這還不能幫你解決問題,你可能需要更深入的探索GPU到底在做什麼。參見Tracer for OpenGL ES。
在UI執行緒建立大量檢視
如果是第二種情況,你可能會看到類似這樣的結果:
注意一開始JS執行緒工作了很久,然後你看到原生模組執行緒幹了些事情,最後帶來了UI執行緒的巨大開銷。
這個問題並沒有什麼簡單直接的優化辦法,除非你能把建立UI的步驟推遲到互動結束以後去進行,或者你能直接簡化你所要建立的UI。React Native小組正在架構層設法提供一個方案,使得新的UI檢視可以在主執行緒之外去建立和配置,這樣就可以使得互動變得更加流暢。