摘要:摘要實(shí)踐內(nèi)存初探閑魚(yú)技術(shù)匠修我們想使用來(lái)統(tǒng)一移動(dòng)開(kāi)發(fā)并做了一些實(shí)踐。將內(nèi)存管理分為新生代和老年代。在標(biāo)記階段,所有線程參與并發(fā)的完成對(duì)回收對(duì)象的標(biāo)記,降低標(biāo)記階段耗時(shí)。的首幀渲染耗時(shí)較高,在版本有明顯感受,大概會(huì)黑屏秒,版本會(huì)好很多。
摘要: Android Flutter實(shí)踐內(nèi)存初探 閑魚(yú)技術(shù)-匠修我們想使用Flutter來(lái)統(tǒng)一移動(dòng)App開(kāi)發(fā)并做了一些實(shí)踐。移動(dòng)設(shè)備上的資源有限,通常內(nèi)存使用都是一個(gè)我們?nèi)粘i_(kāi)發(fā)中十分關(guā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è)試和我們的開(kāi)發(fā)實(shí)踐,對(duì)日常關(guān)心的Bitmap內(nèi)存使用,View繪制內(nèi)存使用方面做一些探索。
閑魚(yú)技術(shù)-匠修
我們想使用Flutter來(lái)統(tǒng)一移動(dòng)App開(kāi)發(fā)并做了一些實(shí)踐。移動(dòng)設(shè)備上的資源有限,通常內(nèi)存使用都是一個(gè)我們?nèi)粘i_(kāi)發(fā)中十分關(guā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è)試和我們的開(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)都是隔離的,彼此之間通過(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)記階段,所有線程參與并發(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,我們可以方便的使用:
//使用本地圖片 new Image.asset("images/xxxx.jpg"); //使用網(wǎng)絡(luò)圖片 new Image.network("https://xxxxxx");
我們知道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)黾?,Android 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):
//不做判斷,強(qiáng)制改為打包release版本的engine private static String buildModeFor(buildType) { // if (buildType.name == "profile") { // return "profile" // } else if (buildType.debuggable) { // return "debug" // } return "release" }
相同地,我們向Flutter界面中添加圖片并用Android Profiler來(lái)觀察內(nèi)存,測(cè)試使用的dart代碼:
class StackImageState extends State{ var images = []; var index = 0; @override Widget build(BuildContext context) { var widgets = []; for (int i = 0; i <= index; i++) { var pos = i - (i ~/ 103) * 103; widgets.add(new Container( child: new Image.asset("images/${pos}.jpg", fit: BoxFit.cover), padding: new EdgeInsets.only(top: i * 2.0))); } widgets.add(new Center( child: new GestureDetector( child: new Container( child: new Text("添加圖片(${index})", style: new TextStyle(color: Colors.red)), color: Colors.green, padding: const EdgeInsets.all(8.0)), onTap: () { setState(() { index++; }); }))); return new Stack( children: widgets, alignment: AlignmentDirectional.topCenter); } }
得到的結(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多帶帶拎出來(lái)使用。所幸現(xiàn)在FlutterView和Activity耦合程度并不很深,最關(guān)鍵的地方是FlutterNativeView必須attach一個(gè)Activity:
//attach到當(dāng)前Activity mNativeView.attachViewAndActivity(this, 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首幀渲染,代碼如下:
final WindowManager wm = mFakeActivity.getWindowManager(); final FrameLayout root = new FrameLayout(mFakeActivity); //一個(gè)像素足矣 FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(1, 1); root.addView(flutterView,params); WindowManager.LayoutParams wlp = new WindowManager.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); final FlutterView.FirstFrameListener[] listenerRef = new FlutterView.FirstFrameListener[1]; listenerRef[0] = new FlutterView.FirstFrameListener() { @Override public void onFirstFrame() { //首幀渲染完后取消窗口 wm.removeView(root); flutterView.removeFirstFrameListener(listenerRef[0]); } }; flutterView.addFirstFrameListener(listenerRef[0]); String appBundlePath = FlutterMain.findAppBundlePath(mFakeActivity.getApplicationContext()); flutterView.runFromBundle(appBundlePath, null, "main", true);
原文鏈接
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/71453.html
摘要:跨平臺(tái)開(kāi)發(fā)是目前開(kāi)發(fā)較熱門(mén)的方向,在這方面取得了很大的成功,同時(shí)也獲得了非常多的關(guān)注。最近發(fā)現(xiàn)了跨平臺(tái)的一個(gè)新框架,從官方簡(jiǎn)介來(lái)看這個(gè)框架還處于狀態(tài),目前還沒(méi)有完整的文檔,只有體驗(yàn)。希望能有更多的跨平臺(tái)開(kāi)發(fā)框架出現(xiàn),推動(dòng)技術(shù)的發(fā)展。 跨平臺(tái)開(kāi)發(fā)是目前開(kāi)發(fā)較熱門(mén)的方向,React Native 在這方面取得了很大的成功,同時(shí) Flutter 也獲得了非常多的關(guān)注。React Native...
摘要:原方式中是經(jīng)過(guò)壓縮的腳本文件,預(yù)編譯后則是二進(jìn)制文件。兩者影響疊加導(dǎo)致整體減小,包大小得到優(yōu)化。引擎包引擎包官方文檔中對(duì)內(nèi)存區(qū)的描述您的應(yīng)用用于處理代碼和資源如字節(jié)碼已優(yōu)化或已編譯的碼庫(kù)和字體的內(nèi)存。本文首發(fā)自普惠出行產(chǎn)品技術(shù) 自從 Google 的 Flutter 發(fā)布之后,F(xiàn)acebook 對(duì) React-Native 的迭代開(kāi)始快了起來(lái),優(yōu)化 React-Native 的性能表現(xiàn)...
摘要:是谷歌的移動(dòng)框架,可以快速在和上構(gòu)建高質(zhì)量的原生用戶(hù)界面。在全世界好了這些,大家早就知道了,來(lái)點(diǎn)實(shí)在的話說(shuō)隔壁師兄,閑魚(yú)是最早一批與谷歌展開(kāi)合作,并在重要的商品詳情頁(yè)中使用技術(shù)上線的。一切皆來(lái)自的組件皆來(lái)自。是狀態(tài)不可變的稱(chēng)為無(wú)狀態(tài)。 前言 要說(shuō)2018年最火的跨端技術(shù),當(dāng)屬于 Flutter 莫屬,應(yīng)該沒(méi)人質(zhì)疑吧。一個(gè)新的技術(shù)的趨勢(shì),最明顯的特征,就是它一定想把前浪拍死在沙灘上。這個(gè)...
摘要:是谷歌的移動(dòng)框架,可以快速在和上構(gòu)建高質(zhì)量的原生用戶(hù)界面。在全世界好了這些,大家早就知道了,來(lái)點(diǎn)實(shí)在的話說(shuō)隔壁師兄,閑魚(yú)是最早一批與谷歌展開(kāi)合作,并在重要的商品詳情頁(yè)中使用技術(shù)上線的。一切皆來(lái)自的組件皆來(lái)自。是狀態(tài)不可變的稱(chēng)為無(wú)狀態(tài)。 前言 要說(shuō)2018年最火的跨端技術(shù),當(dāng)屬于 Flutter 莫屬,應(yīng)該沒(méi)人質(zhì)疑吧。一個(gè)新的技術(shù)的趨勢(shì),最明顯的特征,就是它一定想把前浪拍死在沙灘上。這個(gè)...
閱讀 3535·2021-10-14 09:42
閱讀 2803·2021-09-08 10:44
閱讀 1402·2021-09-02 10:18
閱讀 3792·2021-08-30 09:43
閱讀 2906·2021-07-29 13:49
閱讀 3785·2019-08-29 17:02
閱讀 1645·2019-08-29 15:09
閱讀 1094·2019-08-29 11:01