天天看點

Android Flutter 記憶體機制初探

Dart RunTime簡介

Flutter Framework使用Dart語言開發,是以App程序中需要一個Dart運作環境(VM),和Android Art一樣,Flutter也對Dart源碼做了AOT編譯,直接将Dart源碼編譯成了本地位元組碼,沒有了解釋執行的過程,提升執行性能。這裡重點關注Dart VM記憶體配置設定(Allocate)和回收(GC)相關的部分。

和Java顯著不同的是Dart的"線程"(Isolate)是不共享記憶體的,各自的堆(Heap)和棧(Stack)都是隔離的,并且是各自獨立GC的,彼此之間通過消息通道來通信。Dart天然不存在資料競争和變量狀态同步的問題,整個Flutter Framework Widget的渲染過程都運作在一個isolate中。

Android Flutter 記憶體機制初探

Dart VM将記憶體管理分為新生代(New Generation)和老年代(Old Generation)。

Android Flutter 記憶體機制初探

新生代(New Generation): 通常初次配置設定的對象都位于新生代中,該區域主要是存放記憶體較小并且生命周期較短的對象,比如局部變量。新生代會頻繁執行記憶體回收(GC),回收采用“複制-清除”算法,将記憶體分為兩塊(圖中的from 和 to),運作時每次隻使用其中的一塊(圖中的from),另一塊備用(圖中的to)。當發生GC時,将目前使用的記憶體塊中存活的對象拷貝到備用記憶體塊中,然後清除目前使用記憶體塊,最後,交換兩塊記憶體的角色。

Android Flutter 記憶體機制初探
Android Flutter 記憶體機制初探

老年代(Old Generation): 在新生代的GC中“幸存”下來的對象,它們會被轉移到老年代中。老年代存放生命力周期較長,記憶體較大的對象。老年代通常比新生代要大很多。老年代的GC回收采用“标記-清除”算法,分成标記和清除兩個階段。在标記階段會觸發停頓(stop the world),多線程并發的完成對垃圾對象的标記,降低标記階段耗時。在清理階段,由GC線程負責清理回收對象,和應用線程同時執行,不影響應用運作。

Android Flutter 記憶體機制初探

可以看到,Dart VM借鑒了很多JVM的思路,Dart中産生記憶體洩露的方式也和Java類似,Java中很多排查記憶體洩露的思路和防止記憶體洩露的程式設計方法應該也可以借鑒過來。

Image記憶體初探

對圖檔的合理使用和優化是UI程式設計的重要部分,Flutter提供了Image Widget,我們可以友善地使用:

Android Flutter 記憶體機制初探

我們知道Android将記憶體分為Java虛拟機記憶體和Native記憶體,各大廠商都對Java虛拟機記憶體有一個上限限制,到達上限就會觸發OOM異常,而對Native記憶體的使用沒有太嚴格的限制,現在的手機記憶體都很大,一般有較大的Native記憶體富餘。那麼Android中ImageView使用的是Java虛拟機記憶體還是Native記憶體呢?

我們可以來做一個測試:在一個界面上,每點選一次,就在上面堆加一張圖檔。為了防止後面的圖檔完全覆寫前面的圖檔而出現優化的情況,每次都縮小幾個像素,這樣就不會出現完全覆寫。

Android Flutter 記憶體機制初探

打開Android Profiler,一張一張添加圖檔,觀察記憶體資料。分别測試了Android的6.0,7.0和8.0系統,結果如下:

Android Flutter 記憶體機制初探

在測試中,随着圖檔一張張增加,Android 6.0 和 7.0都是Java部分的記憶體在增長,而Android 8.0則是Native部分的記憶體在增長。由此有結論,Android原生的ImageView在6.0和7.0版本中使用的Java虛拟機記憶體,而在Android 8.0中則使用的Native記憶體。

而Flutter Image Widget使用的是哪部分記憶體呢?我們用Flutter界面來做相同的測試。Flutter Engine的Debug版本和Release版本存在很大的性能差異,是以我們測試最好使用Release版本,但是,Release版本的Apk又不能使用Android profiler來觀察記憶體,是以我們需要在Debug版本的Apk中打包一個Release版本的Flutter Engine, 可以修改flutter tool中的flutter.gradle來實作:

Android Flutter 記憶體機制初探

相同地,我們向Flutter界面中添加圖檔并用Android Profiler來觀察記憶體,測試使用的dart代碼:

Android Flutter 記憶體機制初探

得到的結果是:

Android Flutter 記憶體機制初探

可以看到,Flutter Image使用的記憶體既不屬于Java虛拟機記憶體也不屬于Native記憶體,而是Graphics記憶體(在Meizu pro5裝置上也不屬于Graphics,事實上Meizu pro5裝置不能歸類Flutter Image所使用的記憶體),官方對Graphics記憶體的解釋是:

Android Flutter 記憶體機制初探

那麼至少Flutter Image所使用的記憶體不會是Java虛拟機記憶體,這對不少Android裝置都是一個好消息,這意味着使用Flutter Image沒有OOM的風險,能夠較好的利用Native記憶體。

使用Image的時候,建立一個記憶體緩存池是個好習慣,Flutter Framework提供了一個ImageCache來緩存加載的圖檔,但它不同于Android Lru Cache,不能精确的使用記憶體大小來設定緩存池容量,而是隻能粗略的指定最大緩存圖檔張數。

FlutterView記憶體初探

Flutter設計之初是想統一Android和IOS的界面程式設計,是以理想的基于Flutter的apk隻需要提供一個MainActivity做入口即可,後面所有的頁面跳轉都在FlutterView中管理。但是,如果是一個已有規模的app接入Flutter開發,我們不可能将已有的Activity頁面都用Flutter重新實作一遍,這時候就需要考慮本地頁面和Flutter頁面之間的跳轉互動了。iOS可以友善的管理頁面棧,但是Android就很複雜(Android有任務棧機制,低記憶體Activity回收機制等),是以通常我們還是使用Activity作為頁面容器來展示flutter頁面。這時有兩種選擇,可以每次啟動一個Activity就啟動一個新的FlutterView,也可以啟動Activity的時候複用已有的FlutterView。

Android Flutter 記憶體機制初探

Flutter Framework中FlutterView是綁定Activity使用的,要複用FlutterView就必須能夠把FlutterView單獨拎出來使用。所幸現在FlutterView和Activity耦合程度并不很深,最關鍵的地方是FlutterNativeView必須attach一個Activity:

Android Flutter 記憶體機制初探

初始化FlutterView時必須傳入一個Activity,當其他Activity複用FlutterView時再調用該Attach方法即可。這裡有個問題,就是FlutterView中必須儲存一個Activity引用,這個一個記憶體洩露隐患,我們可以在FluterView detach時候将MainActivity傳入,因為通常整個App互動過程中MainActivity都是一直存在的,可以避免其他Activity洩露。

為了更好的權衡兩種方法的利弊,我們先用空頁面來測試一下當頁面增加時記憶體的變化:

Android Flutter 記憶體機制初探

不複用FlutterView時平均打開一個頁面(空頁面),Java記憶體增長0.02M,Native記憶體增長0.73M。複用FlutterView時平均打開一個頁面(空頁面),Java記憶體增長0.019M,Native記憶體增長0.65M。可見複用FlutterView在記憶體使用上是有優勢的,但主要複用的還是Native部分的記憶體。複用FlutterView必然帶來額外的一些複雜邏輯,有時候為了邏輯簡單,後期維護上的友善,犧牲一些相對不太珍貴的Native記憶體也是值得的。

複用單個FlutterView有時會有些“意外”,比如當Activity切換時,就不得不将目前FlutterView detach掉給後面建立的Activity使用,目前界面就會空白閃動,有個想法是可以将目前界面截屏下來遮擋住後面的界面變化,這種方式有時會帶來額外的适配問題。

FlutterView複用與否不是絕對的,有時候可以使用一些綜合性折中方案,比如,我們可以建立一個FlutterViewProvider,裡面維護N個可複用的FlutterView,如圖:

Android Flutter 記憶體機制初探

這樣的好處是,可以存在一定程度上的複用,又可以避免隻有一個FlutterView出現的一些尴尬問題。

FlutterView的首幀渲染耗時較高,在Debug版本有明顯感受,大概會黑屏2秒,release版本會好很多。但我們觀察Cpu曲線,發現還是一個較為耗時的過程。有一種體驗優化的思路是,我們可以預先讓将要使用的FlutterView加載好首幀,這樣,在真正使用的時候就很快了,可以先建立一個隻有1個像素的視窗,在這個視窗裡面完成FlutterView首幀渲染,代碼如下:

Android Flutter 記憶體機制初探

以上就是閑魚團隊在Flutter的應用過程中的一些實踐,希望有更多的新技術嘗試和技術挑戰的同學,請在下面留言告訴我們。

原文釋出時間為:2018-05-22

本文作者:匠修

本文來自雲栖社群合作夥伴“

阿裡技術

”,了解相關資訊可以關注“

”。

繼續閱讀