写点什么

Flutter 图片加载原理与缓存,安卓高级开发工程师面试题

用户头像
Android架构
关注
发布于: 9 小时前

/// 获取下一个动画帧 Future<FrameInfo> getNextFrame() {return _futurize(_getNextFrame);}


String _getNextFrame(_Callback<FrameInfo> callback) native 'Codec_getNextFrame';


我们可以看到Codec最终的结果是一个或多个(动图)帧,而这些帧最终会绘制到屏幕上。


MultiFrameImageStreamCompleter 的?codec参数值为_loadAsync方法的返回值,我们继续看_loadAsync方法的实现:


Future<ui.Codec> _loadAsync(NetworkImage key,StreamController<ImageChunkEvent> chunkEvents,) async {try {//下载图片 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 Exception(...);// 接收图片数据 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


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


');// 对图片数据进行解码 return PaintingBinding.instance.instantiateImageCodec(bytes);} finally {chunkEvents.close();}}


可以看到_loadAsync方法主要做了两件事:


  1. 下载图片。

  2. 对下载的图片数据进行解码。


下载逻辑比较简单:通过HttpClient从网上下载图片,另外下载请求会设置一些自定义的 header,开发者可以通过NetworkImageheaders命名参数来传递。


在图片下载完成后调用了PaintingBinding.instance.instantiateImageCodec(bytes)对图片进行解码,值得注意的是instantiateImageCodec(...)也是一个 Native API 的包装,实际上会调用 Flutter engine 的instantiateImageCodec方法,源码如下:


String _instantiateImageCodec(Uint8List list, _Callback<Codec> callback, _ImageInfo imageInfo, int targetWidth, int targetHeight)native 'instantiateImageCodec';

obtainKey(ImageConfiguration)方法

该接口主要是为了配合实现图片缓存,ImageProvider从数据源加载完数据后,会在全局的ImageCache中缓存图片数据,而图片数据缓存是一个 Map,而 Map 的 key 便是调用此方法的返回值,不同的 key 代表不同的图片数据缓存。

resolve(ImageConfiguration)?方法

resolve方法是ImageProvider的暴露的给Image的主入口方法,它接受一个ImageConfiguration参数,返回ImageStream,即图片数据流。我们重点看一下resolve执行流程:


ImageStream resolve(ImageConfiguration configuration) {... //省略无关代码 final ImageStream stream = ImageStream();T obtainedKey; ////定义错误处理函数 Future<void> handleError(dynamic exception, StackTrace stack) async {... //省略无关代码 stream.setCompleter(imageCompleter);imageCompleter.setError(...);}


// 创建一个新 Zone,主要是为了当发生错误时不会干扰 MainZonefinal Zone dangerZone = Zone.current.fork(...);


dangerZone.runGuarded(() {Future<T> key;// 先验证是否已经有缓存 try {// 生成缓存 key,后面会根据此 key 来检测是否有缓存 key = obtainKey(configuration);} catch (error, stackTrace) {handleError(error, stackTrace);return;}key.then<void>((T key) {obtainedKey = key;// 缓存的处理逻辑在这里,记为 A,下面详细介绍 final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key), onError: handleError);if (completer != null) {stream.setCompleter(completer);}}).catchError(handleError);});return stream;}


ImageConfiguration?包含图片和设备的相关信息,如图片的大小、所在的AssetBundle(只有打到安装包的图片存在)以及当前的设备平台、devicePixelRatio(设备像素比等)。Flutter SDK 提供了一个便捷函数createLocalImageConfiguration来创建ImageConfiguration?对象:


ImageConfiguration createLocalImageConfiguration(BuildContext context, { Size size }) {return ImageConfiguration(bundle: DefaultAssetBundle.of(context),devicePixelRatio: MediaQuery.of(context, nullOk: true)?.devicePixelRatio ?? 1.0,locale: Localizations.localeOf(context, nullOk: true),textDirection: Directionality.of(context),size: size,platform: defaultTargetPlatform,);}


我们可以发现这些信息基本都是通过Context来获取。


上面代码 A 处就是处理缓存的主要代码,这里的PaintingBinding.instance.imageCache?是?ImageCache的一个实例,它是PaintingBinding的一个属性,而 Flutter 框架中的PaintingBinding.instance是一个单例,imageCache事实上也是一个单例,也就是说图片缓存是全局的,统一由PaintingBinding.instance.imageCache?来管理。


下面我们看看ImageCache类定义:


const int _kDefaultSize = 1000;const int _kDefaultSizeBytes = 100 << 20; // 100 MiB


class ImageCache {// 正在加载中的图片队列 final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};// 缓存队列 final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};


// 缓存数量上限(1000)int _maximumSize = _kDefaultSize;// 缓存容量上限 (100 MB)int _maximumSizeBytes = _kDefaultSizeBytes;


// 缓存上限设置的 setterset maximumSize(int value) {...}set maximumSizeBytes(int value) {...}


... // 省略部分定义


// 清除所有缓存 void clear() {// ...省略具体实现代码}


// 清除指定 key 对应的图片缓存 bool evict(Object key) {// ...省略具体实现代码}


ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) {assert(key != null);assert(loader != null);ImageStreamCompleter result = _pendingImages[key]?.completer;// 图片还未加载成功,直接返回 if (result != null)return result;


// 有缓存,继续往下走// 先移除缓存,后再添加,可以让最新使用过的缓存在_map 中的位置更近一些,清理时会 LRU 来清除 final _CachedImage image = _cache.remove(key);if (image != null) {_cache[key] = image;return image.completer;}try {result = loader();} catch (error, stackTrace) {if (onError != null) {onError(error, stackTrace);return null;} else {rethrow;}}void listener(ImageInfo info, bool syncCall) {final int imageSize = info?.image == null ? 0 : info.image.height * info.image.width * 4;final _CachedImage image = _CachedImage(result, imageSize);// 下面是缓存处理的逻辑 if (maximumSizeBytes > 0 && imageSize > maximumSizeBytes) {_maximumSizeBytes = imageSize + 1000;}_currentSizeBytes += imageSize;final _PendingImage pendingImage = _pendingImages.remove(key);if (pendingImage != null) {pendingImage.removeListener();}


_cache[key] = image;_checkCacheSize();}if (maximumSize > 0 && maximumSizeBytes > 0) {final ImageStreamListener streamListener = ImageStreamListener(listener);_pendingImages[key] = _PendingImage(result, streamListener);// Listener is removed in [_PendingImage.removeListener].result.addListener(streamListener);}return result;}


// 当缓存数量超过最大值或缓存的大小超过最大缓存容量,会调用此方法清理到缓存上限以内 void _checkCacheSize() {while (_currentSizeBytes > _maximumSizeBytes || _cache.length > _maximumSize) {final Object key = _cache.keys.first;final _CachedImage image = _cache[key];_currentSizeBytes -= image.sizeBytes;_cache.remove(key);}... //省略无关代码}}


有缓存则使用缓存,没有缓存则调用 load 方法加载图片,加载成功后:


  1. 先判断图片数据有没有缓存,如果有,则直接返回ImageStream

  2. 如果没有缓存,则调用load(T key)方法从数据源加载图片数据,加载成功后先缓存,然后返回 ImageStream。


另外,我们可以看到ImageCache类中有设置缓存上限的 setter,所以,如果我们可以自定义缓存上限:


PaintingBinding.instance.imageCache.maximumSize=2000; //最多 2000 张 PaintingBinding.instance.imageCache.maximumSizeBytes = 200 << 20; //最大 200M


现在我们看一下缓存的 key,因为 Map 中相同 key 的值会被覆盖,也就是说 key 是图片缓存的一个唯一标识,只要是不同 key,那么图片数据就会分别缓存(即使事实上是同一张图片)。那么图片的唯一标识是什么呢?跟踪源码,很容易发现 key 正是ImageProvider.obtainKey()方法的返回值,而此方法需要ImageProvider子类去重写,这也就意味着不同的ImageProvider对 key 的定义逻辑会不同。其实也很好理解,比如对于NetworkImage,将图片的 url 作为 key 会很合适,而对于AssetImage,则应该将“包名+路径”作为唯一的 key。下面我们以NetworkImage为例,看一下它的obtainKey()实现:


@overrideFuture<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) {return SynchronousFuture<NetworkImage>(this);}


代码很简单,创建了一个同步的 future,然后直接将自身做为 key 返回。因为 Map 中在判断 key(此时是NetworkImage对象)是否相等时会使用“==”运算符,那么定义 key 的逻辑就是NetworkImage的“==”运算符:


@overridebool operator ==(dynamic other) {... //省略无关代码 final NetworkImage typedOther = other;return url == typedOther.url&& scale == typedOther.scale;}


很清晰,对于网络图片来说,会将其“url+缩放比例”作为缓存的 key。也就是说如果两张图片的 url 或 scale 只要有一个不同,便会重新下载并分别缓存


另外,我们需要注意的是,图片缓存是在内存中,并没有进行本地文件持久化存储,这也是为什么网络图片在应用重启后需要重新联网下载的原因。


同时也意味着在应用生命周期内,如果缓存没有超过上限,相同的图片只会被下载一次。

总结

上面主要结合源码,探索了ImageProvider的主要功能和原理,如果要用一句话来总结ImageProvider功能,那么应该是:加载图片数据并进行缓存、解码。在此再次提醒读者,Flutter 的源码是非常好的第一手资料,建议读者多多探索,另外,在阅读源码学习的同时一定要有总结,这样才不至于在源码中迷失。

14.5.2 Image 组件原理

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Flutter图片加载原理与缓存,安卓高级开发工程师面试题