乐趣区

关于flutter:定位Flutter内存问题很难么

简介: flutter 内存透露定位

作者:闲鱼技术 - 三莅

内存水位升高导致的稳定性问题重大影响 app 用户体验,所以开发者们十分关注 Flutter 的内存体现。随着 Flutter 业务越来越多,闲鱼也面临着 oom 导致的 crash 率晋升的问题,上面咱们联合我的项目中理论遇到的内存问题和解决思路跟大家分享下 flutter 内存优化的教训。

本文分为三个局部

  1. 理解 Dart VM 内存调配及销毁原理
  2. 通过 Observatory 工具剖析内存透露,缩小不必要的内存占用
  3. Flutter 中常见内存透露场景有哪些,如何在业务利用中防止踩坑

Dart VM 内存调配及销毁原理

DartVM 的垃圾回收机制分两个阶段,新生代(New Generation)和老年代(Old Generation)。

新生代用来存储生命周期较短的对象,由两个内存空间组成,Active 内存空间用来调配新对象,inActive 内存空间用来作为备用空间,DartVM 的内存调配策略非常简单,创建对象时只须要在现有堆上挪动指针,内存增长始终是线形的,省去了查找可用内存段的过程。每个 Isolate 有本人独立的 Heap,相互之间无奈共享内存,这样能够实现无锁的疾速调配。

一旦 Active 的内存空间被填满,垃圾回收器会从根对象开始遍历查看查看所有对象的援用状态,没有被援用的对象标记为 dead 状态,非 dead 状态的对象在下次内存回收事件中会被复制到 inActive 内存空间,革除 Active 内存空间,最初 Active 和 inActive 内存空间状态调换。

当对象达到肯定的生命周期,会被移到老年代内存空间治理,这种垃圾收集策略有两个阶段,1. 首先遍历对象图,并标记仍在应用的对象。2. 扫描整个内存,并回收任何未标记的对象,而后革除所有标记。这种内存清理的频率较低,并且在扫描回收阶段须要暂停 Dart runtime,回收老本较高,比拟适宜 Flutter 中大量 StatelessWidget 的模式(大部分都寄存在新生代)。

另外,当 engine 检测到利用是 idle 状态并且没有用户交互的时候会发送告诉垃圾收集器开始清理内存,最小化对性能的影响。这些策略让 Dart 的内存调配和回收都十分高效。

Android 和 IOS 中都存在强援用弱援用的概念,区别在于一个对象具备强 / 弱援用,零碎会不会开释该对象占用的内存空间,Dart 并没有弱援用的概念,然而有个特例 Expando,它会以弱援用的形式持有 key,相当于一个弱利用的 map,感兴趣的能够理解下。

Dart VM 借鉴了很多 JVM 的思路,Dart 中产生内存泄露的形式也和 Java 相似,Java 中很多排查内存泄露的思路和避免内存泄露的办法应该也能够借鉴过去。Android 能够通过 Profile 和 LeakMemory 等工具检测 app 中的内存透露,Flutter 如何检测呢?能够应用 Observatory 或者 DevTools。

通过 Observatory 剖析内存透露

Observatory 是官网提供的调试工具,通过 dart vm 获取运行时信息,通过它咱们能够剖析一系列性能相干数据,例如 app 耗时统计,代码覆盖率等,这里咱们重点介绍内存相干的调试工具。(DevTools 也能够用来调试剖析性能数据,它是在 Observatory 层做了一层封装,然而目前还是 beta 版本)。

上面咱们用闲鱼中的理论例子介绍下如何应用 Observatory 查看看 Dart VM 内存应用状况,留神所有对于性能的剖析要在 Profile 模式下进行。

  1. 关上 Observatory URL 的 Web 页面。

运行 app,在控制台中查找相似输入日志 listening on ws://127.0.0.1:64673/hXsWR_ZOsGk=/ws,示意以后连贯的 VM 地址,输出到浏览器就能够看到 Observatory 主界面,显示了 dart vm 一些根底信息,具体应用办法能够参考 官网文档,这里不再详细描述,咱们重点关注右下角的 allocation profile 选项。

  1. 点击右下角 allocation profile 选项后,操作 app 进入想要剖析的 Flutter 页操作,退出该页面,点击页面右上角 GC 按钮触发手动 GC,查看 Class,发现有局部 DX Class 内存占用,这类 class 本应该只有在指标剖析页会呈现,退出指标剖析也后手动 GC 会被齐全开释,然而这里任然能看到相干内存占用,阐明产生了内存透露。

  1. 点击对应 class 查看具体利用实例,点击对应实例进入查看援用门路,就能找到没有导致开释的援用变量,联合业务代码具体分析,就能发现透露的源头。

这里有一点须要留神,Observatory 显示的 Dart VM 占用的内存信息要远远小于 Android Profile/Xcode 检测出的内存大小,因为存在局部只有零碎工具能检测出的内存区块,例如一些齐全不依赖于 DartVM 的 skia 对象,并且 layer 在 engine 中创立时并不能明确晓得大小,所以采纳虚构近似值代替。

//engine/lib/ui/painting/engine_layer.cc
...
size_t EngineLayer::GetAllocationSize() {
  // Provide an approximation of the total memory impact of this object to the
  // Dart GC.  The ContainerLayer may hold references to a tree of other layers,
  // which in turn may contain Skia objects.
  return 3000;
};

上面咱们总结了几种常见内存透露的场景,在 Java 中都能够一一对应找到相似的场景,大家在业务开发中留神防止

常见内存问题

  1. 未勾销注册或回调导致内存泄露

示例代码:

class DownloadManager extends Object {
  ......
  abstract class DownloadListener {void completed(DXTemplateItem item);
      void failed(DXTemplateItem item, String errorInfo);
  }
  static List<DownloadListener> listenerList = List();
  static void downloadSingleTemplate(DXTemplateItem template, DownloadListener listener) async {listenerList.add(listener);
        ...
  }
...

批改办法:手动勾销注册或回调

  // 移除
  static void removeDownloadListener(DownloadListener listener) {if (listener != null && listenerList != null && listenerList.contains(listener)) {listenerList.remove(listener);
    }
  }
  1. 资源未敞开或开释导致内存泄露,例如 ImageStream 的图片资源有没有被失常敞开导致的内存透露。

问题代码:

void _resolveImage() {
    final ImageStream newStream =
    widget.image.resolve(createLocalImageConfiguration(context));
    assert(newStream != null);
    _updateSourceStream(newStream);
  }

批改办法:在图片组件被销毁时正确开释资源

@override
  void dispose() {
    ...
    _imageInfo.image.dispose();
    _imageInfo = null;

    super.dispose();}
  1. PlatformAssetBundle().loadString 通过 asset 读取 String 内容会始终缓存读取的内容,造成内存无奈开释

问题代码:

/// 通过 asset 读取 Json
Future<Map<String, dynamic>> loadJsonAsset(String assetPath) async {_rootBundle ??= PlatformAssetBundle();
  final String jsonStr = await _rootBundle.loadString(assetPath);
  return json.decode(jsonStr);
}

批改办法:

/// 通过 asset 读取 Json
Future<Map<String, dynamic>> loadJsonAsset(String assetPath) async {_rootBundle ??= PlatformAssetBundle();
  final String jsonStr = await _rootBundle.loadString(assetPath, cache: false);
  return json.decode(jsonStr);
}

PlatformAssetBundle 继承于 CachingAssetBundle,会在 app 整个生命周期中缓存读取的数据,如果不是须要频繁拜访的话倡议 cache 参数设置为 false

  1. 另外很多同学有反馈过 Flutter 带图片的长列表滑动会造成内存始终上涨,flutter 在 1.17 版本对这个问题做了优化,具体改变能够参考 pr 14265

以上内容介绍了闲鱼在实践中遇到的 Flutter 内存问题解决思路,给出了内存透露定位办法。优化后能在肯定水平上减小内存压力,防止不必要的内存占用。闲鱼在内存优化的方向上还有很多须要持续摸索的中央,正在做的包含对图片库的缓存革新,layer 层内存检测工具等等,朝着一直优化 flutter 性能体验致力。

退出移动版