上文研究完 Flutter 的图片加载和缓存管理
Flutter图片加载和缓存机制探究
今天继续研究下 Flutter 是怎么处理动图的。Flutter 的 Image
加载默认会支持 gif、webp 等动态图片。在之前的文章中,我们会看到不同类型的图片加载逻辑是大致一样的,只是异步加载的逻辑不一样,
@override
ImageStreamCompleter load(
final StreamController<ImageChunkEvent> chunkEvents =
StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
chunkEvents: chunkEvents.stream,
codec: _loadAsync(key as NetworkImage, decode, chunkEvents),
scale: key.scale,
debugLabel: key.url,
informationCollector: _imageStreamInformationCollector(key));
}
load
方法都会返回一个 MultiFrameImageStreamCompleter 对象。从名字我们就有可以看到,这个可以处理多个帧的图片。这也是 Flutter 在加载图片的时候默认会使用的 Completer 对象。它的基类是 ImageStreamCompleter�
。
codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) {
reportError(
context: ErrorDescription('resolving an image codec'),
exception: error,
stack: stack,
informationCollector: informationCollector,
silent: true,
);
});
图片加载完成后,会执行 _handleCodecReady 的逻辑。可以看到这里已经是开始图片解码了。
代码语言:javascript复制// _handleCodecReady
void _handleCodecReady(ui.Codec codec) {
_codec = codec;
assert(_codec != null);
if (hasListeners) {
_decodeNextFrameAndSchedule();
}
}
这里会进行下一帧的解码:
代码语言:javascript复制// _handleCodecReady
void _handleCodecReady(ui.Codec codec) {
_codec = codec;
assert(_codec != null);
if (hasListeners) {
_decodeNextFrameAndSchedule();
}
}
// _decodeNextFrameAndSchedule
Future<void> _decodeNextFrameAndSchedule() async {
try {
// 获取下一帧
_nextFrame = await _codec!.getNextFrame();
} catch (exception, stack) {
return;
}
if (_codec!.frameCount == 1) {
// 只有一帧
_emitFrame(ImageInfo(image: _nextFrame!.image, scale: _scale, debugLabel: debugLabel));
return;
}
// 不止一帧,说明是动图
_scheduleAppFrame();
}
void _scheduleAppFrame() {
if (_frameCallbackScheduled) {
return;
}
_frameCallbackScheduled = true;
SchedulerBinding.instance!.scheduleFrameCallback(_handleAppFrame);
}
我们来梳理一下这里的逻辑:解析过程会尝试读取图片的下一帧。当图片解码信息里图片只有一帧的话,那么直接提交这一帧内容并结束, 如果 frameCount > 1 的话,则说明图片不止一帧内容,说明此时加载的是一张动图。继续执行 _sheduleAppFrame 方法。这个会在 ShedulerBinding 执行下一帧的时候执行 _handleAppFrame 方法。
代码语言:javascript复制void _handleAppFrame(Duration timestamp) {
_frameCallbackScheduled = false;
// 如果是第一帧
if (_isFirstFrame() || _hasFrameDurationPassed(timestamp)) {
// 提交一帧图片
_emitFrame(ImageInfo(
image: _nextFrame!.image,
scale: _scale, debugLabel: debugLabel));
_shownTimestamp = timestamp;
_frameDuration = _nextFrame!.duration;
_nextFrame = null;
// 计算这是第几次播放
final int completedCycles = _framesEmitted ~/ _codec!.frameCount;
// 如果重复次数是-1 或者完成的次数小于等于动图循环次数,继续执行_decodeNextFrameAndSchedule
if (_codec!.repetitionCount == -1 || completedCycles <= _codec!.repetitionCount) {
_decodeNextFrameAndSchedule();
}
return;
}
final Duration delay = _frameDuration! - (timestamp - _shownTimestamp);
_timer = Timer(delay * timeDilation, () {
// 循环到下一帧
_scheduleAppFrame();
});
}
这个方法里会处理当前的帧。如果是第一帧或者应该是下一帧出现的时间了,就会去提交该帧的内容。接下来会判断这张图是否播放完毕,如果没有,则会继续执行上面的解码工作,去解析下一帧图片。这里判断图片是否播放完毕依赖于两个条件:
- 设置的重复次数是 -1,需要一直循环播
- 播放的轮数小于设置的重复次数,轮数就是当前提交的帧数和图片总帧数取整。比如图片总共有10帧,播放到第32帧的时候,说明当前是第三轮。
整个动图的加载流程如图:
总结
从上面的代码中我们可以获取一些结论:
- Flutter 默认是支持解析动图的,包括 webp、gif 这些
- 我们可以自己参考上述内容去实现我们的动图播放,增加例如动画控制、动图播放监听等功能