二、Flutter 內(nèi)存機(jī)制梳理

閑魚(yú)技術(shù)團(tuán)隊(duì)一直在探索如何使用Flutter來(lái)統(tǒng)一移動(dòng)App開(kāi)發(fā)斗锭。移動(dòng)設(shè)備上的資源有限地淀,內(nèi)存使用成了日常開(kāi)發(fā)中的常見(jiàn)問(wèn)題。那么岖是,F(xiàn)lutter是如何使用內(nèi)存帮毁,又會(huì)對(duì)Native App的內(nèi)存帶來(lái)哪些影響呢?本文將簡(jiǎn)單介紹Flutter內(nèi)存機(jī)制豺撑,結(jié)合測(cè)試和閑魚(yú)技術(shù)團(tuán)隊(duì)的開(kāi)發(fā)實(shí)踐作箍,對(duì)普遍關(guān)心的Bitmap內(nèi)存使用,View繪制內(nèi)存使用方面做一些探索前硫。

Dart RunTime簡(jiǎn)介

Flutter Framework使用Dart語(yǔ)言開(kāi)發(fā)胞得,所以App進(jìn)程中需要一個(gè)Dart運(yùn)行環(huán)境(VM),和Android Art一樣屹电,F(xiàn)lutter也對(duì)Dart源碼做了AOT編譯阶剑,直接將Dart源碼編譯成了本地字節(jié)碼,沒(méi)有了解釋執(zhí)行的過(guò)程危号,提升執(zhí)行性能牧愁。這里重點(diǎn)關(guān)注Dart VM內(nèi)存分配(Allocate)和回收(GC)相關(guān)的部分。

和Java顯著不同的是Dart的"線程"(Isolate)是不共享內(nèi)存的外莲,各自的堆(Heap)和棧(Stack)都是隔離的猪半,并且是各自獨(dú)立GC的兔朦,彼此之間通過(guò)消息通道來(lái)通信。Dart天然不存在數(shù)據(jù)競(jìng)爭(zhēng)和變量狀態(tài)同步的問(wèn)題磨确,整個(gè)Flutter Framework Widget的渲染過(guò)程都運(yùn)行在一個(gè)isolate中沽甥。

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

新生代(New Generation): 通常初次分配的對(duì)象都位于新生代中乏奥,該區(qū)域主要是存放內(nèi)存較小并且生命周期較短的對(duì)象摆舟,比如局部變量。新生代會(huì)頻繁執(zhí)行內(nèi)存回收(GC)邓了,回收采用“復(fù)制-清除”算法恨诱,將內(nèi)存分為兩塊(圖中的from 和 to),運(yùn)行時(shí)每次只使用其中的一塊(圖中的from)骗炉,另一塊備用(圖中的to)照宝。當(dāng)發(fā)生GC時(shí),將當(dāng)前使用的內(nèi)存塊中存活的對(duì)象拷貝到備用內(nèi)存塊中句葵,然后清除當(dāng)前使用內(nèi)存塊硫豆,最后,交換兩塊內(nèi)存的角色笼呆。

老年代(Old Generation): 在新生代的GC中“幸存”下來(lái)的對(duì)象熊响,它們會(huì)被轉(zhuǎn)移到老年代中。老年代存放生命力周期較長(zhǎng)诗赌,內(nèi)存較大的對(duì)象汗茄。老年代通常比新生代要大很多。老年代的GC回收采用“標(biāo)記-清除”算法铭若,分成標(biāo)記和清除兩個(gè)階段洪碳。在標(biāo)記階段會(huì)觸發(fā)停頓(stop the world),多線程并發(fā)的完成對(duì)垃圾對(duì)象的標(biāo)記叼屠,降低標(biāo)記階段耗時(shí)瞳腌。在清理階段,由GC線程負(fù)責(zé)清理回收對(duì)象镜雨,和應(yīng)用線程同時(shí)執(zhí)行嫂侍,不影響應(yīng)用運(yùn)行。

可以看到荚坞,Dart VM借鑒了很多JVM的思路挑宠,Dart中產(chǎn)生內(nèi)存泄露的方式也和Java類(lèi)似,Java中很多排查內(nèi)存泄露的思路和防止內(nèi)存泄露的編程方法應(yīng)該也可以借鑒過(guò)來(lái)颓影。

Image內(nèi)存初探

對(duì)圖片的合理使用和優(yōu)化是UI編程的重要部分各淀,F(xiàn)lutter提供了Image Widget,我們可以方便地使用:

我們知道Android將內(nèi)存分為Java虛擬機(jī)內(nèi)存和Native內(nèi)存诡挂,各大廠商都對(duì)Java虛擬機(jī)內(nèi)存有一個(gè)上限限制碎浇,到達(dá)上限就會(huì)觸發(fā)OOM異常临谱,而對(duì)Native內(nèi)存的使用沒(méi)有太嚴(yán)格的限制,現(xiàn)在的手機(jī)內(nèi)存都很大奴璃,一般有較大的Native內(nèi)存富余悉默。那么Android中ImageView使用的是Java虛擬機(jī)內(nèi)存還是Native內(nèi)存呢?

我們可以來(lái)做一個(gè)測(cè)試:在一個(gè)界面上溺健,每點(diǎn)擊一次麦牺,就在上面堆加一張圖片钮蛛。為了防止后面的圖片完全覆蓋前面的圖片而出現(xiàn)優(yōu)化的情況鞭缭,每次都縮小幾個(gè)像素,這樣就不會(huì)出現(xiàn)完全覆蓋魏颓。

打開(kāi)Android Profiler岭辣,一張一張?zhí)砑訄D片,觀察內(nèi)存數(shù)據(jù)甸饱。分別測(cè)試了Android的6.0沦童,7.0和8.0系統(tǒng),結(jié)果如下:

Android 6.0(Google Nextus5)

Android 7.0(Meizu pro5)

Android 8.0(Google pixel)

在測(cè)試中,隨著圖片一張張?jiān)黾犹净埃珹ndroid 6.0 和 7.0都是Java部分的內(nèi)存在增長(zhǎng)偷遗,而Android 8.0則是Native部分的內(nèi)存在增長(zhǎng)。由此有結(jié)論驼壶,Android原生的ImageView在6.0和7.0版本中使用的Java虛擬機(jī)內(nèi)存氏豌,而在Android 8.0中則使用的Native內(nèi)存。

而Flutter Image Widget使用的是哪部分內(nèi)存呢热凹?我們用Flutter界面來(lái)做相同的測(cè)試泵喘。Flutter Engine的Debug版本和Release版本存在很大的性能差異,所以我們測(cè)試最好使用Release版本般妙,但是纪铺,Release版本的Apk又不能使用Android profiler來(lái)觀察內(nèi)存,所以我們需要在Debug版本的Apk中打包一個(gè)Release版本的Flutter Engine, 可以修改flutter tool中的flutter.gradle來(lái)實(shí)現(xiàn):

相同地碟渺,我們向Flutter界面中添加圖片并用Android Profiler來(lái)觀察內(nèi)存,測(cè)試使用的dart代碼:

得到的結(jié)果是:

Android 6.0?

Android 8.0

可以看到鲜锚,F(xiàn)lutter Image使用的內(nèi)存既不屬于Java虛擬機(jī)內(nèi)存也不屬于Native內(nèi)存,而是Graphics內(nèi)存(在Meizu pro5設(shè)備上也不屬于Graphics,事實(shí)上Meizu pro5設(shè)備不能歸類(lèi)Flutter Image所使用的內(nèi)存)苫拍,官方對(duì)Graphics內(nèi)存的解釋是:

那么至少Flutter Image所使用的內(nèi)存不會(huì)是Java虛擬機(jī)內(nèi)存烹棉,這對(duì)不少Android設(shè)備都是一個(gè)好消息,這意味著使用Flutter Image沒(méi)有OOM的風(fēng)險(xiǎn)怯疤,能夠較好的利用Native內(nèi)存浆洗。

使用Image的時(shí)候,建立一個(gè)內(nèi)存緩存池是個(gè)好習(xí)慣集峦,F(xiàn)lutter Framework提供了一個(gè)ImageCache來(lái)緩存加載的圖片伏社,但它不同于Android Lru Cache抠刺,不能精確的使用內(nèi)存大小來(lái)設(shè)定緩存池容量,而是只能粗略的指定最大緩存圖片張數(shù)摘昌。

FlutterView內(nèi)存初探

Flutter設(shè)計(jì)之初是想統(tǒng)一Android和IOS的界面編程速妖,所以理想的基于Flutter的apk只需要提供一個(gè)MainActivity做入口即可,后面所有的頁(yè)面跳轉(zhuǎn)都在FlutterView中管理聪黎。但是罕容,如果是一個(gè)已有規(guī)模的app接入Flutter開(kāi)發(fā),我們不可能將已有的Activity頁(yè)面都用Flutter重新實(shí)現(xiàn)一遍稿饰,這時(shí)候就需要考慮本地頁(yè)面和Flutter頁(yè)面之間的跳轉(zhuǎn)交互了锦秒。iOS可以方便的管理頁(yè)面棧,但是Android就很復(fù)雜(Android有任務(wù)棧機(jī)制喉镰,低內(nèi)存Activity回收機(jī)制等)旅择,所以通常我們還是使用Activity作為頁(yè)面容器來(lái)展示flutter頁(yè)面。這時(shí)有兩種選擇侣姆,可以每次啟動(dòng)一個(gè)Activity就啟動(dòng)一個(gè)新的FlutterView生真,也可以啟動(dòng)Activity的時(shí)候復(fù)用已有的FlutterView。

不復(fù)用FlutterView

復(fù)用FlutterView

Flutter Framework中FlutterView是綁定Activity使用的捺宗,要復(fù)用FlutterView就必須能夠把FlutterView單獨(dú)拎出來(lái)使用柱蟀。所幸現(xiàn)在FlutterView和Activity耦合程度并不很深,最關(guān)鍵的地方是FlutterNativeView必須attach一個(gè)Activity:

初始化FlutterView時(shí)必須傳入一個(gè)Activity蚜厉,當(dāng)其他Activity復(fù)用FlutterView時(shí)再調(diào)用該Attach方法即可长已。這里有個(gè)問(wèn)題,就是FlutterView中必須保存一個(gè)Activity引用弯囊,這個(gè)一個(gè)內(nèi)存泄露隱患痰哨,我們可以在FluterView detach時(shí)候?qū)ainActivity傳入,因?yàn)橥ǔU麄€(gè)App交互過(guò)程中MainActivity都是一直存在的匾嘱,可以避免其他Activity泄露斤斧。

為了更好的權(quán)衡兩種方法的利弊,我們先用空頁(yè)面來(lái)測(cè)試一下當(dāng)頁(yè)面增加時(shí)內(nèi)存的變化:

不復(fù)用FlutterView時(shí)霎烙,頁(yè)面增加時(shí)內(nèi)存變化

復(fù)用FlutterView時(shí)撬讽,頁(yè)面增加時(shí)內(nèi)存變化

不復(fù)用FlutterView時(shí)平均打開(kāi)一個(gè)頁(yè)面(空頁(yè)面),Java內(nèi)存增長(zhǎng)0.02M,Native內(nèi)存增長(zhǎng)0.73M悬垃。復(fù)用FlutterView時(shí)平均打開(kāi)一個(gè)頁(yè)面(空頁(yè)面),Java內(nèi)存增長(zhǎng)0.019M,Native內(nèi)存增長(zhǎng)0.65M游昼。可見(jiàn)復(fù)用FlutterView在內(nèi)存使用上是有優(yōu)勢(shì)的尝蠕,但主要復(fù)用的還是Native部分的內(nèi)存烘豌。復(fù)用FlutterView必然帶來(lái)額外的一些復(fù)雜邏輯,有時(shí)候?yàn)榱诉壿嫼?jiǎn)單看彼,后期維護(hù)上的方便廊佩,犧牲一些相對(duì)不太珍貴的Native內(nèi)存也是值得的囚聚。

復(fù)用單個(gè)FlutterView有時(shí)會(huì)有些“意外”,比如當(dāng)Activity切換時(shí)标锄,就不得不將當(dāng)前FlutterView detach掉給后面新建的Activity使用顽铸,當(dāng)前界面就會(huì)空白閃動(dòng),有個(gè)想法是可以將當(dāng)前界面截屏下來(lái)遮擋住后面的界面變化料皇,這種方式有時(shí)會(huì)帶來(lái)額外的適配問(wèn)題谓松。

FlutterView復(fù)用與否不是絕對(duì)的,有時(shí)候可以使用一些綜合性折中方案践剂,比如鬼譬,我們可以建立一個(gè)FlutterViewProvider,里面維護(hù)N個(gè)可復(fù)用的FlutterView,如圖:

這樣的好處是舷手,可以存在一定程度上的復(fù)用拧簸,又可以避免只有一個(gè)FlutterView出現(xiàn)的一些尷尬問(wèn)題劲绪。

FlutterView的首幀渲染耗時(shí)較高男窟,在Debug版本有明顯感受,大概會(huì)黑屏2秒贾富,release版本會(huì)好很多歉眷。但我們觀察Cpu曲線,發(fā)現(xiàn)還是一個(gè)較為耗時(shí)的過(guò)程颤枪。有一種體驗(yàn)優(yōu)化的思路是汗捡,我們可以預(yù)先讓將要使用的FlutterView加載好首幀,這樣畏纲,在真正使用的時(shí)候就很快了扇住,可以先建立一個(gè)只有1個(gè)像素的窗口,在這個(gè)窗口里面完成FlutterView首幀渲染盗胀,代碼如下:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末艘蹋,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子票灰,更是在濱河造成了極大的恐慌女阀,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件屑迂,死亡現(xiàn)場(chǎng)離奇詭異浸策,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)惹盼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)庸汗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人手报,你說(shuō)我怎么就攤上這事蚯舱〉裥剑” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵晓淀,是天一觀的道長(zhǎng)所袁。 經(jīng)常有香客問(wèn)我,道長(zhǎng)凶掰,這世上最難降的妖魔是什么燥爷? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮懦窘,結(jié)果婚禮上前翎,老公的妹妹穿的比我還像新娘。我一直安慰自己畅涂,他們只是感情好港华,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著午衰,像睡著了一般立宜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上臊岸,一...
    開(kāi)封第一講書(shū)人閱讀 51,365評(píng)論 1 302
  • 那天橙数,我揣著相機(jī)與錄音,去河邊找鬼帅戒。 笑死灯帮,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的逻住。 我是一名探鬼主播钟哥,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼瞎访!你這毒婦竟也來(lái)了腻贰?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤装诡,失蹤者是張志新(化名)和其女友劉穎银受,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體鸦采,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡宾巍,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了渔伯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片顶霞。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出选浑,到底是詐尸還是另有隱情蓝厌,我是刑警寧澤,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布古徒,位于F島的核電站拓提,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏隧膘。R本人自食惡果不足惜代态,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望疹吃。 院中可真熱鬧蹦疑,春花似錦、人聲如沸萨驶。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)腔呜。三九已至叁温,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間育谬,已是汗流浹背券盅。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工帮哈, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留膛檀,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓娘侍,卻偏偏與公主長(zhǎng)得像咖刃,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子憾筏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,111評(píng)論 25 707
  • 導(dǎo)讀:閑魚(yú)技術(shù)團(tuán)隊(duì)一直在探索如何使用Flutter來(lái)統(tǒng)一移動(dòng)App開(kāi)發(fā)嚎杨。移動(dòng)設(shè)備上的資源有限,內(nèi)存使用成了日常開(kāi)發(fā)...
    蓋世英雄_ix4n04閱讀 587評(píng)論 0 4
  • CSS1選擇器 .class 選擇 class="info" 的所有元素.info{background:red;...
    程序蝸牛閱讀 337評(píng)論 0 0
  • 體驗(yàn):做的事情氧腰,你可以有遺憾枫浙,但不可以有后悔!當(dāng)你覺(jué)得自己很苦的時(shí)候古拴,請(qǐng)看看你周?chē)嶂悖纯催@個(gè)世界別的人,一定會(huì)有人...
    郝佳慶閱讀 218評(píng)論 0 0