软件编程
位置:首页>> 软件编程>> flutter编程>> flutter图片组件核心类源码解析

flutter图片组件核心类源码解析

作者:allenymt  发布时间:2023-09-14 16:29:00 

标签:flutter,图片组件,核心类,源码解析

导语

在使用flutter 自带图片组件的过程中,大家有没有考虑过flutter是如何加载一张网络图片的? 以及对自带的图片组件我们可以做些什么优化?

问题

flutter 网络图片是怎么请求的?

图片请求成功后是这么展示的? gif的每一帧是怎么支持展示的?

如何支持图片的磁盘缓存?

接下来,让我们带着问题一起探究flutter 图片组件的内部原理

本文源码分析以flutter-1.22版本为准,只涉及到dart端,c层图片解码不涉及

Image的核心类图及其关系

自己重新画一张

flutter图片组件核心类源码解析

  • Image,是一个statefulWidget,flutter image的核心入口类,包含了network,file,assert,memory这几个主要的功能,分包对应网络图片,文件图片,APP内置assert图片,从文件流解析图片

  • _ImageState,由于Image是statefulWidget,所以核心代码都在_ImageState

  • ImageStream ,处理图片资源,ImageState和ImageStreamCompleter的桥梁

  • ImageInfo ,图片原生信息存储者

  • ImageStreamCompleter,可以理解为一帧帧解析图片,并把解析的数据回调给展示方,主要有两个实现类


    • OneFrameImageStreamCompleter单帧图片解析器(貌似没在用)


    • MultiFrameImageStreamCompleter多帧图片解析器,源码里所有图片都是默认使用这个了

  • ImageProvider,图片加载器,不同的加载方式有不同的实现


    • NetworkImage 网络加载图片


    • MemoryImage 从二进制流加载图片


    • AssetImage 加载asset里的image


    • FileImage 从文件中加载图片

  • ImageCache ,flutter自带的图片缓存,只有内存缓存,官方自带cache ,最大个数100,最大内存100MB

  • ScrollAwareImageProvider,避免图片在快速滑动中加载

网络图片的加载过程

// 网络图片
Image.network(imgUrl,  //图片链接
     width: w,
     height: h),
)

上文中提到过,Image是个StatefulWidget,那核心逻辑看对应的ImageState,ImageState继承自State,State的生命周期我们知道,首次初始化时按InitState()->didChangeDependencies->didUpdateWidget()-> build()顺序执行

ImageState的InitState没做什么,图片请求的发起是在didChangeDependencies里做的

// ImageState->didChangeDependencies
@override
void didChangeDependencies() {
   // ios在辅助模式下的配置,不影响主流程,我们不分析
 _updateInvertColors();

// 核心方法,开始请求解析图片,从这里开始,provier,stream,completer开始悉数登场
 _resolveImage();

// 这个判断可以认为是,当前widget 在tree中是否还是激活状态
 if (TickerMode.of(context))
   _listenToStream();
 else
   _stopListeningToStream();

super.didChangeDependencies();
}

再看ImageState里的_resolveImage方法

void _resolveImage() {
   // ScrollAwareImageProvider代理模式,它本身也是继承的ImageProvider,
   // 它的功能是防止在快速滚动时加载图片
 final ScrollAwareImageProvider provider = ScrollAwareImageProvider<dynamic>(
   context: _scrollAwareContext,
   imageProvider: widget.image,
 );

// 这里调用了ImageProvider的resolve方法,图片请求的主流程
 final ImageStream newStream =
   provider.resolve(createLocalImageConfiguration(
     context,
     size: widget.width != null && widget.height != null ? Size(widget.width, widget.height) : null,
   ));
 assert(newStream != null);
 // 对resolve返回的Stream注册监听,这个监听很重要,决定了后续的图片展示(包括gif)
 // 刷新当前图片展示一次,例如帧数,加载状态等等
 _updateSourceStream(newStream);
}

我们接着看ImageProvider的resolve方法

// 这方法初次看比较绕,其实就干了三个事
// 1. 创建了一个ImageStream
// 2. 创建一个Key,key由具体的provider自己实现,这个key用在后面ImageCache里
// 3. 把接下来的流程封装在一个Zone里,捕获了同步异常和异步异常,不了解Zone的同学可以参考我另一篇文章
@nonVirtual
ImageStream resolve(ImageConfiguration configuration) {
 assert(configuration != null);
 final ImageStream stream = createStream(configuration);
 // 创建了key,把后续的流程封装在zone里,源码我不贴了,感兴趣的同学自己看下
 _createErrorHandlerAndKey(
   configuration,
   (T key, ImageErrorListener errorHandler) {
     resolveStreamForKey(configuration, stream, key, errorHandler);
   },
   (T? key, dynamic exception, StackTrace? stack) async {
     await null; // wait an event turn in case a listener has been added to the image stream.
     final _ErrorImageCompleter imageCompleter = _ErrorImageCompleter();
     stream.setCompleter(imageCompleter);
     InformationCollector? collector;
     assert(() {
       collector = () sync* {
         yield DiagnosticsProperty<ImageProvider>('Image provider', this);
         yield DiagnosticsProperty<ImageConfiguration>('Image configuration', configuration);
         yield DiagnosticsProperty<T>('Image key', key, defaultValue: null);
       };
       return true;
     }());
     imageCompleter.setError(
       exception: exception,
       stack: stack,
       context: ErrorDescription('while resolving an image'),
       silent: true, // could be a network error or whatnot
       informationCollector: collector,
     );
   },
 );
 return stream;
}

接着看resolveStreamForKey方法,在1.22里,默认的provider都是ScrollAwareImageProvider,ScrollAwareImageProvider重写了resolveStreamForKey,这里有滚动控制加载的逻辑,但最终调用的还是ImageProvier的resolveStreamForKey

// ImageProvier -> resolveStreamForKey
@protected
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) {
   // streem中已经有completer了,从缓存中拿,
 if (stream.completer != null) {
   final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent(
     key,
     () => stream.completer!,
     onError: handleError,
   );
   assert(identical(completer, stream.completer));
   return;
 }

// 如果是首次,新建一个completer,然后会执行load这个函数,就是putIfAbsent的第二个入参
 final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent(
   key,
   () => load(key, PaintingBinding.instance!.instantiateImageCodec),
   onError: handleError,
 );
 // 赋值,注意这里,后面讲图片展示的时候会说到这里
 if (completer != null) {
   stream.setCompleter(completer);
 }
}

接着看ImageProvider的load,load方法就是图片的具体加载方法,不同的provider有不同的实现,此时我们关注NetworkImage的Provier里的实现

// NetworkImage
@override
ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
 // Ownership of this controller is handed off to [_loadAsync]; it is that
 // method's responsibility to close the controller's stream when the image
 // has been loaded or an error is thrown.
 final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();

// MultiFrameImageStreamCompleter是多帧解析器,默认使用的是就是这个,所以默认支持gif
 return MultiFrameImageStreamCompleter(
   codec: _loadAsync(key as NetworkImage, chunkEvents, decode), // 异步加载图片,我们接着看这个方法
   chunkEvents: chunkEvents.stream, //加载过程的回调
   scale: key.scale,
   debugLabel: key.url,
   informationCollector: () {
     return <DiagnosticsNode>[
       DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this),
       DiagnosticsProperty<image_provider.NetworkImage>('Image key', key),
     ];
   },
 );
}

接着看NetworkImage的_loadAsync

// 这里就很清晰了吧,内置的HttpClient去加载
Future<ui.Codec> _loadAsync(
 NetworkImage key,
 StreamController<ImageChunkEvent> chunkEvents,
 image_provider.DecoderCallback decode,
) async {
 try {
   assert(key == this);
   final Uri resolved = Uri.base.resolve(key.url);
   final HttpClientRequest request = await _httpClient.getUrl(resolved);

headers?.forEach((String name, String value) {
     request.headers.add(name, value);
   });
   final HttpClientResponse response = await request.close();
   if (response.statusCode != HttpStatus.ok) {
       // 请求失败,报错
     throw image_provider.NetworkImageLoadException(statusCode: response.statusCode, uri: resolved);
   }

// 二进制流数据回调
   final Uint8List bytes = await consolidateHttpClientResponseBytes(
     response,
     onBytesReceived: (int cumulative, int? total) {
       chunkEvents.add(ImageChunkEvent(
         cumulativeBytesLoaded: cumulative,
         expectedTotalBytes: total,
       ));
     },
   );
   if (bytes.lengthInBytes == 0)
     throw Exception('NetworkImage is an empty file: $resolved');
   //解析二进制流
   return decode(bytes);
 } catch (e) {
   // Depending on where the exception was thrown, the image cache may not
   // have had a chance to track the key in the cache at all.
   // Schedule a microtask to give the cache a chance to add the key.
   scheduleMicrotask(() {
     PaintingBinding.instance!.imageCache!.evict(key);
   });
   rethrow;
 } finally {
   chunkEvents.close();
 }
}

至此,第一个问题回答完毕,那当图片数据请求成功后,是怎么回调到ImageState并展示到界面中的呢?

网络图片数据的回调和展示过程

要看回调和展示,我们从终点ImageState的build方法开始看

// 很容易发现RawImage,RawImage是实际渲染图片的widget,这么说其实也不对,RenderImage才是最终渲染的
// 可以看到RawImage的第一个参数_imageInfo?.image,那_imageInfo?.image是什么时候赋值的?
Widget result = RawImage(
 image: _imageInfo?.image,
 debugImageLabel: _imageInfo?.debugLabel,
 width: widget.width,
 height: widget.height,
 scale: _imageInfo?.scale ?? 1.0,
 color: widget.color,
 colorBlendMode: widget.colorBlendMode,
 fit: widget.fit,
 alignment: widget.alignment,
 repeat: widget.repeat,
 centerSlice: widget.centerSlice,
 matchTextDirection: widget.matchTextDirection,
 invertColors: _invertColors,
 isAntiAlias: widget.isAntiAlias,
 filterQuality: widget.filterQuality,
);

还记得第一部分提到的_updateSourceStream(newStream);方法吗?在这个方法里对ImageStrem设置了一个监听

// 设置了监听
_imageStream.addListener(_getListener());

// ImageStreamListener
ImageStreamListener _getListener({bool recreateListener = false}) {
 if(_imageStreamListener == null || recreateListener) {
   _lastException = null;
   _lastStack = null;
   _imageStreamListener = ImageStreamListener(
     _handleImageFrame, // 每一帧图片解析完,代表可以展示这一帧了
     onChunk: widget.loadingBuilder == null ? null : _handleImageChunk, // 图片加载互调
     onError: widget.errorBuilder != null // 图片加载错误互调
         ? (dynamic error, StackTrace stackTrace) {
             setState(() {
               _lastException = error;
               _lastStack = stackTrace;
             });
           }
         : null,
   );
 }
 return _imageStreamListener;
}

接着看ImageState的_handleImageFrame

// 很简单,就是setState,可以看到这里赋值了_imageInfo
void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {
 setState(() {
   _imageInfo = imageInfo;
   _loadingProgress = null;
   _frameNumber = _frameNumber == null ? 0 : _frameNumber + 1;
   _wasSynchronouslyLoaded |= synchronousCall;
 });
}

那么这个_imageStreamListener 是什么时候回调的呢? 还记得第一步加载过程最后一步的MultiFrameImageStreamCompleter吗?

// MultiFrameImageStreamCompleter就是支持gif的多帧解析器,还有一个OneFrameImageStreamCompleter,但已经不用了
MultiFrameImageStreamCompleter({
 required Future<ui.Codec> codec,
 required double scale,
 String? debugLabel,
 Stream<ImageChunkEvent>? chunkEvents,
 InformationCollector? informationCollector,
}) : assert(codec != null),
    _informationCollector = informationCollector,
    _scale = scale {
 this.debugLabel = debugLabel;
 // _handleCodecReady就是图片加载完的回调,我们看看他内部干了什么
 codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) {
   // 捕获错误并上报
 });
 // 监听回调
 if (chunkEvents != null) {
   chunkEvents.listen(reportImageChunkEvent,
     onError: (dynamic error, StackTrace stack) {
       reportError(
         context: ErrorDescription('loading an image'),
         exception: error,
         stack: stack,
         informationCollector: informationCollector,
         silent: true,
       );
     },
   );
 }
}

这里回答了第二个问题,gif的每帧是怎么支持的,关键就是MultiFrameImageStreamCompleter这个类, 接着看MultiFrameImageStreamCompleter的_handleCodecReady

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

if (hasListeners) {
     // 看函数名就知道了,解析下一帧并执行
   _decodeNextFrameAndSchedule();
 }
}

MultiFrameImageStreamCompleter的_decodeNextFrameAndSchedule()

Future<void> _decodeNextFrameAndSchedule() async {
 try {
     // 获得下一帧,这一步在C中处理
   _nextFrame = await _codec!.getNextFrame();
 } catch (exception, stack) {
   reportError(
     context: ErrorDescription('resolving an image frame'),
     exception: exception,
     stack: stack,
     informationCollector: _informationCollector,
     silent: true,
   );
   return;
 }
 // 帧数不等于1,说明图片有多帧
 if (_codec!.frameCount == 1) {
   // This is not an animated image, just return it and don't schedule more
   // frames.
   _emitFrame(ImageInfo(image: _nextFrame!.image, scale: _scale, debugLabel: debugLabel));
   return;
 }
 // 如果只有一帧,_scheduleAppFrame最终也会走到_emitFrame
 _scheduleAppFrame();
}

接着看MultiFrameImageStreamCompleter的_emitFrame

// 调用了setImage
void _emitFrame(ImageInfo imageInfo) {
 setImage(imageInfo);
 _framesEmitted += 1;
}

ImageStreamCompleter的setImage

@protected
void setImage(ImageInfo image) {
 _currentImage = image;
 if (_listeners.isEmpty)
   return;
 // Make a copy to allow for concurrent modification.
 final List<ImageStreamListener> localListeners =
     List<ImageStreamListener>.from(_listeners);
 for (final ImageStreamListener listener in localListeners) {
   try {
       // 在这里回调了onImage,那这个回调是哪里注册的呢? 回到ImageStream的addLister里
     listener.onImage(image, false);
   } catch (exception, stack) {
   }
 }
}

ImageStream的addLister里

void addListener(ImageStreamListener listener) {
   // 这里破案了,_completer 不为null的时候,注册了回调,而ImageStream的completer在ImageStream被创建的还是就赋值了
   // 所以前面的listener.onImage(image, false);最终会回调到ImageState里的_imageStreamListener
 if (_completer != null)
   return _completer!.addListener(listener);
 _listeners ??= <ImageStreamListener>[];
 _listeners!.add(listener);
}

至此,图片的是展示流程也分析完毕,第二个问题也回答完了。

补上图片内存缓存的源码分析

首先要说明的是,flutter内存缓存默认只有内存缓存,也就意味着如果杀进程重启,图片就需要重新加载了。

1.22的内存缓存主要分三部分,相比1.17增加了一部分

_pendingImages 正在加载中的缓存,这个有什么作用呢? 假设Widget1加载了图片A,Widget2也在这个时候加载了图片A,那这时候Widget就复用了这个加载中的缓存

_cache 已经加载成功的图片缓存,这个很好理解

_liveImages 存活的图片缓存,看代码主要是在CacheImage之外再加一层缓存,在CacheImage被清楚后,

对于一张图片,当首次加载时,首先会在_pendingImages中,注意此时图片还未加载成功,所以如果有复用的情况,会命中_pendingImages,当图片请求成功后,在_cache和_liveImages都会保存一份,此时_pendingImages会移除。 当超过缓存中的最大数时,会从_cache里按照LRU的规则删除

如何支持图片的磁盘缓存

在看完整个流程后,对磁盘缓存应该也有思路了。第一个是可以自定义ImageProvider,在图片数据请求成功后写入磁盘缓存,不过对于混合项目来说,更好的方式应该是替换图片的网络请求方式,利用channel和原生(Android ,ios)的图片库加载图片,这样可以复用原生图片库的磁盘缓存,但也有缺陷,在效率上会有降低,毕竟多了内存的多次拷贝和channel通信。

来源:https://juejin.cn/post/6965148083596296223

0
投稿

猜你喜欢

手机版 软件编程 asp之家 www.aspxhome.com