热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Flutter图片加载

奇技指南本篇文章你将获得?1、Flutter图片加载方式2、Flutter图片加载源码实现流程3、Flutter图片加载优化点有什么本文转载自奇舞移动技术。FlutterImage

奇技指南

本篇文章你将获得?

1、Flutter 图片加载方式

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

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

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

Flutter Image

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

Image 结构如下:

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

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 与其他语言数据结构类比:

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

5、CacheNetworkImage

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

new CachedNetworkImage(
   fit:BoxFit.fill,
   width:200,
   height:100,
   imageUrl:'https://p0.ssl.qhimg.com/t0183421f63f84fccaf.gif',
   placeholder:(context, url) => new ProgressView(),
   errorWidget:(context, url, error) => new Icon(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.Registrar lookupKeyForAsset FlutterView getLookupKeyForAsset 得到;

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

pubspec.yaml

flutter:
assets:
  - icons/happy.png

Java plugin code

AssetManager assetManager = registrar.context().getAssets();
String key = registrar.lookupKeyForAsset("icons/happy.png");
AssetFileDescriptor fd = assetManager.openFd(key);

iOS

iOS 开发使用 mainbundle 获取 assets。

使用FlutterPluginRegistrar的 lookupKeyForAsset 和

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

FlutterViewControllerlookupKeyForAsset

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

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

Objective-C plugin

NSString* key = [registrar lookupKeyForAsset:@"icons/happy.png"];
NSString* path = [[NSBundle mainBundle] pathForResource:key ofType:nil];

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

源码分析

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

Image.network 的方法如下:

Image.network(
   String src, {
   Key key,
   double scale = 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);

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

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

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

Image 源码部分如下

class Image extends StatefulWidget {
/// 用于显示的 image
 final ImageProvider image;
 
..........
 @override
 _ImageState createState() => _ImageState();
}

_ImageState 类

class _ImageState extends State<Image> with WidgetsBindingObserver {
 ImageStream _imageStream;
 ImageInfo _imageInfo;
   
.......
@override
void initState() {
   super.initState();
   WidgetsBinding.instance.addObserver(this);
}
 
@override
void didChangeDependencies() {
   _updateInvertColors();
   _resolveImage();//解析图片从这里开始
   //设置和移除监听图片变化的回调
   if (TickerMode.of(context))
     _listenToStream();
   else
     _stopListeningToStream();
   super.didChangeDependencies();
}
 
void _resolveImage() {
   //根据 ImageConfiguration 调用 ImageProvider 的 resolve 函数获得 ImageStream 对象
   final ImageStream newStream =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方法,代码如下:

ImageStream resolve(ImageConfiguration configuration) {
 final ImageStream stream = ImageStream();
 T obtainedKey;
 bool didError = false;
 Future<void> handleError(dynamic exception, StackTrace stack) async {
   if (didError) {
     return;
  }
   didError = true;
   await null; // 等待事件轮询,以防侦听器被添加到图像流中。
 
   final _ErrorImageCompleter imageCompleter = _ErrorImageCompleter();
   stream.setCompleter(imageCompleter);
  ......
}
   
......
     
     Future<T> key;
     try {
       key = obtainKey(configuration);
    } catch (error, stackTrace) {
       return;
    }
     key.then<void>((T key) {
       obtainedKey = key;
       final ImageStreamCompleter completer = 
       PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key), onError:handleError);
       if (completer != null) {
         stream.setCompleter(completer);
      }
    }).catchError(handleError);
   
return stream;

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

ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), {ImageErrorListener onError }) {
   ImageStreamCompleter result = _pendingImages[key]?.completer;
   // 因为图像还没有加载,不需要做任何事情。
   if (result != null)
     return result;
   // 从缓存列表中根据Key删除对应的 imageprovider,便于将它移动到下面最近使用位置。
   final _CachedImage image = _cache.remove(key);
   if (image != null) {
     _cache[key] = image;
     return image.completer;
  }
   try {
     result = loader();
  } catch (error, stackTrace) {
    ......
  }
   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);
     // 如果图像大于最大缓存大小,且缓存大小不为零,则将缓存大小增加到图像大小加上 1000。
     // 思考点:一直这么加什么时候引起崩溃?
     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);
     // 移除 [_PendingImage.removeListener] 上的监听
     result.addListener(streamListener);
  }
   return result;
}

load

/// 拉取网络图片的 image_provider.NetworkImage 具体实现.
class NetworkImage extendsimage_provider.ImageProvider<image_provider.NetworkImage> implementsimage_provider.NetworkImage {
......................
 @override
 ImageStreamCompleter load(image_provider.NetworkImage key) {
   
   final StreamController<ImageChunkEvent> chunkEvents =StreamController<ImageChunkEvent>();
   
   return MultiFrameImageStreamCompleter(
       
     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(
   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 image_provider.NetworkImageLoadException(statusCode:response.statusCode, uri: resolved);
       //将网络返回的 response 信息,转换成内存中的 Uint8List bytes。这里面有解压 gzip 的逻辑。
     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 PaintingBinding.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({
   @required Future<ui.Codec> codec,
   @required double scale,
   Stream<ImageChunkEvent> chunkEvents,
   InformationCollector informationCollector,
}) : assert(codec != null),
      _informationCollector = informationCollector,
      _scale = scale {
   codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) {
    ..........
  });

_handelCodecReady

这里 codec 异步回调次方法

void _handleCodecReady(ui.Codec codec) {
   _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(ImageInfo imageInfo) {
   setImage(imageInfo);
   _framesEmitted += 1;
}
 
 @protected
 void setImage(ImageInfo image) {
   _currentImage = image;
   if (_listeners.isEmpty)
     return;
   // 复制一份以允许并发修改。
final List<ImageStreamListener> localListeners =List<ImageStreamListener>.from(_listeners);
     
   for (ImageStreamListener listener in localListeners) {
     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([ImageLoadingBuilder loadingBuilder]) {
   loadingBuilder ??= widget.loadingBuilder;
   return ImageStreamListener(
     _handleImageFrame,
     onChunk: loadingBuilder == null ? null : _handleImageChunk,
  );
}
 void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {
   setState(() {
     _imageInfo = imageInfo;
     _loadingProgress = null;
     _frameNumber = _frameNumber == null ? 0 : _frameNumber + 1;
     _wasSynchronouslyLoaded |= synchronousCall;
  });
}

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

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

总结

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

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

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

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

推荐阅读



推荐阅读
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • 在重复造轮子的情况下用ProxyServlet反向代理来减少工作量
    像不少公司内部不同团队都会自己研发自己工具产品,当各个产品逐渐成熟,到达了一定的发展瓶颈,同时每个产品都有着自己的入口,用户 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • 本文介绍了在Python3中如何使用选择文件对话框的格式打开和保存图片的方法。通过使用tkinter库中的filedialog模块的asksaveasfilename和askopenfilename函数,可以方便地选择要打开或保存的图片文件,并进行相关操作。具体的代码示例和操作步骤也被提供。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • Linux重启网络命令实例及关机和重启示例教程
    本文介绍了Linux系统中重启网络命令的实例,以及使用不同方式关机和重启系统的示例教程。包括使用图形界面和控制台访问系统的方法,以及使用shutdown命令进行系统关机和重启的句法和用法。 ... [详细]
  • 本文介绍了Redis的基础数据结构string的应用场景,并以面试的形式进行问答讲解,帮助读者更好地理解和应用Redis。同时,描述了一位面试者的心理状态和面试官的行为。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • eclipse学习(第三章:ssh中的Hibernate)——11.Hibernate的缓存(2级缓存,get和load)
    本文介绍了eclipse学习中的第三章内容,主要讲解了ssh中的Hibernate的缓存,包括2级缓存和get方法、load方法的区别。文章还涉及了项目实践和相关知识点的讲解。 ... [详细]
  • Android自定义控件绘图篇之Paint函数大汇总
    本文介绍了Android自定义控件绘图篇中的Paint函数大汇总,包括重置画笔、设置颜色、设置透明度、设置样式、设置宽度、设置抗锯齿等功能。通过学习这些函数,可以更好地掌握Paint的用法。 ... [详细]
  • 篇首语:本文由编程笔记#小编为大家整理,主要介绍了软件测试知识点之数据库压力测试方法小结相关的知识,希望对你有一定的参考价值。 ... [详细]
  • Asp.net Mvc Framework 七 (Filter及其执行顺序) 的应用示例
    本文介绍了在Asp.net Mvc中应用Filter功能进行登录判断、用户权限控制、输出缓存、防盗链、防蜘蛛、本地化设置等操作的示例,并解释了Filter的执行顺序。通过示例代码,详细说明了如何使用Filter来实现这些功能。 ... [详细]
  • 全面介绍Windows内存管理机制及C++内存分配实例(四):内存映射文件
    本文旨在全面介绍Windows内存管理机制及C++内存分配实例中的内存映射文件。通过对内存映射文件的使用场合和与虚拟内存的区别进行解析,帮助读者更好地理解操作系统的内存管理机制。同时,本文还提供了相关章节的链接,方便读者深入学习Windows内存管理及C++内存分配实例的其他内容。 ... [详细]
  • Centos下安装memcached+memcached教程
    本文介绍了在Centos下安装memcached和使用memcached的教程,详细解释了memcached的工作原理,包括缓存数据和对象、减少数据库读取次数、提高网站速度等。同时,还对memcached的快速和高效率进行了解释,与传统的文件型数据库相比,memcached作为一个内存型数据库,具有更高的读取速度。 ... [详细]
author-avatar
手机用户2502861065
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有