Flutter加载图片流程MultiFrameImageStreamCompleter解析
作者:Nicholas68 发布时间:2023-07-19 02:45:55
MultiFrameImageStreamCompleter
MultiFrameImageStreamCompleter
是一个可组合的 ImageStreamCompleter
类,用于将多个 ImageStreamCompleter
对象合并为一个单独的 ImageStream
对象,通常用于动画效果。每当子 ImageStreamCompleter
接收到一个新的 ImageInfo
对象,它会立即通知其所有的 * ,并将对象传递给它们。
当 MultiFrameImageStreamCompleter
的 addListener()
方法被调用时,它将传入的 ImageStreamListener
添加到其内部的子 ImageStreamCompleter
的 * 列表中。如果 MultiFrameImageStreamCompleter
本身接收到一个 ImageInfo
对象,它会将它传递给其所有的 * 。但是,它不会自己管理这些帧,而是委托给每个子 ImageStreamCompleter
来完成。
MultiFrameImageStreamCompleter
还支持渐进式 JPEG,并实现了 addListener()
、removeListener()
和 dispose()
方法,以及一个名为 getNextFrame()
的方法,用于从图像流中获取下一帧。
当所有帧都加载完毕后,MultiFrameImageStreamCompleter
将使用 dart:ui.Codec
解码器将它们合并为一个单独的 dart:ui.Image
对象,并将其传递给 setImage()
方法。最后,它将通知所有 * ,并将它们传递给 ImageStreamListener.onImage()
回调函数,以通知它们新的 ImageInfo
已经可用。
当 MultiFrameImageStreamCompleter
的 dispose()
方法被调用时,它会将其所有子 ImageStreamCompleter
的 dispose()
方法依次调用,以释放所有资源,并取消所有未处理的帧请求。同时,它还会确保在释放资源之前将所有错误通知给其 * 。
_handleCodecReady
void _handleCodecReady(ui.Codec codec) {
_codec = codec;
assert(_codec != null);
if (hasListeners) {
_decodeNextFrameAndSchedule();
}
}
在_handleCodecReady
方法中,首先将传入的codec
对象赋值给成员变量_codec
,然后使用assert
语句来确保该变量不为空。接着,如果当前对象有 * ,则调用_decodeNextFrameAndSchedule
方法来解码下一帧并将其调度执行。这里的目的是为了尽早地开始解码下一帧图像,以尽快展示出完整的动画效果。如果没有 * ,则不需要解码下一帧图像,因为没有地方可以展示它。
_decodeNextFrameAndSchedule
Future<void> _decodeNextFrameAndSchedule() async {
// This will be null if we gave it away. If not, it's still ours and it
// must be disposed of.
_nextFrame?.image.dispose();
_nextFrame = null;
try {
_nextFrame = await _codec!.getNextFrame();
} catch (exception, stack) {
reportError(
context: ErrorDescription('resolving an image frame'),
exception: exception,
stack: stack,
informationCollector: _informationCollector,
silent: true,
);
return;
}
if (_codec!.frameCount == 1) {
// ImageStreamCompleter listeners removed while waiting for next frame to
// be decoded.
// There's no reason to emit the frame without active listeners.
if (!hasListeners) {
return;
}
// This is not an animated image, just return it and don't schedule more
// frames.
_emitFrame(ImageInfo(
image: _nextFrame!.image.clone(),
scale: _scale,
debugLabel: debugLabel,
));
_nextFrame!.image.dispose();
_nextFrame = null;
return;
}
_scheduleAppFrame();
}
这个方法的作用是获取下一帧并在获取成功后调度下一帧的解码,如果帧数为1,即这是一个静态图片,则只需返回该帧,并在没有 * 时直接返回,如果帧数大于1,则调度下一帧的解码。
在获取下一帧之前,方法会清除上一帧并将_nextFrame置为null,以便准备下一帧。
如果解码下一帧时发生异常,则会记录错误并返回。如果在等待下一帧的解码期间移除了 * ,则在没有活动的 * 时不会发出帧,否则会发出帧并调度下一帧的解码。
_emitFrame
方法的作用是向 ImageStreamCompleter
发送新的 ImageInfo
。具体实现是通过调用 setImage
方法将 ImageInfo
设置为 ImageStreamCompleter
的当前值,同时更新 _framesEmitted
计数器。
_codec!.getNextFrame()
_nextFrame = await _codec!.getNextFrame();
/// Fetches the next animation frame.
///
/// Wraps back to the first frame after returning the last frame.
///
/// The returned future can complete with an error if the decoding has failed.
///
/// The caller of this method is responsible for disposing the
/// [FrameInfo.image] on the returned object.
Future<FrameInfo> getNextFrame() async {
final Completer<FrameInfo> completer = Completer<FrameInfo>.sync();
final String? error = _getNextFrame((_Image? image, int durationMilliseconds) {
if (image == null) {
completer.completeError(Exception('Codec failed to produce an image, possibly due to invalid image data.'));
} else {
completer.complete(FrameInfo._(
image: Image._(image, image.width, image.height),
duration: Duration(milliseconds: durationMilliseconds),
));
}
});
if (error != null) {
throw Exception(error);
}
return completer.future;
}
/// Returns an error message on failure, null on success.
String? _getNextFrame(void Function(_Image?, int) callback) native 'Codec_getNextFrame';
getNextFrame()
是 Codec
类的一个方法,用于获取解码后的帧。具体来说,它会在 Codec
内部解码图像帧,返回一个 FrameInfo
对象,其中包含了解码后的 Image
对象以及该帧的时间戳和持续时间等信息。由于 Codec
可能会支持动画图像,因此 getNextFrame()
方法可能会返回多个帧。
在 MultiFrameImageStreamCompleter
中,_decodeNextFrameAndSchedule()
方法会调用 _codec.getNextFrame()
方法获取下一帧图像,然后将其保存在 _nextFrame
属性中。如果 _codec
的 frameCount
属性为 1,说明这是一个静态图像,直接使用 _emitFrame()
方法发布该帧图像;否则,调用 _scheduleAppFrame()
方法安排下一帧的发布。
_emitFrame(重要方法, 通知 * 触发回调,更新UI)
void _emitFrame(ImageInfo imageInfo) {
setImage(imageInfo);
_framesEmitted += 1;
}
这个方法在 _decodeNextFrameAndSchedule
中被调用,用于处理已解码的下一帧图像。如果当前帧是非动画图像,它会直接调用 setImage
方法更新 ImageStreamCompleter
的值,如果是动画图像,它会计划下一帧的显示并等待下一帧的解码。
_scheduleAppFrame
void _scheduleAppFrame() {
if (_frameCallbackScheduled) {
return;
}
_frameCallbackScheduled = true;
SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame);
}
函数 _scheduleAppFrame()
的作用是调度一个Flutter引擎帧回调,在回调中会调用 _handleAppFrame()
函数。
具体来说,这个函数的实现包含以下步骤:
1、检查 _frameCallbackScheduled
标志,如果为 true,则说明帧回调已经被调度过,直接返回。
2、将 _frameCallbackScheduled
标志设置为 true,表示帧回调已经被调度。
3、调用 SchedulerBinding.instance.scheduleFrameCallback()
函数,向Flutter引擎注册一个帧回调。回调函数为 _handleAppFrame()
。
4、在 _handleAppFrame()
函数中,将会根据当前动画播放的帧率和帧数,计算下一帧应该在何时被显示,然后再次调用 _decodeNextFrameAndSchedule()
函数,以获取并显示下一帧图像。这样就完成了一次动画播放的循环。
_handleAppFrame
void _handleAppFrame(Duration timestamp) {
_frameCallbackScheduled = false;
if (!hasListeners) {
return;
}
assert(_nextFrame != null);
if (_isFirstFrame() || _hasFrameDurationPassed(timestamp)) {
_emitFrame(ImageInfo(
image: _nextFrame!.image.clone(),
scale: _scale,
debugLabel: debugLabel,
));
_shownTimestamp = timestamp;
_frameDuration = _nextFrame!.duration;
_nextFrame!.image.dispose();
_nextFrame = null;
final int completedCycles = _framesEmitted ~/ _codec!.frameCount;
if (_codec!.repetitionCount == -1 || completedCycles <= _codec!.repetitionCount) {
_decodeNextFrameAndSchedule();
}
return;
}
final Duration delay = _frameDuration! - (timestamp - _shownTimestamp);
_timer = Timer(delay * timeDilation, () {
_scheduleAppFrame();
});
}
函数 _handleAppFrame
是 MultiFrameImageStreamCompleter 的核心函数,用于处理多帧图像的逻辑。下面是对该函数的详细解读:
1、
_frameCallbackScheduled = false;
将
_frameCallbackScheduled
设为 false,表示下一帧还没有调度。2、
if (!hasListeners) { return; }
如果没有 * ,则直接返回。
3、
assert(_nextFrame != null);
断言
_nextFrame
不为空。4、
_isFirstFrame() || _hasFrameDurationPassed(timestamp)
如果是第一帧或者帧时间已经超过了
_frameDuration
,则进行以下操作:5、
_emitFrame(ImageInfo(image: _nextFrame!.image.clone(), scale: _scale, debugLabel: debugLabel));
发出
ImageInfo
事件,将_nextFrame
的图像信息作为参数传入。6、
_shownTimestamp = timestamp;
更新
_shownTimestamp
为当前时间戳。7、
_frameDuration = _nextFrame!.duration;
更新
_frameDuration
为_nextFrame
的帧间隔时间。8、
_nextFrame!.image.dispose(); _nextFrame = null;
释放
_nextFrame
的图像资源并将_nextFrame
设为 null。9、
final int completedCycles = _framesEmitted ~/ _codec!.frameCount;
计算已经完成的循环次数。
10、
_codec!.repetitionCount == -1 || completedCycles <= _codec!.repetitionCount
如果循环次数为 -1(表示无限循环)或者已经完成的循环次数小于等于
_codec
的循环次数,则进行以下操作:11、
_decodeNextFrameAndSchedule();
解码下一帧并调度下一帧的绘制。
12、
final Duration delay = _frameDuration! - (timestamp - _shownTimestamp);
计算下一帧需要延迟的时间。
13、
_timer = Timer(delay * timeDilation, () { _scheduleAppFrame(); });
使用计时器来实现下一帧的延迟绘制。延迟时间为
delay
乘以timeDilation
(可以通过调用timeDilation = x
来改变时间流逝的速度)。当计时器触发时,将调用_scheduleAppFrame
来调度下一帧的绘制。
addListener
void addListener(ImageStreamListener listener) {
if (!hasListeners && _codec != null && (_currentImage == null || _codec!.frameCount > 1)) {
_decodeNextFrameAndSchedule();
}
super.addListener(listener);
}
这个方法是 ImageStreamCompleter
类的方法,用于向 ImageStreamCompleter
添加 * 。当第一个 * 被添加到 ImageStreamCompleter
上时,会检查 _codec
是否为 null,如果不为 null 并且有多帧图像或者当前图像为 null,则会调用 _decodeNextFrameAndSchedule()
方法开始解码下一帧图像并计划渲染。这样做是为了确保在第一个 * 被添加到 ImageStreamCompleter
上时就开始解码下一帧图像并在下一帧渲染完成后通知所有 * 。如果 _codec
为 null 或者当前图像为单帧图像,则不会调用 _decodeNextFrameAndSchedule()
方法。在这个方法中,调用了 super.addListener(listener)
将 * 添加到 * 列表中。
removeListener
void removeListener(ImageStreamListener listener) {
super.removeListener(listener);
if (!hasListeners) {
_timer?.cancel();
_timer = null;
}
}
removeListener
方法用于从 MultiFrameImageStreamCompleter
中移除给定的 ImageStreamListener
。当移除后,如果该对象不再有任何 * ,就会取消定时器 _timer
。
具体来说,该方法会先调用父类的 removeListener
方法,将该 * 从 * 列表中移除。接着,如果此时 hasListeners
为 false
,说明没有任何 * ,就会取消 _timer
定时器,以便释放资源。
_maybeDispose
void _maybeDispose() {
super._maybeDispose();
if (_disposed) {
_chunkSubscription?.onData(null);
_chunkSubscription?.cancel();
_chunkSubscription = null;
}
}
_maybeDispose()
是一个用来释放资源的方法,当图片流不再被监听时调用。它首先调用父类的_maybeDispose()
方法,以处理父类中的一些释放资源的逻辑。然后它会检查_disposed
属性是否为true,如果是,则取消并置空_chunkSubscription
,这个对象是用来订阅图像数据块的流。这样做是为了释放相关的资源,以防止内存泄漏。
来源:https://juejin.cn/post/7219868613666652217


猜你喜欢
- 本文实例为大家分享了iOS新浪微博分享功能的具体代码,供大家参考,具体内容如下做新浪分享 需先去http://open.weibo.com/
- 1、取得控制台应用程序的根目录方法方法1、Environment.CurrentDirectory 取得或设置当前工作目录的完整限定路径方法
- 前言在使用Java开发接口请求中,我们需要对请求进行进行统一返回值,这时候我们自己封装一个统一的Result返回类,下面就介绍下我用的这种的
- 1 自定义类加载器自定义类加载器的代码很简单,只需要继承ClassLoader类,覆写findClass方法即可其默认实现是会抛出一个异常:
- 前言之前用简书的时候一直是在web端,后来下载了客户端,看到了搜索的那个动画,就尝试的去写了,没写之前感觉挺容易的,写了之后,就感觉里面还是
- [LeetCode] 9. Palindrome Number 验证回文数字Determine whether an integer is
- webp格式图片webp格式图片是google推出的,相比jpg png有着巨大的优势,同样质量的图片webp格式的图片占用空间更小,在像电
- Android 微信摇一摇功能实现,最近学习传感器,就想实现摇一摇的功能,上网查了些资料,就整理下。如有错误,还请指正。开发环境Androi
- 具体代码如下所示:***web.xml***<?xml version="1.0" encoding="
- 本文实例为大家分享了C++实现俄罗斯方块的具体代码,供大家参考,具体内容如下先是效果图:主菜单:游戏:设置:错误处理:代码:#include
- 本文实例讲述了Android编程实现WebView添加进度条的方法。分享给大家供大家参考,具体如下:标准的XML界面<?xml ver
- 你知道String、StringBuilder、Stringbuffer的区别吗?当你创建字符串的时候,有考虑过该使用哪个吗?别急,这篇文章
- 一、Kotlin 调用 Java1. kotlin 关键字转义java 中的方法或变量 是 kotlin 的关键字时,使用反引号 `` 对关
- 这里使用的是dynamic-datasource-spring-boot-starter ,它是一个基于springboot的快速集成多数据
- 对于大文件的处理,无论是用户端还是服务端,如果一次性进行读取发送、接收都是不可取,很容易导致内存问题。所以对于大文件上传,采用切块分段上传,
- 最近项目中要做一个带进度条的上传文件的功能,学习了AsyncTask,使用起来比较方便,将几个方法实现就行,另外做了一个很简单的demo,希
- 在使用springMVC框架构建web应用,客户端常会请求字符串、整型、json等格式的数据,通常使用@ResponseBody注解使 co
- 本章将具体讲解下拉菜单的交互。使用下拉菜单(Dropdown)插件,您可以向任何组件(比如导航栏、标签页、胶囊式导航菜单、按钮等)添加下拉菜
- .c 源程序 ----- 编译 ----- 链接 ---- exe ----运行 -------->程序翻译环境和执行环境翻译环境:源
- 前言最近看插件库上少有的取色器大都是圆形的或者奇奇怪的的亚子,所以今天做两个矩形的颜色取色器提示:以下是本篇文章正文内容,下面案例可供参考一