導(dǎo)讀:閑魚技術(shù)團(tuán)隊(duì)一直在探索如何使用Flutter來統(tǒng)一移動(dòng)App開發(fā)。移動(dòng)設(shè)備上的資源有限,內(nèi)存使用成了日常開發(fā)中的常見問題衙傀。那么,F(xiàn)lutter是如何使用內(nèi)存萨咕,又會(huì)對(duì)Native App的內(nèi)存帶來哪些影響呢统抬?本文將簡單介紹Flutter內(nèi)存機(jī)制,結(jié)合測(cè)試和閑魚技術(shù)團(tuán)隊(duì)的開發(fā)實(shí)踐危队,對(duì)普遍關(guān)心的Bitmap內(nèi)存使用聪建,View繪制內(nèi)存使用方面做一些探索。
Dart RunTime簡介
Flutter Framework使用Dart語言開發(fā)茫陆,所以App進(jìn)程中需要一個(gè)Dart運(yùn)行環(huán)境(VM)金麸,和Android Art一樣,F(xiàn)lutter也對(duì)Dart源碼做了AOT編譯簿盅,直接將Dart源碼編譯成了本地字節(jié)碼挥下,沒有了解釋執(zhí)行的過程,提升執(zhí)行性能桨醋。這里重點(diǎn)關(guān)注Dart VM內(nèi)存分配(Allocate)和回收(GC)相關(guān)的部分棚瘟。
和Java顯著不同的是Dart的"線程"(Isolate)是不共享內(nèi)存的,各自的堆(Heap)和棧(Stack)都是隔離的讨盒,并且是各自獨(dú)立GC的解取,彼此之間通過消息通道來通信。Dart天然不存在數(shù)據(jù)競(jìng)爭(zhēng)和變量狀態(tài)同步的問題返顺,整個(gè)Flutter Framework Widget的渲染過程都運(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中“幸存”下來的對(duì)象倡缠,它們會(huì)被轉(zhuǎn)移到老年代中哨免。老年代存放生命力周期較長,內(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類似,Java中很多排查內(nèi)存泄露的思路和防止內(nèi)存泄露的編程方法應(yīng)該也可以借鑒過來蔬浙。
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)存的使用沒有太嚴(yán)格的限制俱病,現(xiàn)在的手機(jī)內(nèi)存都很大官疲,一般有較大的Native內(nèi)存富余。那么Android中ImageView使用的是Java虛擬機(jī)內(nèi)存還是Native內(nèi)存呢亮隙?
我們可以來做一個(gè)測(cè)試:在一個(gè)界面上途凫,每點(diǎn)擊一次,就在上面堆加一張圖片溢吻。為了防止后面的圖片完全覆蓋前面的圖片而出現(xiàn)優(yōu)化的情況维费,每次都縮小幾個(gè)像素,這樣就不會(huì)出現(xiàn)完全覆蓋促王。
打開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)存在增長恶阴,而Android 8.0則是Native部分的內(nèi)存在增長诈胜。由此有結(jié)論,Android原生的ImageView在6.0和7.0版本中使用的Java虛擬機(jī)內(nèi)存冯事,而在Android 8.0中則使用的Native內(nèi)存焦匈。
而Flutter Image Widget使用的是哪部分內(nèi)存呢?我們用Flutter界面來做相同的測(cè)試昵仅。Flutter Engine的Debug版本和Release版本存在很大的性能差異缓熟,所以我們測(cè)試最好使用Release版本,但是摔笤,Release版本的Apk又不能使用Android profiler來觀察內(nèi)存够滑,所以我們需要在Debug版本的Apk中打包一個(gè)Release版本的Flutter Engine, 可以修改flutter tool中的flutter.gradle來實(shí)現(xiàn):
相同地,我們向Flutter界面中添加圖片并用Android Profiler來觀察內(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è)備不能歸類Flutter Image所使用的內(nèi)存),官方對(duì)Graphics內(nèi)存的解釋是:
那么至少Flutter Image所使用的內(nèi)存不會(huì)是Java虛擬機(jī)內(nèi)存命辖,這對(duì)不少Android設(shè)備都是一個(gè)好消息况毅,這意味著使用Flutter Image沒有OOM的風(fēng)險(xiǎn),能夠較好的利用Native內(nèi)存尔艇。
使用Image的時(shí)候尔许,建立一個(gè)內(nèi)存緩存池是個(gè)好習(xí)慣,F(xiàn)lutter Framework提供了一個(gè)ImageCache來緩存加載的圖片终娃,但它不同于Android Lru Cache味廊,不能精確的使用內(nèi)存大小來設(shè)定緩存池容量,而是只能粗略的指定最大緩存圖片張數(shù)棠耕。
FlutterView內(nèi)存初探
Flutter設(shè)計(jì)之初是想統(tǒng)一Android和IOS的界面編程余佛,所以理想的基于Flutter的apk只需要提供一個(gè)MainActivity做入口即可,后面所有的頁面跳轉(zhuǎn)都在FlutterView中管理窍荧。但是衙熔,如果是一個(gè)已有規(guī)模的app接入Flutter開發(fā),我們不可能將已有的Activity頁面都用Flutter重新實(shí)現(xiàn)一遍搅荞,這時(shí)候就需要考慮本地頁面和Flutter頁面之間的跳轉(zhuǎn)交互了红氯。iOS可以方便的管理頁面棧,但是Android就很復(fù)雜(Android有任務(wù)棧機(jī)制咕痛,低內(nèi)存Activity回收機(jī)制等)痢甘,所以通常我們還是使用Activity作為頁面容器來展示flutter頁面。這時(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ú)拎出來使用放椰。所幸現(xiàn)在FlutterView和Activity耦合程度并不很深作烟,最關(guān)鍵的地方是FlutterNativeView必須attach一個(gè)Activity:
初始化FlutterView時(shí)必須傳入一個(gè)Activity,當(dāng)其他Activity復(fù)用FlutterView時(shí)再調(diào)用該Attach方法即可砾医。這里有個(gè)問題拿撩,就是FlutterView中必須保存一個(gè)Activity引用,這個(gè)一個(gè)內(nèi)存泄露隱患如蚜,我們可以在FluterView detach時(shí)候?qū)ainActivity傳入压恒,因?yàn)橥ǔU麄€(gè)App交互過程中MainActivity都是一直存在的,可以避免其他Activity泄露错邦。
為了更好的權(quán)衡兩種方法的利弊探赫,我們先用空頁面來測(cè)試一下當(dāng)頁面增加時(shí)內(nèi)存的變化:
不復(fù)用FlutterView時(shí),頁面增加時(shí)內(nèi)存變化
復(fù)用FlutterView時(shí)撬呢,頁面增加時(shí)內(nèi)存變化
不復(fù)用FlutterView時(shí)平均打開一個(gè)頁面(空頁面)伦吠,Java內(nèi)存增長0.02M,Native內(nèi)存增長0.73M。復(fù)用FlutterView時(shí)平均打開一個(gè)頁面(空頁面),Java內(nèi)存增長0.019M,Native內(nèi)存增長0.65M魂拦√智冢可見復(fù)用FlutterView在內(nèi)存使用上是有優(yōu)勢(shì)的,但主要復(fù)用的還是Native部分的內(nèi)存晨另。復(fù)用FlutterView必然帶來額外的一些復(fù)雜邏輯,有時(shí)候?yàn)榱诉壿嫼唵纹仔眨笃诰S護(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)前界面截屏下來遮擋住后面的界面變化茂契,這種方式有時(shí)會(huì)帶來額外的適配問題。
FlutterView復(fù)用與否不是絕對(duì)的慨绳,有時(shí)候可以使用一些綜合性折中方案掉冶,比如,我們可以建立一個(gè)FlutterViewProvider,里面維護(hù)N個(gè)可復(fù)用的FlutterView脐雪,如圖:
這樣的好處是厌小,可以存在一定程度上的復(fù)用,又可以避免只有一個(gè)FlutterView出現(xiàn)的一些尷尬問題战秋。
FlutterView的首幀渲染耗時(shí)較高璧亚,在Debug版本有明顯感受,大概會(huì)黑屏2秒脂信,release版本會(huì)好很多癣蟋。但我們觀察Cpu曲線透硝,發(fā)現(xiàn)還是一個(gè)較為耗時(shí)的過程。有一種體驗(yàn)優(yōu)化的思路是疯搅,我們可以預(yù)先讓將要使用的FlutterView加載好首幀濒生,這樣,在真正使用的時(shí)候就很快了秉撇,可以先建立一個(gè)只有1個(gè)像素的窗口甜攀,在這個(gè)窗口里面完成FlutterView首幀渲染,代碼如下:
以上就是閑魚團(tuán)隊(duì)在Flutter的應(yīng)用過程中的一些實(shí)踐琐馆。
原文地址鏈接:https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&mid=2247487543&idx=1&sn=c1b739429d56754ce078afa476f7c1a4&chksm=e9292d38de5ea42e31d6beae009be1bfe655e0ae9043a72c3916736b5d39fb6dac6c52b71097&mpshare=1&scene=1&srcid=0524mZK4t4LLUAdzmec6eN3c#rd