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 就好,至于从哪拿来的,不需要太过关心。而后者,一般来说也不会用到,估计只有对应用的性能有着近乎严苛的要求时,才会去考虑从这方面优化。