Flutter源码分析【一】 - Image 组件自带的性能优化

70 阅读4分钟

框架涉及 image 组件的代码:

packages/flutter/lib/src/widgets/image.dart

packages/flutter/lib/src/painting/image_cache.dart
packages/flutter/lib/src/painting/image_decoder.dart
packages/flutter/lib/src/painting/image_stream.dart
packages/flutter/lib/src/painting/image_provider.dart
packages/flutter/lib/src/painting/image_resolution.dart

1. 全局图片缓存

PaintingBinding.instance.imageCache

  1. image组件中的使用缓存
// 使用全局图片缓存来避免重复加载
final ImageStreamCompleter? completer = PaintingBinding.instance.imageCache.putIfAbsent(
  key,
  () => loadImage(key, PaintingBinding.instance.instantiateImageCodecWithSize),
  onError: handleError,
);

2. imagecache 主要维护三个结构

// _PendingImage 待加载图片
final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
// _CachedImage 已加载图片。LRU Cache
final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};
// _liveImages 活跃中的图片(正在被界面使用)
final Map<Object, _LiveImage> _liveImages = <Object, _LiveImage>{};

3. imagecahce 缓存数据结构的具体内容:

  • 缓存的不是原始图片字节数据,而是 ImageStreamCompleter 对象
abstract class _CachedImageBase {
  final ImageStreamCompleter completer;  // 图片流完成器
  int? sizeBytes;                         // 图片大小(字节)
  ImageStreamCompleterHandle? handle;     // 保活句柄
}
class _PendingImage {
  final ImageStreamCompleter completer;
  final ImageStreamListener listener;
}

class _CachedImage extends _CachedImageBase {}

class _LiveImage extends _CachedImageBase {
  late VoidCallback _handleRemove;
}

4. 使用同个 imageStreamCompleter可以共享哪些东西:

  • 避免重复请求:同个网络请求/文件读取 io
  • 减少图片解码开销
  • 图片数据只有一份内存占用
  • 共享底层 gpu 纹理,提升渲性能

gpu 纹理共享的实现:imageStreamCompleter 产物是 ui.image,ui.image clone 时,只有图片信息进行了复制,

/// A [dart:ui.Image] object with its corresponding scale.
///
/// ImageInfo objects are used by [ImageStream] objects to represent the
/// actual data of the image once it has been obtained.
///
///  * To share the [ImageInfo] or [ui.Image] between objects, use the [clone] method,
/// which will not clone the entire underlying image, but only reference to it and information about it.
///  * After passing a [ui.Image] or [ImageInfo] reference to another object,
/// release the reference.
typedef ImageListener = void Function(ImageInfo image, bool synchronousCall);

class ImageInfo {
  final ui.Image image;

  ImageInfo clone() {
    return ImageInfo(
      image: image.clone(),
      scale: scale,
      debugLabel: debugLabel,
    );
  }
}


class ImageStreamListener {
  final ImageListener onImage;
}

5. imagecache 的索引,根据图片类型的不同,有以下几种

// 1. AssetBundleImageKey,用于 AssetImage 和 ExactAssetImage
class AssetBundleImageKey {
  final AssetBundle bundle;
  final String name;        // 资源名称
  final double scale;       // 缩放比例
}


// 2. ResizeImageKey
class ResizeImageKey {
  final Object _providerCacheKey;      // 原始 ImageProvider 的 key
  final ResizeImagePolicy _policy;     // 调整策略
  final int? _width;                   // 目标宽度
  final int? _height;                  // 目标高度
  final bool _allowUpscaling;          // 是否允许放大
}


// 3. NetworkImage 实例本身
class NetworkImage {
  Future<NetworkImage> obtainKey(ImageConfiguration configuration) {
    return SynchronousFuture<NetworkImage>(this);
  }

  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType) {
      return false;
    }
    return other is NetworkImage
        && other.url == url
        && other.scale == scale;
  }
}


// 4. FileImage 实例本身
class FileImage {
  Future<FileImage> obtainKey(ImageConfiguration configuration) {
    return SynchronousFuture<FileImage>(this);
  }

  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType) {
      return false;
    }
    return other is FileImage
        && other.file.path == file.path
        && other.scale == scale;
  }
}


// 5. MemoryImage 示例本身
class MemoryImage {
  Future<MemoryImage> obtainKey(ImageConfiguration configuration) {
    return SynchronousFuture<MemoryImage>(this);
  }

  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType) {
      return false;
    }
    return other is MemoryImage
        && other.bytes == bytes
        && other.scale == scale;
  }
}

6. 用NetworkImage举例下命中同一份缓存的情况。key校验代码

@override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType) {
      return false;
    }
    return other is NetworkImage
        && other.url == url
        && other.scale == scale;
  }
  • runtimeType:
    • runtimeType 是 Dart 中所有对象都有的一个属性,它返回对象的运行时类型,这里两个NetworkImage一定是相等的
  • URL: 网络请求的 url
  • scale:图片的缩放

条件:两个NetworkImage对象,url和 scale 相同,比如:

// 这两个实例会命中同一份缓存
final img1 = NetworkImage('https://example.com/image.jpg');
final img2 = NetworkImage('https://example.com/image.jpg', headers: {'Authorization': 'Bearer token'});

// 这两个实例也会命中同一份缓存
final img3 = NetworkImage('https://example.com/image.jpg', headers: {'Custom': 'value1'});
final img4 = NetworkImage('https://example.com/image.jpg', headers: {'Custom': 'value2'});

NetworkImage 使用缓存的优缺点, 其他资源图片可以类比:

  • 性能优化:
    • 避免重复网络请求:相同 URL 的图片只下载一次
    • GPU纹理共享:缓存的是解码后的 ui.Image,可直接渲染
    • 内存管理:通过 LRU 算法和字节大小限制管理内存
  • 潜在问题:
    • 判断时不考虑Headers:不同 headers 的请求会命中同一缓存,这可能导致权限问题
    • 缓存污染:第一次请求的结果会被后续相同 URL 的请求复用

2. 使用缩放尺寸解码

  • 直接基于缩放尺寸进行解码,会比解码后再缩放高效
  • 依赖外部调用者传入缩放宽高,不传入使用默认尺寸

通过 resizeImage 实现

class ResizeImage extends ImageProvider<ResizeImageKey> {
  // 在解码时就指定目标尺寸,而不是在渲染时缩放
  final int? width;
  final int? height;
  final ResizeImagePolicy policy;
  final bool allowUpscaling;

  static ImageProvider<Object> resizeIfNeeded(int? cacheWidth, int? cacheHeight, ImageProvider<Object> provider) {
    if (cacheWidth != null || cacheHeight != null) {
      return ResizeImage(provider, width: cacheWidth, height: cacheHeight);
    }
    return provider;
  }
}

解码时可以指定尺寸

/// Performs the decode process for use in [ImageProvider.loadImage].
///
/// This callback allows decoupling of the `getTargetSize` parameter from
/// implementations of [ImageProvider] that do not expose it.
///
/// See also:
///
///  * [ResizeImage], which uses this to load images at specific sizes.
typedef ImageDecoderCallback = Future<ui.Codec> Function(
  ui.ImmutableBuffer buffer, {
  ui.TargetImageSizeCallback? getTargetSize,
});

typedef TargetImageSizeCallback = TargetImageSize Function(
  int intrinsicWidth,
  int intrinsicHeight,
);

3. 流式加载

异步加载图片数据,避免 UI 阻塞

@nonVirtual
ImageStream resolve(ImageConfiguration configuration) {
  final ImageStream stream = createStream(configuration);
  // 立即返回stream,然后异步填充数据
}