Skip to content

Flutter 图片加载 ​

回到总览:图片加载 / Image Loading引擎原理对照:02-图片加载方式总览与原理(Android Glide/Coil/Fresco 内部机制) 横向对比:03-图片加载对比(六类概念/考点/优势/性能)

1. 实现原理 ​

Flutter 不用 Android 的 Bitmap/ImageView,而是走自绘引擎 dart:ui:图片经 Codec 在 Dart 侧解码,结果为 ui.Image,再交给 Image Widget 绘制。核心是 ImageProvider + ImageCache 两层。

1.1 加载链路:ImageProvider.resolve → ImageCache 命中短路 ​

text
Image(...) / Image.network(url)
   └─ ImageProvider.resolve(configuration, size)
        ├─ 1) ImageCache 查 key(live completer 命中?)→ 直接复用 ImageStreamCompleter(跳过解码)
        ├─ 2) 未命中 → 调 loadImage()/loadBuffer():
        │        NetworkImage: http 取字节 → ui.instantiateImageCodec(buffer) → Codec.getNextFrame()
        │        AssetImage   : 从 asset bundle 读字节 → 解码(变体目录选择逻辑、固有逻辑尺寸计算与 GPU 放大渲染致模糊,详见 12/01 §三.1)
        │   产出 ImageStreamCompleter(MultiFrameImageStreamCompleter 处理动图)
        └─ 3) completer 向 ImageCache 登记;Image widget 订阅 ImageStream,拿到 ImageInfo 后绘制

Flutter 图片加载链路:resolve 与 ImageCache 命中短路

图下备注:ImageCache 命中(同 key)→ 复用 completer、跳过网络/解码;未命中 → 取字节 + instantiateImageCodec 解码 → 登记缓存 → 绘制。双限:1000 张 / ~100MB。

1.2 ImageCache:Dart 侧的内存 LRU ​

  • Flutter 全局单例(PaintingBinding.instance.imageCache),绑定 root Isolate;多 Isolate 不共享。
  • 双限:按「数量」maximumSize(默认 1000)和「字节」maximumSizeBytes(随版本调整,约 100MB)淘汰,二者先到先逐出。
  • 存两类对象:ImageStreamCompleter(正在解码/已就绪的流,避免重复发起)和已解码的 ImageInfo。
  • evict(key) 手动清;列表滚走后用不上可主动 evict 控内存。

1.3 CachedNetworkImage:在 ImageCache 之外加了「磁盘缓存」 ​

ImageCache 只管内存。CachedNetworkImage(基于 flutter_cache_manager)额外把网络字节存到设备磁盘,App 重启后仍可命中,且带占位/进度/失败回调:

text
CachedNetworkImage(url)
   └─ CachedNetworkImageProvider
        ├─ CacheManager: 磁盘查 key(文件存在?)→ 命中直接读本地字节
        ├─ 未命中 → http 下载 → 写磁盘 → 交给 ImageProvider 解码进 ImageCache

1.4 降采样:cacheWidth / cacheHeight(对应 Android inBitmap/inSampleSize) ​

Image 的 cacheWidth/cacheHeight 会经 ResizeImage 在解码阶段把图缩到目标像素,内存与解码开销随尺寸平方下降——和 Android 按 ImageView 尺寸降采样同一思路。

1.5 CachedNetworkImage 的已知限制(社区共识 + 公司实测) ​

先看图片引擎的"内存治理"全貌(与 Android 三引擎同一套思想,Flutter 侧对应实现见六层方案 §7):

图片引擎的加载四阶段与三大回收机制

图下备注:取字节 → 解码 → 缓存 → 绘制四阶段;回收靠复用池(BitmapPool)、两级缓存(强 LRU + 弱引用)、压力感知(onTrimMemory / 生命周期取消)三件套。Fresco 额外用编码态缓存 + CloseableReference 引用计数。

CachedNetworkImage(基于 flutter_cache_manager)解决了「磁盘缓存」这一个痛点,但有三层"管不到":

限制表现对策
取消不彻底底层 dart:io HttpClient 无 CancelToken;widget 销毁 / evictFromCache 只丢弃结果,底层 HTTP 连接继续下载完(弱网大图会默默跑完)自管下载(带取消);或换 extended_image(有 CancellationToken)
缓存移除 ≠ 释放evictFromCache() / emptyCache() 只删磁盘文件;内存 ImageCache 是另一套(LRU + live 强引用),不调 imageCache.evict(key) / clearLiveImages() 就还占内存清缓存必须磁盘 + 内存两套都清
大图必须自己降采样不设 cacheWidth/memCacheWidth 就整图解码进 Dart 堆,峰值高(Flutter 无 native 池)列表大图一律 memCacheWidth

公司实测(12.02-leshangquan-delivery / 12.04-xiaojinka):使用层优化已做——useOldImageOnUrlChange: true(URL 变化不闪旧图)、cacheKey 显式指定、fadeInDuration/fadeOutDuration: 10ms(防滚动闪烁)、filterQuality: FilterQuality.medium、progressIndicatorBuilder / errorWidget 占位兜底。缺失层:列表大图未设 memCacheWidth(171dp 卡片整图解码)、无自定义 CacheManager(取消/清理不可控)、无 imageCache 回收——"用了库 ≠ 内存治理好了",见下文六层方案。

2. 特点与优势 ​

  • Flutter 自绘 vs Android 引擎(Glide/Coil/Fresco) :Flutter 走 dart:ui Codec,bitmap 在 Dart VM 堆;Android 走 native Bitmap(Glide BitmapPool/inBitmap 复用、Fresco native CloseableReference)。两者内存归属、复用机制、缓存 API 都不互通——所以 Flutter 没有 Glide 那种 native 内存池,超大图只能靠 cacheWidth 降采样控内存。
  • Image.network vs CachedNetworkImage:前者只进 ImageCache(内存),重启/清内存后必重新下载;后者多一层磁盘缓存 + 占位/进度/失败 UI,网络图场景几乎都该用它。
  • Image 体系 vs 手动解码:除非来源特殊(自定义协议、需逐帧处理),否则用 ImageProvider 体系,缓存与生命周期由框架托管。
  • 跨端一致性:同一套 dart:ui 解码,Android/iOS/Web 行为一致;这是相对「每端一套图库」的最大优势。

3. 用法 ​

1. 基础网络图(Image.network) ​

dart
Image.network(
  'https://example.com/a.jpg',
  width: 120,
  height: 120,
  fit: BoxFit.cover,
)

2. 带占位/进度/失败的 CachedNetworkImage ​

dart
CachedNetworkImage(
  imageUrl: 'https://example.com/a.jpg',
  placeholder: (ctx, url) => const CircularProgressIndicator(),
  errorWidget: (ctx, url, err) => const Icon(Icons.error),
  fadeInDuration: const Duration(milliseconds: 300),
  memCacheWidth: 240, // 解码降采样,等价于 cacheWidth
)

3. 预加载(避免列表首次卡顿) ​

dart
// 在 build 前预热
precacheImage(const NetworkImage('https://example.com/a.jpg'), context);

4. 解码降采样(cacheWidth) ​

dart
Image.network(
  'https://example.com/big.jpg',
  cacheWidth: 400, // 解码到 400px 宽,内存随面积下降
  cacheHeight: 300,
)

5. 自定义 ImageProvider(特殊来源) ​

dart
class MyProvider extends ImageProvider<MyProvider> {
  final String token;
  const MyProvider(this.token);
  @override
  Future<MyProvider> obtainKey(ImageConfiguration c) async => this;
  @override
  ImageStreamCompleter loadImage(MyProvider key, ImageDecoderCallback decode) {
    // 自行取字节 → decode(...) → 返回 completer
    // 与 02 中「自定义委托」同理:把「取字节+解码」逻辑封装后交给框架调度
  }
}

4. 考点 ​

  • ImageCache 双限淘汰:数量 maximumSize 与字节 maximumSizeBytes 哪个先到先逐出;列表大图不 evict 会撑满内存。
  • ImageCache 绑定 root Isolate:新 Isolate 不共享缓存,跨 Isolate 加载不命中彼此。
  • CachedNetworkImage 与 ImageCache 是两层:前者磁盘、后者内存;清内存缓存不代表清磁盘,反之亦然。
  • cacheWidth/cacheHeight 在解码阶段生效:控制的是解码目标尺寸(内存),不是显示尺寸(width/height);要省内存必须设 cacheWidth。
  • precacheImage 的 context 泄漏:传入的 BuildContext 在预热完成前被销毁会抛异常,列表里注意生命周期。
  • Flutter vs Android 内存世界观不同:面试常问「Flutter 有没有 BitmapPool」——没有,靠 cacheWidth 降采样 + ImageCache 双限。

5. 性能对比(与 Android 三引擎横向) ​

维度GlideCoilFrescoCompose 原生Flutter
内存友好(超大图)545(native)43(靠 cacheWidth)
包体积25154(需引 cached_network_image)
解码速度44444
跨端一致13(KMP)135
生命周期绑定5555(组合)4(Widget 树)
易用/生态45254

分数为 1–5 相对排序(5 最好),用于直觉,非基准测试数字。结论:Flutter 胜在跨端一致、API 简洁;短板是无 native 内存池,超大图必须显式 cacheWidth 降采样。

6. 对比分析举例 ​

例:同一个网络图「首次加载 vs 二次进入」的路径差异

  • 首次:CachedNetworkImage → 磁盘 miss → http 下载 → 写磁盘 → ImageProvider 解码进 ImageCache(内存)→ 绘制。
  • 二次(同 session):ImageCache 内存命中 → 跳过下载与解码。
  • 二次(杀进程重开):ImageCache 清空,但磁盘命中 → 读本地字节 → 解码进内存,省一次网络。

例:列表快速滚动为什么要用 CachedNetworkImage + 适时 evict

  • 滚得快时每个 item 都发起下载会瞬时打满带宽与 ImageCache;CachedNetworkImage 的磁盘层让滚回的 item 直接读本地,且 ImageCache 双限自动淘汰离屏图,避免 OOM。

7. 大列表 + 大图:六层方案(实战) ​

对应 Android 引擎四段(取字节→解码→缓存→生命周期),Flutter 无 native 内存池,降采样是硬要求。每层解决一个资源约束、可单独验证。

Flutter 大列表 + 大图:六层方案

  • L1 降采样(硬要求) :cacheWidth/memCacheWidth 按显示尺寸解码,整图进 Dart 堆必炸。
  • L2 视口驱动:ListView.builder 懒构建 + visibility_detector 可见才发起请求,不可见不下载不解码。
  • L3 预加载:对 index±3 即将可见项 precacheImage,避免滚动到才请求导致白屏闪烁。
  • L4 双层缓存:ImageCache(默认 1000 张 / 约 100MB 双限)+ CachedNetworkImage 磁盘 LRU——回滚不重下、离线可用。
  • L5 回收:imageCache.clear() / clearLiveImages() + WidgetsBindingObserver 监听内存警告(对应 Android onTrimMemory)。
  • L6 超大图:全景 / 地图用 interactive_image 类 tile 分块(等价 BitmapRegionDecoder);普通大图服务端下发"缩略图 + 原图"两档。

图下备注:L1~L6 每层都是"单点可验证"——L1 看 getByteCount 等价的内存下降,L2 看请求是否按视口触发,L4 看二次进入是否命中,L5 看内存警告后 imageCache.currentSizeBytes 回落。不要试图一次全上,先补 L1 + L4(收益最大),再补 L2/L3 优化体验。

  • 渲染层补充(L7,可选) :列表海量缩略图可把 filterQuality 降到 FilterQuality.low / none,牺牲极微弱的边缘平滑换滚动流畅(公司实测用 FilterQuality.medium 平衡);图文混排里给含大图的子树包 RepaintBoundary,避免其他小组件动画连带重绘大图(机制详见 07/11 Layer Tree)。

复习检查题 ​

  1. Flutter 的 ImageCache 和 Android 的 BitmapPool 是一回事吗?

    答:不是。ImageCache 是 Dart 侧的 LRU(按数量+字节双限),存 ui.Image;BitmapPool 是 Glide 在 native 堆的 Bitmap 复用池。Flutter 没有 native 复用池,超大图只能靠 cacheWidth 降采样控内存。

  2. CachedNetworkImage 清了 ImageCache 还会重新下载吗?

    答:不一定。它有两层:清 ImageCache 只清内存,磁盘缓存还在就不重新下载;只有磁盘也失效才会再 http 下载。反过来,evictFromCache() 只删磁盘文件,内存里的 ImageCache 条目还要 imageCache.evict(key) / clearLiveImages() 才释放。

  3. 大列表里 CachedNetworkImage 不设 memCacheWidth 会怎样?

    答:整图解码进 Dart 堆(Flutter 无 native 池),列表大图会撑爆内存;必须按显示尺寸设 memCacheWidth/cacheWidth 降采样(六层方案 L1)。

对应实验 ​

  • labs/flutter/host/lib/topics/image_loading/cached_network_image_demo_page.dart —— 可跑的 CachedNetworkImage 演示(网络图 + 占位/进度/失败 + cacheWidth 降采样),见 lab README。

站点构建时间:2026/8/24 23:43:17