摘要:摘要實踐內(nèi)存初探閑魚技術匠修我們想使用來統(tǒng)一移動開發(fā)并做了一些實踐。將內(nèi)存管理分為新生代和老年代。在標記階段,所有線程參與并發(fā)的完成對回收對象的標記,降低標記階段耗時。的首幀渲染耗時較高,在版本有明顯感受,大概會黑屏秒,版本會好很多。
摘要: Android Flutter實踐內(nèi)存初探 閑魚技術-匠修我們想使用Flutter來統(tǒng)一移動App開發(fā)并做了一些實踐。移動設備上的資源有限,通常內(nèi)存使用都是一個我們?nèi)粘i_發(fā)中十分關注的問題。那么,F(xiàn)lutter是如何使用內(nèi)存,又會對Native App的內(nèi)存帶來哪些影響呢?本文將簡單介紹Flutter內(nèi)存機制,結合測試和我們的開發(fā)實踐,對日常關心的Bitmap內(nèi)存使用,View繪制內(nèi)存使用方面做一些探索。
閑魚技術-匠修
我們想使用Flutter來統(tǒng)一移動App開發(fā)并做了一些實踐。移動設備上的資源有限,通常內(nèi)存使用都是一個我們?nèi)粘i_發(fā)中十分關注的問題。那么,F(xiàn)lutter是如何使用內(nèi)存,又會對Native App的內(nèi)存帶來哪些影響呢?本文將簡單介紹Flutter內(nèi)存機制,結合測試和我們的開發(fā)實踐,對日常關心的Bitmap內(nèi)存使用,View繪制內(nèi)存使用方面做一些探索。
Dart RunTime簡介
Flutter Framework使用Dart語言開發(fā),所以App進程中需要一個Dart運行環(huán)境(VM),和Android Art一樣,F(xiàn)lutter也對Dart源碼做了AOT編譯,直接將Dart源碼編譯成了本地字節(jié)碼,沒有了解釋執(zhí)行的過程,提升執(zhí)行性能。這里重點關注Dart VM內(nèi)存分配(Allocate)和回收(GC)相關的部分。
和Java顯著不同的是Dart的"線程"(Isolate)是不共享內(nèi)存的,各自的堆(Heap)和棧(Stack)都是隔離的,彼此之間通過消息通道來通信。Dart天然不存在數(shù)據(jù)競爭和變量狀態(tài)同步的問題,整個Flutter Framework Widget的渲染過程都運行在一個isolate中。
Dart VM將內(nèi)存管理分為新生代(New Generation)和老年代(Old Generation)。
新生代(New Generation): 通常初次分配的對象都位于新生代中,該區(qū)域主要是存放內(nèi)存較小并且生命周期較短的對象,比如局部變量。新生代會頻繁執(zhí)行內(nèi)存回收(GC),回收采用“復制-清除”算法,將內(nèi)存分為兩塊(圖中的from 和 to),運行時每次只使用其中的一塊(圖中的from),另一塊備用(圖中的to)。當發(fā)生GC時,將當前使用的內(nèi)存塊中存活的對象拷貝到備用內(nèi)存塊中,然后清除當前使用內(nèi)存塊,最后,交換兩塊內(nèi)存的角色。
老年代(Old Generation): 在新生代的GC中“幸存”下來的對象,它們會被轉移到老年代中。老年代存放生命力周期較長,內(nèi)存較大的對象。老年代通常比新生代要大很多。老年代的GC回收采用“標記-清除”算法,分成標記和清除兩個階段。在標記階段,所有線程參與并發(fā)的完成對回收對象的標記,降低標記階段耗時。在清理階段,由GC線程負責清理回收對象,和應用線程同時執(zhí)行,不影響應用運行。
可以看到,Dart VM借鑒了很多JVM的思路,Dart中產(chǎn)生內(nèi)存泄露的方式也和Java類似,Java中很多排查內(nèi)存泄露的思路和防止內(nèi)存泄露的編程方法應該也可以借鑒過來。
Image內(nèi)存初探
對圖片的合理使用和優(yōu)化是UI編程的重要部分,F(xiàn)lutter提供了Image Widget,我們可以方便的使用:
//使用本地圖片 new Image.asset("images/xxxx.jpg"); //使用網(wǎng)絡圖片 new Image.network("https://xxxxxx");
我們知道Android將內(nèi)存分為Java虛擬機內(nèi)存和Native內(nèi)存,各大廠商都對Java虛擬機內(nèi)存有一個上限限制,到達上限就會觸發(fā)OOM異常,而對Native內(nèi)存的使用沒有太嚴格的限制,現(xiàn)在的手機內(nèi)存都很大,一般有較大的Native內(nèi)存富余。那么Android中ImageView使用的是Java虛擬機內(nèi)存還是Native內(nèi)存呢?
我們可以來做一個測試:在一個界面上,每點擊一次,就在上面堆加一張圖片。為了防止后面的圖片完全覆蓋前面的圖片而出現(xiàn)優(yōu)化的情況,每次都縮小幾個像素,這樣就不會出現(xiàn)完全覆蓋。
打開Android Profiler,一張一張?zhí)砑訄D片,觀察內(nèi)存數(shù)據(jù)。分別測試了Android的6.0,7.0和8.0系統(tǒng),結果如下:
Android 6.0(Google Nextus5)
Android 7.0(Meizu pro5)
Android 8.0(Google pixel)
在測試中,隨著圖片一張張增加,Android 6.0 和 7.0都是Java部分的內(nèi)存在增長,而Android 8.0則是Native部分的內(nèi)存在增長。由此有結論,Android原生的ImageView在6.0和7.0版本中使用的Java虛擬機內(nèi)存,而在Android 8.0中則使用的Native內(nèi)存。
而Flutter Image Widget使用的是哪部分內(nèi)存呢?我們用Flutter界面來做相同的測試。Flutter Engine的Debug版本和Release版本存在很大的性能差異,所以我們測試最好使用Release版本,但是,Release版本的Apk又不能使用Android profiler來觀察內(nèi)存,所以我們需要在Debug版本的Apk中打包一個Release版本的Flutter Engine, 可以修改flutter tool中的flutter.gradle來實現(xiàn):
//不做判斷,強制改為打包release版本的engine private static String buildModeFor(buildType) { // if (buildType.name == "profile") { // return "profile" // } else if (buildType.debuggable) { // return "debug" // } return "release" }
相同地,我們向Flutter界面中添加圖片并用Android Profiler來觀察內(nèi)存,測試使用的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); } }
得到的結果是:
Android 6.0
Android 8.0
可以看到,F(xiàn)lutter Image使用的內(nèi)存既不屬于Java虛擬機內(nèi)存也不屬于Native內(nèi)存,而是Graphics內(nèi)存(在Meizu pro5設備上也不屬于Graphics,事實上Meizu pro5設備不能歸類Flutter Image所使用的內(nèi)存),官方對Graphics內(nèi)存的解釋是:
那么至少Flutter Image所使用的內(nèi)存不會是Java虛擬機內(nèi)存,這對不少Android設備都是一個好消息,這意味著使用Flutter Image沒有OOM的風險,能夠較好的利用Native內(nèi)存。
使用Image的時候,建立一個內(nèi)存緩存池是個好習慣,F(xiàn)lutter Framework提供了一個ImageCache來緩存加載的圖片,但它不同于Android Lru Cache,不能精確的使用內(nèi)存大小來設定緩存池容量,而是只能粗略的指定最大緩存圖片張數(shù)。
FlutterView內(nèi)存初探
Flutter設計之初是想統(tǒng)一Android和IOS的界面編程,所以理想的基于Flutter的apk只需要提供一個MainActivity做入口即可,后面所有的頁面跳轉都在FlutterView中管理。但是,如果是一個已有規(guī)模的app接入Flutter開發(fā),我們不可能將已有的Activity頁面都用Flutter重新實現(xiàn)一遍,這時候就需要考慮本地頁面和Flutter頁面之間的跳轉交互了。iOS可以方便的管理頁面棧,但是Android就很復雜(Android有任務棧機制,低內(nèi)存Activity回收機制等),所以通常我們還是使用Activity作為頁面容器來展示flutter頁面。這時有兩種選擇,可以每次啟動一個Activity就啟動一個新的FlutterView,也可以啟動Activity的時候復用已有的FlutterView。
不復用FlutterView
復用FlutterView
Flutter Framework中FlutterView是綁定Activity使用的,要復用FlutterView就必須能夠把FlutterView多帶帶拎出來使用。所幸現(xiàn)在FlutterView和Activity耦合程度并不很深,最關鍵的地方是FlutterNativeView必須attach一個Activity:
//attach到當前Activity mNativeView.attachViewAndActivity(this, activity);
初始化FlutterView時必須傳入一個Activity,當其他Activity復用FlutterView時再調(diào)用該Attach方法即可。這里有個問題,就是FlutterView中必須保存一個Activity引用,這個一個內(nèi)存泄露隱患,我們可以在FluterView detach時候將MainActivity傳入,因為通常整個App交互過程中MainActivity都是一直存在的,可以避免其他Activity泄露。
為了更好的權衡兩種方法的利弊,我們先用空頁面來測試一下當頁面增加時內(nèi)存的變化:
不復用FlutterView時,頁面增加時內(nèi)存變化
復用FlutterView時,頁面增加時內(nèi)存變化
不復用FlutterView時平均打開一個頁面(空頁面),Java內(nèi)存增長0.02M,Native內(nèi)存增長0.73M。復用FlutterView時平均打開一個頁面(空頁面),Java內(nèi)存增長0.019M,Native內(nèi)存增長0.65M。可見復用FlutterView在內(nèi)存使用上是有優(yōu)勢的,但主要復用的還是Native部分的內(nèi)存。復用FlutterView必然帶來額外的一些復雜邏輯,有時候為了邏輯簡單,后期維護上的方便,犧牲一些相對不太珍貴的Native內(nèi)存也是值得的。
復用單個FlutterView有時會有些“意外”,比如當Activity切換時,就不得不將當前FlutterView detach掉給后面新建的Activity使用,當前界面就會空白閃動,有個想法是可以將當前界面截屏下來遮擋住后面的界面變化,這種方式有時會帶來額外的適配問題。
FlutterView復用與否不是絕對的,有時候可以使用一些綜合性折中方案,比如,我們可以建立一個FlutterViewProvider,里面維護N個可復用的FlutterView,如圖:
這樣的好處是,可以存在一定程度上的復用,又可以避免只有一個FlutterView出現(xiàn)的一些尷尬問題。
FlutterView的首幀渲染耗時較高,在Debug版本有明顯感受,大概會黑屏2秒,release版本會好很多。但我們觀察Cpu曲線,發(fā)現(xiàn)還是一個較為耗時的過程。有一種體驗優(yōu)化的思路是,我們可以預先讓將要使用的FlutterView加載好首幀,這樣,在真正使用的時候就很快了,可以先建立一個只有1個像素的窗口,在這個窗口里面完成FlutterView首幀渲染,代碼如下:
final WindowManager wm = mFakeActivity.getWindowManager(); final FrameLayout root = new FrameLayout(mFakeActivity); //一個像素足矣 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);
原文鏈接
文章版權歸作者所有,未經(jīng)允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://systransis.cn/yun/71453.html
摘要:跨平臺開發(fā)是目前開發(fā)較熱門的方向,在這方面取得了很大的成功,同時也獲得了非常多的關注。最近發(fā)現(xiàn)了跨平臺的一個新框架,從官方簡介來看這個框架還處于狀態(tài),目前還沒有完整的文檔,只有體驗。希望能有更多的跨平臺開發(fā)框架出現(xiàn),推動技術的發(fā)展。 跨平臺開發(fā)是目前開發(fā)較熱門的方向,React Native 在這方面取得了很大的成功,同時 Flutter 也獲得了非常多的關注。React Native...
摘要:原方式中是經(jīng)過壓縮的腳本文件,預編譯后則是二進制文件。兩者影響疊加導致整體減小,包大小得到優(yōu)化。引擎包引擎包官方文檔中對內(nèi)存區(qū)的描述您的應用用于處理代碼和資源如字節(jié)碼已優(yōu)化或已編譯的碼庫和字體的內(nèi)存。本文首發(fā)自普惠出行產(chǎn)品技術 自從 Google 的 Flutter 發(fā)布之后,F(xiàn)acebook 對 React-Native 的迭代開始快了起來,優(yōu)化 React-Native 的性能表現(xiàn)...
摘要:不努力不奮斗,可能就會在基層一輩子止步不前。不過,只一句,如果你還在做這一行,還是一名程序猿媛,想走上坡路的你,也許我這到手的十幾家一線互聯(lián)網(wǎng)公司性能優(yōu)化項目實戰(zhàn)可能會對你有所幫助。 ...
摘要:是谷歌的移動框架,可以快速在和上構建高質(zhì)量的原生用戶界面。在全世界好了這些,大家早就知道了,來點實在的話說隔壁師兄,閑魚是最早一批與谷歌展開合作,并在重要的商品詳情頁中使用技術上線的。一切皆來自的組件皆來自。是狀態(tài)不可變的稱為無狀態(tài)。 前言 要說2018年最火的跨端技術,當屬于 Flutter 莫屬,應該沒人質(zhì)疑吧。一個新的技術的趨勢,最明顯的特征,就是它一定想把前浪拍死在沙灘上。這個...
閱讀 3446·2021-10-14 09:42
閱讀 2737·2021-09-08 10:44
閱讀 1310·2021-09-02 10:18
閱讀 3619·2021-08-30 09:43
閱讀 2807·2021-07-29 13:49
閱讀 3729·2019-08-29 17:02
閱讀 1588·2019-08-29 15:09
閱讀 1041·2019-08-29 11:01