Android Flutter實踐內存初探

摘要:Android Flutter實踐內存初探 閑魚技術-匠修我們想使用Flutter來統一移動App開發并做了一些實踐。移動設備上的資源有限,通常內存使用都是一個我們日常開發中十分關注的問題。那么,Flutter是如何使用內存,又會對Native App的內存帶來哪些影響呢?本文將簡單介紹Flutter內存機制,結合測試和我們的開發實踐,對日常關心的Bitmap內存使用,View繪制內存使用方面做一些探索。

閑魚技術-匠修

我們想使用Flutter來統一移動App開發并做了一些實踐。移動設備上的資源有限,通常內存使用都是一個我們日常開發中十分關注的問題。那么,Flutter是如何使用內存,又會對Native App的內存帶來哪些影響呢?本文將簡單介紹Flutter內存機制,結合測試和我們的開發實踐,對日常關心的Bitmap內存使用,View繪制內存使用方面做一些探索。

Dart RunTime簡介

Flutter Framework使用Dart語言開發,所以App進程中需要一個Dart運行環境(VM),和Android Art一樣,Flutter也對Dart源碼做了AOT編譯,直接將Dart源碼編譯成了本地字節碼,沒有了解釋執行的過程,提升執行性能。這里重點關注Dart VM內存分配(Allocate)和回收(GC)相關的部分。

和Java顯著不同的是Dart的"線程"(Isolate)是不共享內存的,各自的堆(Heap)和棧(Stack)都是隔離的,彼此之間通過消息通道來通信。Dart天然不存在數據競爭和變量狀態同步的問題,整個Flutter Framework Widget的渲染過程都運行在一個isolate中。

Dart VM將內存管理分為新生代(New Generation)和老年代(Old Generation)。

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

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

可以看到,Dart VM借鑒了很多JVM的思路,Dart中產生內存泄露的方式也和Java類似,Java中很多排查內存泄露的思路和防止內存泄露的編程方法應該也可以借鑒過來。

Image內存初探

對圖片的合理使用和優化是UI編程的重要部分,Flutter提供了Image Widget,我們可以方便的使用:

//使用本地圖片newImage.asset("images/xxxx.jpg");//使用網絡圖片newImage.network("https://xxxxxx");

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

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

打開Android Profiler,一張一張添加圖片,觀察內存數據。分別測試了Android的6.0,7.0和8.0系統,結果如下:

Android 6.0(Google Nextus5)

Android 7.0(Meizu pro5)

Android 8.0(Google pixel)

在測試中,隨著圖片一張張增加,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來實現:

//不做判斷,強制改為打包release版本的engineprivatestaticStringbuildModeFor(buildType) {? ? //if(buildType.name =="profile") {? ? //return"profile"http:// }elseif(buildType.debuggable) {? ? //return"debug"http:// }return"release"}

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

classStackImageStateextendsState{varimages = [];varindex =0;? @override? Widget build(BuildContext context) {varwidgets = [];for(int i =0; i <= index; i++) {varpos = i - (i ~/103) *103;? ? ? widgets.add(newContainer(? ? ? ? ? child:newImage.asset("images/${pos}.jpg", fit: BoxFit.cover),? ? ? ? ? padding:newEdgeInsets.only(top: i *2.0)));? ? }? ? widgets.add(newCenter(? ? ? ? child:newGestureDetector(? ? ? ? ? ? child:newContainer(? ? ? ? ? ? ? ? child:newText("添加圖片(${index})",? ? ? ? ? ? ? ? ? ? style:newTextStyle(color: Colors.red)),? ? ? ? ? ? ? ? color: Colors.green,? ? ? ? ? ? ? ? padding:constEdgeInsets.all(8.0)),? ? ? ? ? ? onTap: () {? ? ? ? ? ? ? setState(() {? ? ? ? ? ? ? ? index++;? ? ? ? ? ? ? });? ? ? ? ? ? })));returnnewStack(? ? ? ? children: widgets, alignment: AlignmentDirectional.topCenter);? }}

得到的結果是:

Android 6.0

Android 8.0

可以看到,Flutter Image使用的內存既不屬于Java虛擬機內存也不屬于Native內存,而是Graphics內存(在Meizu pro5設備上也不屬于Graphics,事實上Meizu pro5設備不能歸類Flutter Image所使用的內存),官方對Graphics內存的解釋是:

那么至少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。

不復用FlutterView

復用FlutterView

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

//attach到當前ActivitymNativeView.attachViewAndActivity(this, activity);

初始化FlutterView時必須傳入一個Activity,當其他Activity復用FlutterView時再調用該Attach方法即可。這里有個問題,就是FlutterView中必須保存一個Activity引用,這個一個內存泄露隱患,我們可以在FluterView detach時候將MainActivity傳入,因為通常整個App交互過程中MainActivity都是一直存在的,可以避免其他Activity泄露。

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

不復用FlutterView時,頁面增加時內存變化

復用FlutterView時,頁面增加時內存變化

不復用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,如圖:

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

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

finalWindowManager wm = mFakeActivity.getWindowManager();finalFrameLayout root =newFrameLayout(mFakeActivity);//一個像素足矣FrameLayout.LayoutParams params =newFrameLayout.LayoutParams(1,1);root.addView(flutterView,params);WindowManager.LayoutParams wlp =newWindowManager.LayoutParams();wlp.width =1;wlp.height =1;wlp.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;wlp.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;wm.addView(root,wlp);finalFlutterView.FirstFrameListener[] listenerRef =newFlutterView.FirstFrameListener[1];? ? listenerRef[0] =newFlutterView.FirstFrameListener() {@OverridepublicvoidonFirstFrame(){//首幀渲染完后取消窗口wm.removeView(root);? ? ? ? ? flutterView.removeFirstFrameListener(listenerRef[0]);? ? ? }? ? ? };flutterView.addFirstFrameListener(listenerRef[0]);String appBundlePath = FlutterMain.findAppBundlePath(mFakeActivity.getApplicationContext());flutterView.runFromBundle(appBundlePath,null,"main",true);

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

簡歷投遞:guicai.gxy@alibaba-inc.com

原文鏈接

原文鏈接

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,610評論 25 708
  • 導讀:閑魚技術團隊一直在探索如何使用Flutter來統一移動App開發。移動設備上的資源有限,內存使用成了日常開發...
    蓋世英雄_ix4n04閱讀 588評論 0 4
  • 流年帶走了青春,徒留回憶,拒絕懷舊,因為太過傷感。但我生來就是一個喜歡懷舊的人,不論心中花田的過往是繁華亦...
    木語MH閱讀 1,165評論 3 11
  • 一座小城,一對夫妻,一雙兒女,三個小家。 對,沒錯,我今天要講的就是一個大家的故事。 這個大家里,三代同堂,只是同...
    十八般文藝人閱讀 320評論 0 0
  • 兩天的日立培訓結束了,也拿到了合格證書。收獲很大,日企中,日立還算是比較厚道的。昨天,讀了關于暖通的資料,寫了一篇...
    國慶_b919閱讀 157評論 0 0