哈嘍,我是老劉
前段時間有朋友諮詢我在樹莓派上開發的Flutter程序如何優化性能的問題。
老劉寫了6年多的Flutter代碼,樹莓派這種平台還真是頭一次碰到。
不過我聽他説完他們的場景,我就知道他們大概率是碰到性能問題了。
那麼今天就來説説這種極端場景下的性能優化如何進行。
場景描述
我們先來説明一下整個應用的運行場景。
首先,他的應用是運行在樹莓派硬件平台上的ubuntu系統中的。
我們知道樹莓派的處理器是arm系列處理器,因此其實本質上就是使用Flutter開發一個運行在arm架構cpu的ubuntu平台的應用。
其次,處理器的性能比較差。
樹莓派的設計目標不是pc端的應用場景,因此採用的cpu和gpu都是低功耗低性能的型號。
所以其主要特點就是處理器性能比較差。
其實gpu也差,但是這裏我們碰到的主要問題還是cpu的問題。
渲染原理
其實在這個場景中,如果是一些邏輯比較簡單的頁面,比如只是靜態的展示一些信息。
即使信息比較多,使用體驗也還能夠接受。
就是當碰到頁面中有大量圖表,而且圖表還有一定的動態交互的時候,就會有非常明顯的卡頓。
我們從Flutter的基本渲染原理來分析卡頓的原因。
其實不僅僅是Flutter,客户端上的非遊戲類App的渲染原理是類似的。
UI線程
對Flutter來説,當系統需要渲染一幀圖像,就會通知到Flutter框架。
框架就會調用當前頁面的build方法。
我們程序員通過Dart語言編寫的build方法會計算出當前頁面需要渲染的內容。
比如有幾個文本、幾個按鈕、幾個圖片,它們的位置在哪裏,我們可以把這些內容統稱為頁面佈局。
framework將頁面佈局和其它一些需要渲染的內容整理成layer tree。
上面的步驟都是在UI線程完成的,也就是Dart代碼完成的,全部依賴CPU運行。
GPU線程
Layer tree最終會交給引擎進行光柵化。
也就是在GPU線程中將layer tree的每一個layer中的佈局信息計算成像素信息。
這部分主要是由skia完成,雖然不是Dart代碼了,但是仍然由CPU完成。
GPU
最終Skia生成的像素信息會交給GPU,由GPU形成最終的展示數據並交給顯示器展示。
好了,基本的渲染原理講完了,接下來我們基於渲染原理分析一下卡頓的原因。
性能問題分析
從我們常見的非遊戲類APP來看,其實每一幀需要GPU繪製的內容差異都不是很大。
那為什麼有的頁面卡頓,有的頁面流暢呢?
其實主要就是需要CPU計算的這塊不同了。
更具體一點分析,如果一個靜態頁面的內容非常複雜,那麼交給skia的layer tree可能比一個動態頁面的layer tree還要複雜一些。
那為什麼卡頓總在動態頁面出現呢?
其實最主要的原因就出在我們寫的Dart代碼上。
換句話説,大多數常見的卡頓都是UI線程內的優化不到位引起的。
具體來説,當展示一個靜態頁面時。
第一幀的時候,我們程序員編寫的build方法需要大量計算,最終得到整個頁面的佈局。
所以第一幀會有些延遲。
後面的幀只需要簡單判斷一下頁面是否需要rebuild,發現不需要就沒有任何計算工作了。
所以UI線程的工作量很少,也就不會覺得卡頓。
當展示一個動態頁面時,頁面展示的內容會隨着狀態不停的變化。
也就是説每一幀都有可能需要rebuild,需要CPU重新計算頁面佈局。
回到文章開頭的例子,當展示一個包含大量圖表的動態頁面時。
每一幀都需要根據當前鼠標的位置,重新計算圖表的內容,比如鼠標指向的位置線條需要高亮等等。
這時如果CPU的運算能力比較差,例如樹莓派,就會導致每一幀都因為計算會有延遲,甚至會有丟幀等情況出現。
這就是動態頁面卡頓的根本原因。
知道了卡頓原因,接下來我們就可以分析一下優化方案了。
優化方案
1. 減少UI渲染的複雜性
分批加載數據:
數據和頁面元素的懶加載是老生常談了,也是大家都會用的常規手段。
但是之所以會成為常規手段,就是因為好用啊。
所以別看沒啥技術含量,一定先用起來。
簡化圖表元素:
儘量減少每個圖表中的元素數量(如線條、點、標籤等)。
特別是如果頁面中同時顯示多個圖表時,可以嘗試讓每個圖表只呈現關鍵數據。
對於低性能平台,從產品功能設計上減少頁面複雜性是一種取捨。
這是開發和產品的一種博弈,但是低性能平台的成本控制是一個好的切入點。
取消動畫:
圖表動畫雖然提升視覺效果,但也會消耗大量資源。如果發現卡頓嚴重,可以減少或禁用複雜動畫效果。
避免過度使用透明層:
透明層次的疊加會顯著增加GPU負載,儘量減少不必要的透明圖層或陰影效果。
2. 使用低級別的圖形API
繪製優化:
可以使用自定義繪製(例如CustomPainter)來直接繪製需要的圖形,這樣可以繞過一些性能瓶頸。
SDK提供的各種組件為了其通用性,在功能上是會有一點複雜的。
所以通過底層API來自定義繪製,在一定程度上能減少通用組件一些不必要的損耗。
當然,這是開發成本和運行效率間的一種取捨,也不能簡單的算是技術問題了。
考慮使用圖表庫的輕量級替代品:
如果你在使用某個第三方圖表庫,確認該庫是否支持較低性能的設備,或者選擇一些性能更高、適合嵌入式設備的輕量級圖表庫。
例如,可以考慮通過平台通道使用Raspberry Pi上更高效的圖表庫或繪圖工具,並通過與Flutter交互來顯示結果。
3. 分離計算和UI渲染
後台處理數據:
複雜的圖表通常涉及大量數據計算。
為了避免阻塞主線程的UI渲染,可以將數據計算放到後台(例如使用compute函數或Isolate),這樣主線程只負責圖表的渲染,數據處理則由獨立的線程完成。
其實在低性能平台上即使把計算任務放到後台仍然會有卡頓,因為真的可能是算不過來啊。
但是仍然需要把計算任務放到後台運行,這樣可以有效的避免主線程不響應。
也就是説雖然算不過來,會出現內容更新的不及時,但是至少不會整個UI沒有響應了。
4. 其它Flutter優化常用技巧
使用RepaintBoundary:
對於不需要頻繁刷新的圖表,可以將其包裹在RepaintBoundary中,這樣可以防止Flutter對這些部分的重複重繪,從而減少GPU的負擔。
減少重建組件的頻率:
避免不必要的setState調用,確保只有需要刷新的部分組件才會重新構建,儘量減少無關組件的重建。
比如使用Bloc、Provider這樣的狀態管理方案,儘可能減少需要重建的組件範圍。
避免不必要的ListView/ScrollView嵌套:
如果頁面中有大量數據需要滾動,儘量避免在一個大ListView中嵌套多個小ScrollView,這會增加渲染負擔。
總結
前面我們從渲染原理的角度分析了在樹莓派這種低性能平台上Flutter程序卡頓的原因。
然後提供了一些優化建議。
這些建議中大部分是相對通用的,比如懶加載、限制重繪範圍等,也有一些是針對低性能平台特定的,比如通過底層API自定義繪製、取消動畫等。
總的來説 Flutter 已經在各種大型項目中展示出足夠的支撐能力。
但是在樹莓派這樣的低性能平台上還有很大的優化空間。
當然這不僅僅是框架自身的問題,也有包括三方庫在內的生態系統需要共同完成的挑戰。
如果看到這裏的同學有學習Flutter的興趣,歡迎聯繫老劉,我們互相學習。
點擊免費領老劉整理的《Flutter開發手冊》,覆蓋90%應用開發場景。
可以作為Flutter學習的知識地圖。
覆蓋90%開發場景的《Flutter開發手冊》