Flutter加载图片流程之MultiFrameImageStreamCompleter源码解析(三)

MultiFrameImageStreamCompleter

MultiFrameImageStreamCompleter 是一个可组合的 ImageStreamCompleter 类,用于将多个 ImageStreamCompleter 对象合并为一个单独的 ImageStream 对象,通常用于动画效果。每当子 ImageStreamCompleter 接收到一个新的 ImageInfo 对象,它会立即通知其所有的监听器,并将对象传递给它们。

MultiFrameImageStreamCompleteraddListener() 方法被调用时,它将传入的 ImageStreamListener 添加到其内部的子 ImageStreamCompleter 的监听器列表中。如果 MultiFrameImageStreamCompleter 本身接收到一个 ImageInfo 对象,它会将它传递给其所有的监听器。但是,它不会自己管理这些帧,而是委托给每个子 ImageStreamCompleter 来完成。

MultiFrameImageStreamCompleter 还支持渐进式 JPEG,并实现了 addListener()removeListener()dispose() 方法,以及一个名为 getNextFrame() 的方法,用于从图像流中获取下一帧。

当所有帧都加载完毕后,MultiFrameImageStreamCompleter 将使用 dart:ui.Codec 解码器将它们合并为一个单独的 dart:ui.Image 对象,并将其传递给 setImage() 方法。最后,它将通知所有监听器,并将它们传递给 ImageStreamListener.onImage() 回调函数,以通知它们新的 ImageInfo 已经可用。

MultiFrameImageStreamCompleterdispose() 方法被调用时,它会将其所有子 ImageStreamCompleterdispose() 方法依次调用,以释放所有资源,并取消所有未处理的帧请求。同时,它还会确保在释放资源之前将所有错误通知给其监听器。

_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 属性中。如果 _codecframeCount 属性为 1,说明这是一个静态图像,直接使用 _emitFrame() 方法发布该帧图像;否则,调用 _scheduleAppFrame() 方法安排下一帧的发布。

_emitFrame

这个方法在 _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 方法,将该监听器从监听器列表中移除。接着,如果此时 hasListenersfalse,说明没有任何监听器,就会取消 _timer 定时器,以便释放资源。

_maybeDispose

void _maybeDispose() {
  super._maybeDispose();
  if (_disposed) {
    _chunkSubscription?.onData(null);
    _chunkSubscription?.cancel();
    _chunkSubscription = null;
  }
}
复制代码

_maybeDispose()是一个用来释放资源的方法,当图片流不再被监听时调用。它首先调用父类的_maybeDispose()方法,以处理父类中的一些释放资源的逻辑。然后它会检查_disposed属性是否为true,如果是,则取消并置空_chunkSubscription,这个对象是用来订阅图像数据块的流。这样做是为了释放相关的资源,以防止内存泄漏。

总结

MultiFrameImageStreamCompleter 是 Flutter 中用于处理多帧图片的类,主要用于将多帧动画图片的每一帧渲染到屏幕上。

该类内部主要维护了一个 Codec 对象,用于解码图片,同时也有一个 ImageInfo 对象用于存储当前帧的信息,并且该类也实现了 ImageStreamCompleter 类,可以作为 Image 对象的 ImageStream。

在 MultiFrameImageStreamCompleter 的初始化过程中,会创建 Codec 对象,并且在该对象准备好后进行处理,并且在添加监听器时,如果该类当前没有监听器,并且已经获取了第一帧图像,那么该类会进行后续帧的解码和渲染。如果该类被销毁,则会清空 Codec 对象。

在该类的主要方法中,_handleCodecReady() 方法会在初始化时进行调用,用于设置解码后的 Codec 对象,并在有监听器的情况下开始解码和渲染下一帧图片。

_decodeNextFrameAndSchedule() 方法用于解码和渲染下一帧图片,通过 _codec!.getNextFrame() 方法获取下一帧图像,并进行渲染处理,如果当前只有一帧图片,则直接渲染该帧图片并停止。

_handleAppFrame() 方法用于处理渲染下一帧图片的逻辑,会根据时间戳计算出下一帧图片的渲染时间,并设置延时定时器,定时调用该方法。

addListener() 和 removeListener() 方法用于添加和删除监听器,并在没有监听器时停止解码和渲染。

最后,_maybeDispose() 方法会在该类被销毁时进行调用,用于清空内部缓存。

参考链接

深入图片加载流程

猜你喜欢

转载自juejin.im/post/7219868613666652217