flutter PaintingBinding

flutter 中的 WidgetsFlutterBinding 集成了 GestureBinding、ServicesBinding、SchedulerBinding、PaintingBinding、SemanticsBinding、RendererBinding、WidgetsBinding 等 7 种 Binding,它们都有自己在功能上的划分,其中,PaintingBinding 主要负责的是 flutter 中图片缓存、GPU 着色器预编译等相关的。

首先还是看 initInstance 的实现:

void initInstances() {
  super.initInstances();
  _instance = this;
  _imageCache = createImageCache();
  if (shaderWarmUp != null) {
    shaderWarmUp.execute();
  }
}

这个函数有两个操作,第一步创建 ImageCache,第二步执行 shaderWarmUp。

ImageCache

ImageCache 就是用于图片缓存的,在 ImageProvider 中被使用,它的工作原理也比较简单,内部有两个字典,分别用于缓存 image 和待获取的 image ,它提供 putIfAbsent 负责缓存/取出图片,以及 evict 移除某个缓存,它有一个缓存上限(容量上限和数量上限),超出之后会清理最不常使用的一个/些,直到容量降低到上限以内。

putIfAbsent

ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) {
  assert(key != null);
  assert(loader != null);
  ImageStreamCompleter result = _pendingImages[key]?.completer;
  // Nothing needs to be done because the image hasn't loaded yet.
  if (result != null)
    return result;
  // Remove the provider from the list so that we can move it to the
  // recently used position below.
  final _CachedImage image = _cache.remove(key);
  if (image != null) {
    _cache[key] = image;
    return image.completer;
  }
  try {
    result = loader();
  } catch (error, stackTrace) {
    if (onError != null) {
      onError(error, stackTrace);
      return null;
    } else {
      rethrow;
    }
  }
  void listener(ImageInfo info, bool syncCall) {
    // Images that fail to load don't contribute to cache size.
    final int imageSize = info?.image == null ? 0 : info.image.height * info.image.width * 4;
    final _CachedImage image = _CachedImage(result, imageSize);
    // If the image is bigger than the maximum cache size, and the cache size
    // is not zero, then increase the cache size to the size of the image plus
    // some change.
    if (maximumSizeBytes > 0 && imageSize > maximumSizeBytes) {
      _maximumSizeBytes = imageSize + 1000;
    }
    _currentSizeBytes += imageSize;
    final _PendingImage pendingImage = _pendingImages.remove(key);
    if (pendingImage != null) {
      pendingImage.removeListener();
    }
    _cache[key] = image;
    _checkCacheSize();
  }
  if (maximumSize > 0 && maximumSizeBytes > 0) {
    final ImageStreamListener streamListener = ImageStreamListener(listener);
    _pendingImages[key] = _PendingImage(result, streamListener);
    // Listener is removed in [_PendingImage.removeListener].
    result.addListener(streamListener);
  }
  return result;
}

首先需要明确两点,_pendingImages 存放的是已经准备缓存但是还没有获取到图片内容的 ImageStreamCompleter,_cache 存放的就是已经获取到的图片。

那么在这个函数最初,首先判断 _pendingImages 是否存有这个图片,如果是,那就无需再次添加,直接返回。然后再判断 _cache 中是否存有图片,如果有,返回它的 Completer ,以上就是取缓存的过程。

接着执行 loader 得到 ImageStreamCompleter ,ImageStreamCompleter 内部存有 Image 的具体内容,同时它还是一个简单的通知结构,会在 image 得到的时候通知所有的 listener 执行回调。

接着在函数内部声明了一个回调函数 listener。最后,将 ImageStreamCompleter 加入 _pendingImages 中,并将 listener 加入到 ImageStreamCompleter 中,这个 listener 会在图片数据加载完成时回调,也就是调用 ImageStreamCompleter.setImage 时。

在 listener 中,将 ImageInfo 封装成 _CachedImage 后存到 _cache 中,同时将其从 _pendingImages 中移除。最后会调用 _checkCacheSize,将超出缓存容量的部分移出去。

void _checkCacheSize() {
  while (_currentSizeBytes > _maximumSizeBytes || _cache.length > _maximumSize) {
    final Object key = _cache.keys.first;
    final _CachedImage image = _cache[key];
    _currentSizeBytes -= image.sizeBytes;
    _cache.remove(key);
  }
  assert(_currentSizeBytes >= 0);
  assert(_cache.length <= maximumSize);
  assert(_currentSizeBytes <= maximumSizeBytes);
}

evict

bool evict(Object key) {
  final _PendingImage pendingImage = _pendingImages.remove(key);
  if (pendingImage != null) {
    pendingImage.removeListener();
    return true;
  }
  final _CachedImage image = _cache.remove(key);
  if (image != null) {
    _currentSizeBytes -= image.sizeBytes;
    return true;
  }
  return false;
}

这里就是先后将缓存从 _pendingImages 和 _cache 中移除。

ShaderWarmUp

PaintingBinding 的另一个作用就是预编译着色器,大致的原理就是在这个阶段执行一些 GPU 渲染操作(渲染结果一般不会使用),让 GPU 预先编译某些渲染过程中需要用的着色器脚本,一般来说,以 OpenGL 为例,很多 Program 是可以复用的,只需要第一次进行初始化,包括编译、链接等,后面应该可以直接用,这也应该就是 ShaderWarmUp 存在的原理。在初始化阶段是一般用不到 GPU 的,如果在开始渲染界面的时候才第一次使用 GPU,就需要先进行初始化才能使用,而如果把它的初始化这个阶段移到 flutter 初始化阶段,就达到了“预热”的目的,因为 GPU 的初始化跟 flutter 初始化并不是工作在同一个线程,理论上二者同时初始化会提高效率,如此就可以避免第一次渲染的时候耗时过长等问题。

具体的,就是在 initInstances 函数中执行 shaderWarmUp.execute,

Future<void> execute() async {
  final ui.PictureRecorder recorder = ui.PictureRecorder();
  final ui.Canvas canvas = ui.Canvas(recorder);
  
  await warmUpOnCanvas(canvas);
  
  final ui.Picture picture = recorder.endRecording();
  final TimelineTask shaderWarmUpTask = TimelineTask();
  shaderWarmUpTask.start('Warm-up shader');
  await picture.toImage(size.width.ceil(), size.height.ceil());
  shaderWarmUpTask.finish();
}

创建一个 canvas、在 canvas 上进行绘制操作、得到 picture、最后将 picture 转换成 Image,一个比较完整的 GPU 操作了,我们可以在 warmUpOnCanvas 中进行自由绘制,一般来说绘制的内容不同,GPU 进行渲染的时候所需的着色器也会不同,所以如何能够更好地达到预编译的目的,也是需要考究的。

flutter 中有一个默认的实现,大致就是用了几种不同的画笔(style 与 isAntiAlias 进行组合),画了几个图形(矩形、圆形、path)等,这么做应该是按照穷举的思想,尽可能多用几种不同的渲染方式,可能会效果比较好?

当然如果开发者觉得这种不符合需求,也可以自己实现一个 ShaderWarmUp,在 runApp 之前将其赋值给 PaintingBinding.shaderWarmUp,绘制其他的内容提升预编译阶段的效率,同时 flutter 还给出了一些建议,比如--profile --trace-skia等判断是否有成效,还指出可能要针对不同的机型进行适配等问题。当然对于普通开发者来说,没有必要(可能也不会)搞这么精细的优化。

总结

以上就是 PaintingBinding 中所做的工作,它提供了图片缓存工具和 shaderWarmUp 来预编译着色器脚本以提高后续的渲染效率,对于前者,一般开发者不会使用到,因为在 Imageprovider 中会使用这个,我们只需要使用 ImageProvider 得到 Image 就好,至于从哪拿来的,不需要太过关心。而后者,一般来说也不会用到,估计只有对应用的性能有着近乎严苛的要求时,才会去考虑从这方面优化。