30

Flutter 图片加载

 4 years ago
source link: https://www.tuicool.com/articles/iU77zyI
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

奇技指南

本篇文章你将获得?

1、Flutter 图片加载方式

2、Flutter 图片加载源码实现流程

3、Flutter 图片加载优化点有什么

本文转载自奇舞移动技术。

Flutter Image

在 Flutter 中 Image 是展示图片的 widget ,用于从 ImageProvider 获取图像。 Image 支持的图片格式有 JPEG、WebP、GIF、animated WebP/GIF 、PNG 、BMP、 and WBMP。

Image 结构如下:

fQrqiiN.jpg!web

可以看到图片上部有多个加载方式。

Flutter图片加载方式

1、Image.asset

使用 key 从AssetBundle获得的图片;

两种方式如下:

Image(height: 100, width: 100, image: AssetImage(happy.png), )
Image.asset( happy.png, width: 100, height: 100,)

当然这一方式,需要在 pubspec.yaml 文件中配置图片路径。

2、Image.network

从网络URL中获取图片;

Image.network('https://p0.ssl.qhimg.com/t0183421f63f84fccaf.gif',fit: BoxFit.fill);

3、Image.file

从本地文件中获取图片;

Image.file(File('/sdcard/happy.png')),

4、Image.memory

用于从 Uint8List 获取图片;

new Image.memory(Uint8List bytes),

bytes 指内存中的图片数据,将其转化为图片对象。

Unit8List 与其他语言数据结构类比:

iqqyAfb.png!web

其他相关常用的加载图片的方式

5、CacheNetworkImage

缓存的网络图片,此类属于 cached_network_image 库;

newCachedNetworkImage(
fit:BoxFit.fill,
width:200,
height:100,
imageUrl:'https://p0.ssl.qhimg.com/t0183421f63f84fccaf.gif',
placeholder:(context,url)=>newProgressView(),
errorWidget:(context,url,error)=>newIcon(Icons.error),
);

6、FadeInImage.memoryNetwork

默认占位图和淡入效果

import'package:transparent_image/transparent_image.dart';

FadeInImage.memoryNetwork(
placeholder:kTransparentImage,//kTransparentImage 属于 transparent_image 库
image:'https://p0.ssl.qhimg.com/t0183421f63f84fccaf.gif',
);

7、Icon Icons 图片参考URL

new Icon(Icons.android,size: 200,);

Flutter 加载 images 分辨率

Flutter 可以为当前设备加载适合其分辨率的图像。指定不同设备像素比例的图片可以这样分配asset文件夹:

  • …icon/happy.png

  • …/2.0x/happy.png

  • …/3.0x/happy.png

主资源默认对应于 1.0 倍的分辨率图片;在设备像素比率为 1.8 的设备上会选用

.../2.0x/happy.png 对于在像素比率 2.7 的设备上 ,会选用 .../3.0x/happy.png

pubspec.yaml 文件中的 asset 声明中每一项都标识与实际文件对应。但是主资源缺少时,会按分辨率从低到高的顺序寻找加载。这里的加载方案,可以参考 Android 系统中图片加载的逻辑作对比。

Flutter 打包应用时,资源会按照 key-value 的形式存入apk 的                            assets/flutter_assets/AssetManifest.json 文件中,加载资源时先解析 json 文件,选择最适合的图片进行加载显示,其中 AssetManifest.json 的具体内容简介如:

{
"assets/happy.png":[
"assets/2.0x/happy.png",
"assets/3.0x/happy.png"
]
}

Android

android 上可以通过 AssetManager 获取 asset, 根据 key 查找到 openFd 。

key 是由 PluginRegistry.RegistrarlookupKeyForAssetFlutterViewgetLookupKeyForAsset 得到;

PluginRegistry.Registrar 用于开发插件,而 FlutterView 则用于开发平台 app的view。

pubspec.yaml

flutter:
assets:
- icons/happy.png

Java plugin code

AssetManagerassetManager=registrar.context().getAssets();
Stringkey=registrar.lookupKeyForAsset("icons/happy.png");
AssetFileDescriptorfd=assetManager.openFd(key);

iOS

iOS 开发使用 mainbundle 获取 assets。

使用 Flutter PluginRegistrar 的 lookupKeyForAsset 和

lookupKeyForAsset:fromPackage: 方法获取文件路径 ;

Flutter ViewController lookupKeyForAsset

lookupKeyForAsset:fromPackage: 方法获取文件路径 ;

然后 FlutterPluginRegistrar 用于开发插件,而 FlutterViewController 则用于开发平台 app 的 view 。

Objective-C plugin

NSString*key=[registrarlookupKeyForAsset:@"icons/happy.png"];
NSString*path=[[NSBundlemainBundle]pathForResource:keyofType:nil];

当然 pubspec.yaml 配置都是一致的。

源码分析

图片加载方式中有四种方式,接下来我们一起看看 framework 层加载图片是如何实现的。我们就以 Image.network 为例,跟进一下相关源码实现。

Image.network 的方法如下:

Image.network(
Stringsrc, {
Keykey,
doublescale=1.0,
this.frameBuilder,
this.loadingBuilder,
this.semanticLabel,
this.excludeFromSemantics=false,
this.width,
this.height,
this.color,
this.colorBlendMode,
this.fit,
this.alignment=Alignment.center,
this.repeat=ImageRepeat.noRepeat,
this.centerSlice,
this.matchTextDirection=false,
this.gaplessPlayback=false,
this.filterQuality=FilterQuality.low,
Map<String,String>headers,
}) :image=NetworkImage(src,scale:scale,headers:headers),
assert(alignment!=null),
assert(repeat!=null),
assert(matchTextDirection!=null),
super(key:key);

这方法的作用就是创建一个 用于显示从网络得到的 ImageStreamimage 小部件,加载网络图片的 image 是由 NetworkImage 创建出来的,其中参数 src, scale, headers 是不能为空的,其他的参数可以不做要求。 NetworkImage 又是继承自 ImageProvider

所以image 就是 ImageProvider 。ImageProvider 是个抽象类,它的实现类包括: 

NetworkImage,FileImage,ExactAssetImage,AssetImage,MemoryImage,AssetBundleImageProvider。

UZzYzyE.jpg!web

Image 源码部分如下

classImageextendsStatefulWidget{
/// 用于显示的 image
finalImageProviderimage;

..........

@override
_ImageStatecreateState()=>_ImageState();
}

_ImageState 类

class_ImageStateextendsState<Image>withWidgetsBindingObserver{
ImageStream_imageStream;
ImageInfo_imageInfo;

.......

@override
voidinitState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}

@override
voiddidChangeDependencies() {
_updateInvertColors();
_resolveImage();//解析图片从这里开始
//设置和移除监听图片变化的回调
if(TickerMode.of(context))
_listenToStream();
else
_stopListeningToStream();
super.didChangeDependencies();
}

void_resolveImage() {
//根据 ImageConfiguration 调用 ImageProvider 的 resolve 函数获得 ImageStream 对象
finalImageStreamnewStream=widget.image.resolve(createLocalImageConfiguration(
context,
size:widget.width!=null&&widget.height!=null?Size(widget.width,widget.height) :null,
));
_updateSourceStream(newStream);
}
......
}

它的生命周期方法方法包括 initState(), didChangeDependencies(), build() deactivate() dispose() didUpdateWidget() 等等。当它插入到渲染树时,先调用 initState() 函数,再调用 didChangeDependencies() 。代码中可以看到调用了方法 _resolveImage() ,这个方法中创建了 ImageStream 的新对象 newStream 。widget.image 就是 ImageProvider ,调用resolve方法,代码如下:

ImageStreamresolve(ImageConfigurationconfiguration) {
finalImageStreamstream=ImageStream();
TobtainedKey;
booldidError=false;
Future<void>handleError(dynamicexception,StackTracestack)async{
if(didError) {
return;
}
didError=true;
awaitnull;// 等待事件轮询,以防侦听器被添加到图像流中。

final_ErrorImageCompleterimageCompleter=_ErrorImageCompleter();
stream.setCompleter(imageCompleter);
......
}

......

Future<T>key;
try{
key=obtainKey(configuration);
}catch(error,stackTrace) {
return;
}
key.then<void>((Tkey) {
obtainedKey=key;
finalImageStreamCompletercompleter=
PaintingBinding.instance.imageCache.putIfAbsent(key, ()=>load(key),onError:handleError);
if(completer!=null) {
stream.setCompleter(completer);
}
}).catchError(handleError);

returnstream;

ImageStreamCompleter 用于管理 dart:ui 加载的类的基类。ImageStreams 的对象很少直接构造,而是由 ImageStreamCompleter 自动配置它。ImageStream 中的图片管理者 ImageStreamCompleter 通过方法创建,imageCache 是 Flutter 框架中实现的用于图片缓存的单例,它在 Dart 虚拟机加载时就已经创建。imageCache 最多可缓存 1000 张图像和 100MB 内存空间。可以使用 [maximumSize] 和 [maximumSizeBytes]调整最大大小。

PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key), onError:handleError);

根据源码可以看到两个关键方法 :putIfAbsent 和 load。

putIfAbsent

ImageStreamCompleterputIfAbsent(Objectkey,ImageStreamCompleterloader(), {ImageErrorListeneronError}) {
ImageStreamCompleterresult=_pendingImages[key]?.completer;
// 因为图像还没有加载,不需要做任何事情。
if(result!=null)
returnresult;
// 从缓存列表中根据Key删除对应的 imageprovider,便于将它移动到下面最近使用位置。
final_CachedImageimage=_cache.remove(key);
if(image!=null) {
_cache[key]=image;
returnimage.completer;
}
try{
result=loader();
}catch(error,stackTrace) {
......
}
voidlistener(ImageInfoinfo,boolsyncCall) {
// 无法加载的图像不会占用缓存大小。
finalintimageSize=info?.image==null?0:info.image.height*info.image.width*4;
final_CachedImageimage=_CachedImage(result,imageSize);
// 如果图像大于最大缓存大小,且缓存大小不为零,则将缓存大小增加到图像大小加上 1000。
// 思考点:一直这么加什么时候引起崩溃?
if(maximumSizeBytes>0&&imageSize>maximumSizeBytes) {
_maximumSizeBytes=imageSize+1000;
}
_currentSizeBytes+=imageSize;
final_PendingImagependingImage=_pendingImages.remove(key);
if(pendingImage!=null) {
pendingImage.removeListener();
}
_cache[key]=image;
_checkCacheSize();
}
if(maximumSize>0&&maximumSizeBytes>0) {
finalImageStreamListenerstreamListener=ImageStreamListener(listener);
_pendingImages[key]=_PendingImage(result,streamListener);
// 移除 [_PendingImage.removeListener] 上的监听
result.addListener(streamListener);
}
returnresult;
}

load

/// 拉取网络图片的 image_provider.NetworkImage 具体实现.
classNetworkImageextendsimage_provider.ImageProvider<image_provider.NetworkImage>implementsimage_provider.NetworkImage{
......................
@override
ImageStreamCompleterload(image_provider.NetworkImagekey) {

finalStreamController<ImageChunkEvent>chunkEvents=StreamController<ImageChunkEvent>();

returnMultiFrameImageStreamCompleter(

codec:_loadAsync(key,chunkEvents),

chunkEvents:chunkEvents.stream,
scale:key.scale,
informationCollector: () {
return<DiagnosticsNode>[
DiagnosticsProperty<image_provider.ImageProvider>('Image provider',this),
DiagnosticsProperty<image_provider.NetworkImage>('Image key',key),
];
},
);
}

loadAsync

Future<ui.Codec>_loadAsync(
NetworkImagekey,
StreamController<ImageChunkEvent>chunkEvents,
)async{
try{
finalUriresolved=Uri.base.resolve(key.url);
finalHttpClientRequestrequest=await_httpClient.getUrl(resolved);
headers?.forEach((Stringname,Stringvalue) {
request.headers.add(name,value);
});
finalHttpClientResponseresponse=awaitrequest.close();
if(response.statusCode!=HttpStatus.ok)
throwimage_provider.NetworkImageLoadException(statusCode:response.statusCode,uri:resolved);
//将网络返回的 response 信息,转换成内存中的 Uint8List bytes。这里面有解压 gzip 的逻辑。
finalUint8Listbytes=awaitconsolidateHttpClientResponseBytes(
response,
onBytesReceived: (intcumulative,inttotal) {
chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded:cumulative,
expectedTotalBytes:total,
));
},
);
if(bytes.lengthInBytes==0)
throwException('NetworkImage is an empty file:$resolved');

returnPaintingBinding.instance.instantiateImageCodec(bytes);
}finally{
chunkEvents.close();
}
}

将网络返回的response信息,转换成内存中的 Uint8List bytes,最终返回一个实例化图像编解码器对象Codec,此处 Codec 可以移步到 painting.dart 文件的 _instantiateImageCodec 看出来它是调用了native方法去处理了。

MultiFrameImageStreamCompleter

这个对象就是 ImageStreamCompleter 的具体实现,见名知意,多帧图片流管理,作用管理图像帧的解码和调度。

这个类处理两种类型的帧:

  • 图像帧 :动画图像的图像帧。

  • app 帧 :Flutter 引擎绘制到屏幕的帧,显示到应用程序 GUI。

这就不贴所有代码了,在 image_stream.dart 文件中 可见 class

MultiFrameImageStreamCompleter。    

MultiFrameImageStreamCompleter({
@requiredFuture<ui.Codec>codec,
@requireddoublescale,
Stream<ImageChunkEvent>chunkEvents,
InformationCollectorinformationCollector,
}) :assert(codec!=null),
_informationCollector=informationCollector,
_scale=scale{
codec.then<void>(_handleCodecReady,onError: (dynamicerror,StackTracestack) {
..........
});

_handelCodecReady

这里 codec 异步回调次方法

void_handleCodecReady(ui.Codeccodec) {
_codec=codec;
if(hasListeners) {
_decodeNextFrameAndSchedule();
}
}

_decodeNextFrameAndSchedule

codec 解码获取到图片的帧数,判断图片是只有一帧的话,就是png、jpg这样静态图片。

Future<void>_decodeNextFrameAndSchedule()async{
try{
_nextFrame=await_codec.getNextFrame();
}catch(exception,stack) {
........
return;
}
if(_codec.frameCount==1) {// 此处判断图片是只有一帧的逻辑.
_emitFrame(ImageInfo(image:_nextFrame.image,scale:_scale));
return;
}
_scheduleAppFrame();
}

void_scheduleAppFrame() {
if(_frameCallbackScheduled) {
return;
}
_frameCallbackScheduled=true;
SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame);
}
_emitFrame(ImageInfo(image:_nextFrame.image,scale:_scale));

void_emitFrame(ImageInfoimageInfo) {
setImage(imageInfo);
_framesEmitted+=1;
}


@protected
voidsetImage(ImageInfoimage) {
_currentImage=image;
if(_listeners.isEmpty)
return;
// 复制一份以允许并发修改。
finalList<ImageStreamListener>localListeners=List<ImageStreamListener>.from(_listeners);

for(ImageStreamListenerlistenerinlocalListeners) {
try{
listener.onImage(image,false);
}catch(exception,stack) {
..........
}
}
}

setImage 核心逻辑就是通知所有注册上的监听,表示图片发生了变化可以更新啦。此时我们回到 开始提到的_ImageState 类中 didChangeDependencies 方法调用的 _listenToStream 方法,最终调用方法 _handleImageFrame ,改变 图片信息 _imageInfo 和 图片帧数变化 _frameNumber ,最终执行 setState(() {}) 来刷新了 UI。

void_listenToStream() {
if(_isListeningToStream)
return;
_imageStream.addListener(_getListener());
_isListeningToStream=true;
}

ImageStreamListener_getListener([ImageLoadingBuilderloadingBuilder]) {
loadingBuilder??=widget.loadingBuilder;
returnImageStreamListener(
_handleImageFrame,
onChunk:loadingBuilder==null?null:_handleImageChunk,
);
}

void_handleImageFrame(ImageInfoimageInfo,boolsynchronousCall) {
setState(() {
_imageInfo=imageInfo;
_loadingProgress=null;
_frameNumber=_frameNumber==null?0:_frameNumber+1;
_wasSynchronouslyLoaded|=synchronousCall;
});
}

这样就结束了一个网络图片的加载过程。

此处应该有流程图就更加简洁明了的表达啦。

总结

图片加载显示的方式 framework 提供了多种方式,我们就图片网络加载进行了分析。 从源码角度对网络图片加载过程有了大致的了解。 发现的可以优化点,这里先提出来优化的点:

1、看到网络图片只是在 ImageCache 管理类中进行了内存缓存,当应用进程重新启动后还是要重新下载图片,此处是可以优化的,比如保存到本地磁盘外存。

2、拿到图片加载到内存里面的时候,是否有对图片进行压缩处理,这种处理最好既适应当前平台又不过分地改变图片的清晰度。

期待下一篇的迭代优化点。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK