Flutter 图片解码与缓存管理研究

简介: 图片解码和缓存管理是渲染引擎的一个重要模块,这是因为图片解码的耗时很长,特别是对于设计为跨平台的通用渲染引擎来说,依赖于CPU来做图片解码,会消耗大量的CPU时间,并且图片解码后占用的内存很大,一张 1024x1024 分辨率的图片解码后就需要 4M 内存(除非硬件支持实时生成无损压缩格式纹理,通常这也不在通用渲染引擎的考虑范围之内)。所以一个设计良好的图片解码和缓存管理模块需要平衡很多不同的因素

图片解码和缓存管理是渲染引擎的一个重要模块,这是因为图片解码的耗时很长,特别是对于设计为跨平台的通用渲染引擎来说,依赖于CPU来做图片解码,会消耗大量的CPU时间,并且图片解码后占用的内存很大,一张 1024x1024 分辨率的图片解码后就需要 4M 内存(除非硬件支持实时生成无损压缩格式纹理,通常这也不在通用渲染引擎的考虑范围之内)。所以一个设计良好的图片解码和缓存管理模块需要平衡很多不同的因素,包括内存占用,CPU占用,解码任务调度的及时性等。

在对 Flutter 的图片解码和缓存管理模块进行研究后,发现它跟 Chromium 有很大的差别。一方面它实现比较简单,给予了应用更直接的控制权,引擎本身只提供了最基本的支持,更契合 Native UI 的实际使用场景,另外一方面因为引擎本身缺少控制权,如果应用生成的 UI 界面较为极端,可能会导致比较灾难性的结果。

在这篇文章,我会先对 Flutter 的图片解码和缓存管理机制进行说明。然后再说明这种机制存在的一些问题。

Image Widget and Provider

class Image extends StatefulWidget {
  ...
  /// The image to display.
  final ImageProvider image;
}

abstract class NetworkImage extends ImageProvider<NetworkImage> {
  ...
}

Flutter 通过 Image.asset,Image.file,Image.network 等方法创建一个 Image Widget 来显示图片,方法名字说明了图片数据的来源,他们实际上是为 Image Widget 提供了不同的 ImageProvider,比如说 Image.network 创建的 Image Widget,它的 ImageProvider 就是 NetworkImage。因为 Image Widget 是一个 StatefulWidget,所以它核心的状态处理逻辑代码是位于 _ImageState 对象中,由它来创建真正显示图片的 RawImage Widget。

class _ImageState extends State<Image> with WidgetsBindingObserver {
  ...
  @override
  void didChangeDependencies() {
    _updateInvertColors();
    _resolveImage();

    if (TickerMode.of(context))
      _listenToStream();
    else
      _stopListeningToStream();

    super.didChangeDependencies();
  }

  void _resolveImage() {
    final ScrollAwareImageProvider provider = ScrollAwareImageProvider<dynamic>(
      context: _scrollAwareContext,
      imageProvider: widget.image,
    );
    final ImageStream newStream =
      provider.resolve(createLocalImageConfiguration(
        context,
        size: widget.width != null && widget.height != null ? Size(widget.width, widget.height) : null,
      ));
    assert(newStream != null);
    _updateSourceStream(newStream);
  }

  void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {
    setState(() {
      _imageInfo = imageInfo;
      _loadingProgress = null;
      _frameNumber = _frameNumber == null ? 0 : _frameNumber + 1;
      _wasSynchronouslyLoaded |= synchronousCall;
    });
  }
}

ScrollAwareImageProvider 是新版本新增的优化,它包装了最初的 ImageProvider,用来避免在快速滚动的过程中加载图片,也就是说快速滚动过程新增的 Image Widget,它加载图片的时机会被延迟,如果它在滚动过程中移除屏幕然后被移除,就完全不会触发加载。

当 Image Widget 被加入到 UI 的 Widget 树时,Flutter 就会调用 _ImageState.didChangeDependencies,然后 _ImageState._resolveImage 被调用,最后调用 ImageProvider.resolve 来加载图片。ImageProvider.resolve 触发了一连串的事情发生,它会先在 ImageCache 中生成 Entry,然后开始加载数据(异步方法,由 ImageProvider 的子类提供),加载完数据后生成相应的 Codec 开始请求解码(异步方法,由 Native Engine 提供),解码完成后最终通知 _ImageState._handleImageFrame 改变状态,产生新的 child Widget 显示图片。

Flutter 单帧图片的解码是运行在 worker 线程池(可以并发),解码后的 GPU 纹理上传是 io 线程,多帧图片的解码和纹理上传都是在 io 线程。

也就是说:

  1. Flutter 图片解码的调度和图片缓存的管理都在 Widget 层,由 Image Widget 关联的 _ImageState 对象和 ImageProvider 对象负责;
  2. 图片缓存的实现是 ImageCache 对象,通过 PaintingBinding.instance.imageCache 访问,ImageProvider 封装了 ImageCache 的访问;
  3. 当 Image Widget 被加入 Widget 树,就会触发图片的加载,加载完后就会自动请求解码,加载和解码是连在一起不可分割的;
  4. 解码完成后 Image Widget 才会产生 RawImage 作为 child Widget 真正显示图片。

ImageCache 图片缓存

class ImageCache {
  final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
  final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};
  final Map<Object, _LiveImage> _liveImages = <Object, _LiveImage>{};
  
  
  ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(),
      {ImageErrorListener onError}) {
    ...
  }
}

当 ImageProvide.resolve 被调用时,它会去调用 ImageCache.putIfAbsent 生成 Cache Entry(Key 由 ImageProvider 产生),返回一个 ImageStreamCompleter 对象用于监听图片加载和解码完成的情况,如果已经有缓存的 Entry,则直接返回。

ImageCache 实际上有三个 Pool,分别是 Pending,Cache 和 Live Pool,一个新的 Entry 一开始会被加入到这三个 Pool 中。Pending Pool 用来跟踪正在加载和解码的图片,当图片加载和解码完成后,ImageCache 会自动移除 Pending Pool 相应的 Entry。Live Pool 是用来跟踪使用中的图片,当 Image Widget 移除或者更换图片,或者 Image Widget 自身被移除,ImageCache 会从 Live Pool 移除相应的 Entry。如果图片缓存的数量和内存占用大小没有超过 ImageCache 的上限,Cache Pool 就会一直保留 Cache Entry,如果超过则按 LRU 进行释放。只有 ImageCache 从所有 Pool 都释放了同一个图片的 Entry,该图片解码后生成的纹理内存才会真正被释放

我们可以通过一个实际的场景来说明 ImageCache 的处理逻辑。假设 ImageCache 缓存的限制是 100M(100M 也是 Flutter 的默认值),我们的 UI 陆续加入 200 个 Image Widget,每个 Image Widget 显示一个 512x512 的图片,每个图片解码后的纹理内存占用为 1M。

  1. 当 UI 加入 100 个 Image Widget 的时候,Live Pool 和 Cache Pool 都有对应 100 个 Entey,假设图片都已经加载和解码完毕,Pending Pool 里面的 Entry 被全部移除,当前总的图片纹理缓存占用为 100M;
  2. 当加入 101 个 Image Widget,并且图片加载和解码完毕的时候,Live Pool 里面有 101 个 Entry,但是 Cache Pool 因为超过上限,最初的 Entry 被移除,只保留了后面 100 个 Entry,当前总的图片纹理缓存占用为 101M;
  3. 当加入 200 个 Image Widget,并且图片加载和解码完毕的时候,Live Pool 里面有 200 个 Entry,但是 Cache Pool 因为超过上限,最初的 100 个 Entry 被移除,只保留了后面 100 个 Entry,当前总的图片纹理缓存占用为 200M;
  4. 我们移除这 200 个 Image Widget,Live Pool 的 Entry 被完全移除,但是 Cache Pool 没有超过上限,仍然保留,当前总的图片纹理缓存占用为 100M;
  5. 我们使用后 100 张同样的图片重新加入 100 个 Image Widget,因为图片已经存在于 Cache Pool,所以不需要重新加载和解码,ImageCache 会从 Cache Pool 里面取出对应 Entry,并且重新在 Live Pool 生成对应的 Entry,最后 Live Pool 和 Cache Pool 都包含同样的 100 个 Entry,当前总的图片纹理缓存占用为 100M;
  6. 我们继续使用前 100 张图片再加入 100 个 Image Widget,因为 Cache Pool 已经移除了对应的 Entry,所以需要重新加载和解码,最终 Live Pool 包含了 200 个 Entry,Cache Pool 包含 100 对应前 100 张图片的 Entry,当前总的图片纹理缓存占用为 200M;
  7. 我们再次移除所有的 Image Widget,并且手动设置 ImageCache 的内存上限为 0,这样 ImageCache 会移除 Live Pool 和 Cache Pool 的所有 Entry,当前总的图片纹理缓存占用为 0M;

Flutter 图片缓存设计的一些问题

应该说 Flutter 的图片缓存设计还是比较契合 Native UI 的使用场景的,但是对于一些设计比较糟糕的 UI,或者是自动生成的类 Web 的长页面,这样的设计可能会造成一些灾难性的后果。

  1. Flutter 解码的时机非常靠前,如果一次性加入大量的 Image Widget 对象,会马上产生相应数量的加载和解码任务,这可能造成系统较为严重的阻塞,并且部分 Image Widget 实际上可能距离可见区域较远,解码后产生的纹理暂时不会被使用,这造成了内存浪费;
  2. ImageCache 实际上是没有真正封顶的(Live Pool 是无上限的),如果当前的 Widget 树同时包含了大量的 Image Widget,内存峰值可能会非常夸张,很容易造成 OOM;
  3. ImageCache 是在 Framework 层的实现而不是 Engine 层,它的实例由 Widget 层产生,通过 PaintingBinding.instance.imageCache 访问,这意味着每个 FlutterView,每个 Root Isolate 都有一个不同的 ImageCache,如果是混合应用,同时展现多个 FlutterView,不但 ImageCache 的 Live Pool 没法控制,Cache Pool 也会处于叠加的状态,导致内存的峰值会更难以控制;

目前已经有不少尝试是先生成 DOM 树,然后再用不同的后端将 DOM 树转换成适合不同渲染引擎的产物,交给对应的引擎去渲染。比如生成真正的 Web DOM 交给 Web 引擎渲染,或者生成 Flutter Widget 树交给 Flutter 渲染。这种代码自动生成的 Widget 树可能存在的一个问题就是可能会一次性生成大量的 Widget,并且同时加入 Widget 树。

目录
相关文章
|
4月前
|
存储 JavaScript 前端开发
盘点主流 Flutter 状态管理库2024
状态管理是每个应用不可缺少的,本文将会盘点下主流的状态管理包。
312 2
盘点主流 Flutter 状态管理库2024
|
30天前
|
缓存
Flutter Image从网络加载图片刷新、强制重新渲染
Flutter Image从网络加载图片刷新、强制重新渲染
46 1
|
2月前
|
容器
flutter 布局管理【详解】
flutter 布局管理【详解】
25 3
|
2月前
|
Shell Android开发 Python
Flutter如何正确使用图片资源
Flutter如何正确使用图片资源
19 0
|
2月前
Flutter-自定义图片3D画廊
Flutter-自定义图片3D画廊
61 0
|
2月前
flutter的状态管理学习
flutter的状态管理学习
|
4月前
flutter 引用图片资源遇到的问题
flutter 引用图片资源遇到的问题
39 1
|
4月前
|
存储 缓存 前端开发
【Flutter前端技术开发专栏】Flutter中的图片加载与缓存优化
【4月更文挑战第30天】本文探讨了 Flutter 中如何优化图片加载与缓存,以提升移动应用性能。通过使用图片占位符、压缩裁剪、缓存策略(如`cached_network_image`插件)以及异步加载和预加载图片,可以显著加快加载速度。此外,利用`FadeInImage`、`FutureBuilder`和图片库等工具,能进一步改善用户体验。优化图片处理是提升Flutter应用效率的关键,本文为开发者提供了实用指导。
391 0
【Flutter前端技术开发专栏】Flutter中的图片加载与缓存优化
|
4月前
|
前端开发 开发者 UED
【Flutter前端技术开发专栏】Flutter中的图标、字体与样式管理
【4月更文挑战第30天】本文介绍了在Flutter中管理图标、字体和样式的做法。Flutter提供`Icons`类用于内置矢量图标,支持第三方图标库如FontAwesome。自定义字体可通过添加字体文件至`assets`目录并配置`pubspec.yaml`,然后使用`TextStyle`设置。借助`ThemeData`,开发者能统一管理应用主题样式,局部样式可覆盖全局。通过集中管理样式,提升代码复用性和应用一致性。
130 0
【Flutter前端技术开发专栏】Flutter中的图标、字体与样式管理
|
4月前
|
存储 JavaScript 前端开发
【Flutter 前端技术开发专栏】Flutter 中的状态管理框架(如 Provider、Redux 等)
【4月更文挑战第30天】本文探讨了 Flutter 开发中的状态管理,重点介绍了 Provider 和 Redux 两种框架。Provider 以其简单易用性适合初学者和小项目,而 Redux 则适用于大型复杂应用,保证状态一致性。此外,还提到了 Riverpod 和 BLoC 等其他框架。选择框架时要考虑项目规模、团队技术水平和个人偏好。文章通过购物车应用示例展示了不同框架的使用,并展望了状态管理框架的未来发展。
127 0
【Flutter 前端技术开发专栏】Flutter 中的状态管理框架(如 Provider、Redux 等)