Android Flutter實踐內存初探

閒魚技術-匠修
咱們想使用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 Isolate緩存

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

  • 新生代(New Generation): 一般初次分配的對象都位於新生代中,該區域主要是存放內存較小而且生命週期較短的對象,好比局部變量。新生代會頻繁執行內存回收(GC),回收採用「複製-清除」算法,將內存分爲兩塊(圖中的from 和 to),運行時每次只使用其中的一塊(圖中的from),另外一塊備用(圖中的to)。當發生GC時,將當前使用的內存塊中存活的對象拷貝到備用內存塊中,而後清除當前使用內存塊,最後,交換兩塊內存的角色。

New Generation

  • 老年代(Old Generation): 在新生代的GC中「倖存」下來的對象,它們會被轉移到老年代中。老年代存放生命力週期較長,內存較大的對象。老年代一般比新生代要大不少。老年代的GC回收採用「標記-清除」算法,分紅標記和清除兩個階段。在標記階段,全部線程參與併發的完成對回收對象的標記,下降標記階段耗時。在清理階段,由GC線程負責清理回收對象,和應用線程同時執行,不影響應用運行。
    Old Generation

能夠看到,Dart VM借鑑了不少JVM的思路,Dart中產生內存泄露的方式也和Java相似,Java中不少排查內存泄露的思路和防止內存泄露的編程方法應該也能夠借鑑過來。併發

Image內存初探

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

//使用本地圖片
new Image.asset("images/xxxx.jpg");

//使用網絡圖片
new Image.network("https://xxxxxx");

咱們知道Android將內存分爲Java虛擬機內存和Native內存,各大廠商都對Java虛擬機內存有一個上限限制,到達上限就會觸發OOM異常,而對Native內存的使用沒有太嚴格的限制,如今的手機內存都很大,通常有較大的Native內存富餘。那麼Android中ImageView使用的是Java虛擬機內存仍是Native內存呢?ide

咱們能夠來作一個測試:在一個界面上,每點擊一次,就在上面堆加一張圖片。爲了防止後面的圖片徹底覆蓋前面的圖片而出現優化的狀況,每次都縮小几個像素,這樣就不會出現徹底覆蓋。
stack-img-test性能

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

Android 6.0(Google Nextus5)
nexus5-native-memgradle

Android 7.0(Meizu pro5)
nexus5-native-mem

Android 8.0(Google pixel)
nexus5-native-mem

在測試中,隨着圖片一張張增長,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版本的engine
private static String buildModeFor(buildType) {
    // if (buildType.name == "profile") {
    //     return "profile"
    // } else if (buildType.debuggable) {
    //     return "debug"
    // }
    return "release"
}

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

class StackImageState extends State<StackImages> {
  var images = <String>[];
  var index = 0;

  @override
  Widget build(BuildContext context) {
    var widgets = <Widget>[];

    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

能夠看到,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到當前Activity
mNativeView.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首幀渲染,代碼以下:

>>>>閱讀全文

相關文章
相關標籤/搜索