內(nèi)存水位升高導(dǎo)致的穩(wěn)定性問題嚴(yán)重影響App用戶體驗(yàn),所以開發(fā)者們非常關(guān)注Flutter的內(nèi)存表現(xiàn)。隨著Flutter業(yè)務(wù)越來越多,閑魚也面臨著oom導(dǎo)致的crash率提升的問題,下面我們結(jié)合項(xiàng)目中實(shí)際遇到的內(nèi)存問題和解決思路跟大家分享下flutter內(nèi)存優(yōu)化的經(jīng)驗(yàn)。
本文分為三個(gè)部分:
-
了解Dart VM內(nèi)存分配及銷毀原理
-
通過Observatory工具分析內(nèi)存泄漏,減少不必要的內(nèi)存占用
-
Flutter中常見內(nèi)存泄漏場景有哪些,如何在業(yè)務(wù)應(yīng)用中避免踩坑
Dart VM內(nèi)存分配及銷毀原理
DartVM的垃圾回收機(jī)制分兩個(gè)階段,新生代(New Generation)和老年代(Old Generation)。
新生代用來存儲(chǔ)生命周期較短的對(duì)象,由兩個(gè)內(nèi)存空間組成,Active內(nèi)存空間用來分配新對(duì)象,inActive內(nèi)存空間用來作為備用空間,DartVM的內(nèi)存分配策略非常簡單,創(chuàng)建對(duì)象時(shí)只需要在現(xiàn)有堆上移動(dòng)指針,內(nèi)存增長始終是線形的,省去了查找可用內(nèi)存段的過程。每個(gè)Isolate有自己獨(dú)立的Heap,相互之間無法共享內(nèi)存,這樣可以實(shí)現(xiàn)無鎖的快速分配。
一旦Active的內(nèi)存空間被填滿,垃圾回收器會(huì)從根對(duì)象開始遍歷檢查檢查所有對(duì)象的引用狀態(tài),沒有被引用的對(duì)象標(biāo)記為dead狀態(tài),非dead狀態(tài)的對(duì)象在下次內(nèi)存回收事件中會(huì)被復(fù)制到inActive內(nèi)存空間,清除Active內(nèi)存空間,最后Active和inActive內(nèi)存空間狀態(tài)調(diào)換。

當(dāng)對(duì)象達(dá)到一定的生命周期,會(huì)被移到老年代內(nèi)存空間管理,這種垃圾收集策略有兩個(gè)階段
-
首先遍歷對(duì)象圖,并標(biāo)記仍在使用的對(duì)象。
-
掃描整個(gè)內(nèi)存,并回收任何未標(biāo)記的對(duì)象,然后清除所有標(biāo)志。
這種內(nèi)存清理的頻率較低,并且在掃描回收階段需要暫停Dart runtime,回收成本較高,比較適合Flutter中大量StatelessWidget的模式(大部分都存放在新生代)。

另外,當(dāng)engine檢測到應(yīng)用是idle狀態(tài)并且沒有用戶交互的時(shí)候會(huì)發(fā)送通知垃圾收集器開始清理內(nèi)存,最小化對(duì)性能的影響。這些策略讓Dart的內(nèi)存分配和回收都非常高效。
Android和IOS中都存在強(qiáng)引用弱引用的概念,區(qū)別在于一個(gè)對(duì)象具有強(qiáng)/弱引用,系統(tǒng)會(huì)不會(huì)釋放該對(duì)象占用的內(nèi)存空間,Dart并沒有弱引用的概念,但是有個(gè)特例Expando ,它會(huì)以弱引用的方式持有 key,相當(dāng)于一個(gè)弱應(yīng)用的map,感興趣的可以了解下。
Dart VM借鑒了很多JVM的思路,Dart中產(chǎn)生內(nèi)存泄露的方式也和JAVA類似,Java中很多排查內(nèi)存泄露的思路和防止內(nèi)存泄露的方法應(yīng)該也可以借鑒過來。Android可以通過Profile和LeakMemory等工具檢測app中的內(nèi)存泄漏,F(xiàn)lutter如何檢測呢?可以使用Observatory或者DevTools。
通過Observatory分析內(nèi)存泄漏
Observatory是官方提供的調(diào)試工具,通過dart vm獲取運(yùn)行時(shí)信息,通過它我們可以分析一系列性能相關(guān)數(shù)據(jù),例如app耗時(shí)統(tǒng)計(jì),代碼覆蓋率等,這里我們重點(diǎn)介紹內(nèi)存相關(guān)的調(diào)試工具。(DevTools也可以用來調(diào)試分析性能數(shù)據(jù),它是在Observatory層做了一層封裝,但是目前還是beta版本)。
下面我們用閑魚中的實(shí)際例子介紹下如何使用Observatory檢查看Dart VM內(nèi)存使用情況,注意所有關(guān)于性能的分析要在Profile模式下進(jìn)行。
-
打開Observatory URL的Web頁面。運(yùn)行app,在控制臺(tái)中查找類似輸出日志
listening on ws://127.0.0.1:64673/hXsWR_ZOsGk=/ws
, 表示當(dāng)前連接的VM地址,輸入到瀏覽器就可以看到Observatory主界面,顯示了dart vm一些基礎(chǔ)信息,具體使用方法可以參考 官方文檔,這里不再詳細(xì)描述,我們重點(diǎn)關(guān)注右下角的allocation profile選項(xiàng)。

-
點(diǎn)擊右下角allocation profile選項(xiàng)后,操作app進(jìn)入想要分析的Flutter頁操作,退出該頁面,點(diǎn)擊頁面右上角 GC按鈕觸發(fā)手動(dòng)GC,查看Class,發(fā)現(xiàn)有部分DX Class內(nèi)存占用,這類class本應(yīng)該只有在目標(biāo)分析頁會(huì)出現(xiàn),退出目標(biāo)分析也后手動(dòng)GC會(huì)被完全釋放,但是這里任然能看到相關(guān)內(nèi)存占用,說明產(chǎn)生了內(nèi)存泄漏。

-
點(diǎn)擊對(duì)應(yīng)class查看具體應(yīng)用實(shí)例,點(diǎn)擊對(duì)應(yīng)實(shí)例進(jìn)入查看引用路徑,就能找到?jīng)]有導(dǎo)致釋放的引用變量,結(jié)合業(yè)務(wù)代碼具體分析,就能發(fā)現(xiàn)泄漏的源頭。


這里有一點(diǎn)需要注意,Observatory顯示的Dart VM占用的內(nèi)存信息要遠(yuǎn)遠(yuǎn)小于Android Profile/Xcode檢測出的內(nèi)存大小,因?yàn)榇嬖诓糠种挥邢到y(tǒng)工具能檢測出的內(nèi)存區(qū)塊,例如一些完全不依賴于DartVM的skia對(duì)象,并且layer在engine中創(chuàng)建時(shí)并不能明確知道大小,所以采用虛擬近似值代替。
-
//engine/lib/ui/painting/engine_layer.cc
-
...
-
size_tEngineLayer::GetAllocationSize {
-
// Provide an approximation of the total memory impact of this object to the
-
// Dart GC. The ContainerLayer may hold references to a tree of other layers,
-
// which in turn may contain Skia objects.
-
return3000;
-
};
下面我們總結(jié)了幾種常見內(nèi)存泄漏的場景,在Java中都可以一一對(duì)應(yīng)找到類似的場景,大家在業(yè)務(wù)開發(fā)中注意避免。
常見內(nèi)存問題
-
未取消注冊(cè)或回調(diào)導(dǎo)致內(nèi)存泄露
示例代碼:
-
classDownloadManagerextendsObject{
-
......
-
abstractclassDownloadListener{
-
void completed(DXTemplateItem item);
-
void failed(DXTemplateItem item, String errorInfo);
-
}
-
staticList<DownloadListener> listenerList = List;
-
staticvoid downloadSingleTemplate(DXTemplateItemtemplate, DownloadListener listener) async{
-
listenerList.add(listener);
-
...
-
}
-
...
修改方法:手動(dòng)取消注冊(cè)或回調(diào)
-
// 移除
-
staticvoid removeDownloadListener(DownloadListener listener) {
-
if(listener != && listenerList != && listenerList.contains(listener)) {
-
listenerList.remove(listener);
-
}
-
}
-
資源未關(guān)閉或釋放導(dǎo)致內(nèi)存泄露,例如ImageStream的圖片資源有沒有被正常關(guān)閉導(dǎo)致的內(nèi)存泄漏。
問題代碼:
-
void _resolveImage {
-
finalImageStream newStream =
-
widget.image.resolve(createLocalImageConfiguration(context));
-
assert(newStream != );
-
_updateSourceStream(newStream);
-
}
修改方法:在圖片組件被銷毀時(shí)正確釋放資源
-
@override
-
void dispose {
-
...
-
_imageInfo.image.dispose;
-
_imageInfo = ;
-
super.dispose;
-
}
-
PlatformAssetBundle.loadString通過asset讀取String內(nèi)容會(huì)一直緩存讀取的內(nèi)容,造成內(nèi)存無法釋放
問題代碼:
-
/// 通過asset讀取Json
-
Future<Map<String, dynamic>> loadJsonAsset(String assetPath) async{
-
_rootBundle ??= PlatformAssetBundle;
-
finalString jsonStr = await _rootBundle.loadString(assetPath);
-
return json.decode(jsonStr);
-
}
修改方法:
-
/// 通過asset讀取Json
-
Future<Map<String, dynamic>> loadJsonAsset(String assetPath) async{
-
_rootBundle ??= PlatformAssetBundle;
-
finalString jsonStr = await _rootBundle.loadString(assetPath, cache: false);
-
return json.decode(jsonStr);
-
}
PlatformAssetBundle繼承于CachingAssetBundle,會(huì)在app整個(gè)生命周期中緩存讀取的數(shù)據(jù),如果不是需要頻繁訪問的話建議cache參數(shù)設(shè)置為false

-
另外很多同學(xué)有反饋過Flutter帶圖片的長列表滑動(dòng)會(huì)造成內(nèi)存一直上漲,flutter在1.17版本對(duì)這個(gè)問題做了優(yōu)化,具體改動(dòng)可以參考pr 14265。
總結(jié)
以上內(nèi)容介紹了閑魚在實(shí)踐中遇到的Flutter內(nèi)存問題解決思路,給出了內(nèi)存泄漏定位方法。優(yōu)化后能在一定程度上減小內(nèi)存壓力,避免不必要的內(nèi)存占用。閑魚在內(nèi)存優(yōu)化的方向上還有很多需要繼續(xù)探索的地方,正在做的包括對(duì)圖片庫的緩存改造,layer層內(nèi)存檢測工具等等,朝著不斷優(yōu)化flutter性能體驗(yàn)努力。