天天看點

Flutter Image記憶體--強引用分析方法概述分析總結

概述

據了解,很多Flutter業務上線後都出現記憶體占用較高的問題,首當其沖的是 Image 記憶體占用過多。

Image 圖檔記憶體過高,可能由于 Flutter ImageCache 對記憶體缺房控制力導緻,也有可能是被業務代碼強引用,洩漏導緻。如果 Image 被業務強引用,則調整 ImageCache 容量,增加 gc 次數都沒有效果。

Flutter Image記憶體--強引用分析方法概述分析總結

面對這種“強引用”的洩漏,需要定位到引用 Image 的業務代碼才能解決。本文主要描述怎麼利用 Observatory 工具,定位強引用Image的業務代碼。

分析

class ImageTestWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _ImageTestWidgetState();
  }
}

class _ImageTestWidgetState extends State<ImageTestWidget> {
  @override
  Widget build(BuildContext context) {
    return Center(
        child: Image(
          image: NetworkImage("https://avatars2.githubusercontent.com/u/20411648?s=460&v=4"),
          width: 200.0,
        )
    );
  }
}           

以一個簡單的例子來說明,怎麼通過 Image,反查引用鍊,定位到是 ImageTestWidget 對其引用。這裡的 ImagetTestWidget 就是我們的目标,從這裡就能看出 Image 被哪個頁面持有/洩漏。

思路是從 Image 開始反向周遊 Widget tree,定位到 ImageTestWidget。期望的引用鍊如下:

Flutter Image記憶體--強引用分析方法概述分析總結

Image 引用鍊

Flutter Image記憶體--強引用分析方法概述分析總結

Observatory 會将 instance 到 gc Root 的引用鍊顯示在 Retained path 下。如圖中紅圈1所示,Image 的 Retained path 隻顯示 image 對象被 weak persistent handle(參考

Flutter記憶體分析

描述,這是一種 GC Root,gc時會被周遊,如果 dart對象 被回收,其關聯的 external 記憶體會得到釋放)直接持有,但根據這個資訊顯然是無法定位到我們的目标 ImageTestWidget。

為什麼 Observatory 顯示 Image 的引用鍊與上面預期的不一樣?

原來這裡的 Image 與我們代碼中的 Image 并不是同一個類。

part of dart.ui;

class ImageInfo {
  /// Creates an [ImageInfo] object for the given [image] and [scale].
  ///
  /// Both the image and the scale must not be null.
  ///
  /// The tag may be used to identify the source of this image.
  const ImageInfo({ @required this.image, this.scale = 1.0, this.debugLabel })
    : assert(image != null),
      assert(scale != null);

  /// The raw image pixels.
  ///
  /// This is the object to pass to the [Canvas.drawImage],
  /// [Canvas.drawImageRect], or [Canvas.drawImageNine] methods when painting
  /// the image.
  final ui.Image image;
  ...
}

// Observatory 中顯示的 Image 對象
class Image extends NativeFieldWrapperClass2 {
  // This class is created by the engine, and should not be instantiated
  // or extended directly.
  //
  // To obtain an [Image] object, use [instantiateImageCodec].
  @pragma('vm:entry-point')
  Image._();
  ...
  /// Returns an error message on failure, null on success.
  String? _toByteData(int format, _Callback<Uint8List?> callback) native 'Image_toByteData';

  /// Release the resources used by this object. The object is no longer usable
  /// after this method is called.
  void dispose() native 'Image_dispose';
  ...
 }           

代碼中直接用 Image.network() 的 Iamge:

// 業務代碼中 Image 對象
class Image extends StatefulWidget {
  ...
  Image.network();
  ...
}

class _ImageState extends State<Image> with WidgetsBindingObserver {
  @override
  Widget build(BuildContext context) {
    ...

    Widget result = RawImage(
      image: _imageInfo?.image,
      ...
    );
    ...
  }
}
           

從上面代碼片段可知,Observatory 中顯示的 image 是 ui.Image。分析圖檔解碼過程可知,這個是engine中解碼後的 CanvasImage(真正占用記憶體的地方) 在 dart 層的表示,并通過 weak persistent handle 關聯起來。這個 ui.Image會至少被3個對象持有,上圖圈2中:

  • ImageInfo : 解碼完成後儲存 ui.Image
  • RawImage : 持有 ui.Image 的 widget
  • RenderImage : 持有 ui.Image 的 renderObject

結合 _ImageState 的 build() 方法,可知 RawIamge 才是我們用 Image.network() 生成在 Widget tree 上的對象,是以我們應該從 RawImage 開始查找。

RawImage 引用鍊

Flutter Image記憶體--強引用分析方法概述分析總結

通過 Inbounce references 檢視反向引用。可見引用 RawImage 有2個分支(實驗場景簡單,實際場景會有更多引用路徑),哪個分支的路徑能夠到達 ImageTestWidget 呢?

這裡先簡單梳理下 Flutter Widget tree,Element tree 的關系:

Flutter Image記憶體--強引用分析方法概述分析總結

從主要類關系看到,Widget 與 Element 具有一一對應關系。

兩棵樹生成的過程:

  1. runApp() 時候 attachRootWidget(Widget rootWidget),root Widget 為 RenderObjectToWidgetAdapter,root Element 為 RenderObjectToWidgetElement,其 child 對應 RenderObjectToWidgetAdapter.child,即我們的app代碼
  2. mount 時候會遞歸将 child / children 對應的 element 建構出來,并挂載到 Element tree
  3. 最後想成一棵 Element tree。

這裡需要注意到是,Widget 家族是沒有 child 這個成員,而 Element 家族有, 真正具有引用關系的那棵樹是 Element tree,Widget tree 概念上成立,但對象間并無直接引用但關系。

業務代碼通常不會直接去寫 Element,Element是根據我們定義的 Widget 生成的,總之 , 反向引用鍊 查找的方法是:

  1. 找到第一個 Widget 對應的 Element
  2. 從這個 Element 開始反向周遊 Element 樹,通過 _parent, _child 屬性可周遊一條完整的引用支路
  3. Element 中 _widget, _state 可幫我們确定某個節點是否是我們目标到 Widget,例如例子中的 ImageTestWidget

下面回答上面 “哪個才是正确的引用路徑” 的問題。

RawImage 對應的 element 是 LeafRenderObjectElement,其 _widget 引用了 RawImage。接着從 LeafRenderObjectElement 反向周遊 Element tree,跟蹤 _child 屬性引用自己的分支展開即可定位到 ImageTestWidget。如下展示了從 ui.Image 到 ImageTestWidget的完整引用鍊:

Flutter Image記憶體--強引用分析方法概述分析總結

BuildContext 洩漏

通過上面的操作已經可以定位到強引用 Image 的業務代碼 Widget,也可以定位其歸屬的頁面。根據頁面是否關閉,則可确定是否洩漏。如果是洩漏,那是什麼導緻了業務 Widget 沒有被系統回收呢?

Dart_NotifyIdle() 的邏輯可看出,Flutter gc機制是可以應付頻繁建立/銷毀 Widget 的這種操作的 ,它會盡量在每一幀繪制完都去執行 gc 操作,回收掉銷毀的 Widget。

每一幀畫完後,在 finalizeTree() 中會對 inactive elements 進行 unmount 操作

@override
  void drawFrame() {
    ...
    assert(!debugBuildingDirtyElements);
    try {
      if (renderViewElement != null)
        buildOwner.buildScope(renderViewElement);
      super.drawFrame();
      // 回收 elements 
      buildOwner.finalizeTree();
    } finally {
      assert(() {
        debugBuildingDirtyElements = false;
        return true;
      }());
    }
    ...
  }           

elements 被 unmount 之後,脫離 element 樹,連同被它引用的 widget, state 都會後面的被 gc 回收。

Flutter Image記憶體--強引用分析方法概述分析總結

總之,Widget 是在 Element 回收後,關聯被回收的。同樣的,如果 Element 沒有被回收,那被它關聯的 Widget,State 都不會被回收。Element 洩漏的機率比較高,因為 Element 繼承了 BuildContext,業務代碼會經常傳遞這個參數。特别是 異步執行 的代碼的場景(Feature, async/await,methodChannel),這些代碼可能會長期持有傳入的BuildContext,導緻 element 以及關聯的 widget, state 發生洩漏。

追蹤具體洩漏 BuildContext 的代碼,可以繼續從這個 Element 反向引用繼續查找,特别留意 Clouse, Completer 之類對其的引用。

總結

本文主要提煉了一種基于 Observatory 工具,從 Image 反向引用定位到業務 Widget 的方法,用于解決 Image 強引用的問題。而造成 Image 強引用的根本原因很大可能是 BuildContext 被異步代碼長期持有導緻。