Flutter 动图加载机制解析

1,615 阅读3分钟

研究完 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 的逻辑。可以看到这里已经是开始图片解码了。 ​

// _handleCodecReady
void _handleCodecReady(ui.Codec codec) {
    _codec = codec;
    assert(_codec != null);

    if (hasListeners) {
      _decodeNextFrameAndSchedule();
    }
}

这里会进行下一帧的解码: ​

// _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 方法。 ​

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帧的时候,说明当前是第三轮。

整个动图的加载流程如图: image.png

总结

从上面的代码中我们可以获取一些结论:

  • Flutter 默认是支持解析动图的,包括 webp、gif 这些
  • 我们可以自己参考上述内容去实现我们的动图播放,增加例如动画控制、动图播放监听等功能

我的微信公众号是半行代码,也欢迎大家关注交流。

image.png