国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 系統 > Android > 正文

Flutter中網絡圖片加載和緩存的實現

2019-12-12 00:16:34
字體:
來源:轉載
供稿:網友

前言

應用開發中經常會碰到網絡圖片的加載,通常我們會對圖片進行緩存,以便下次加載同一張圖片時不用再重新下載,在包含有大量圖片的應用中,會大幅提高圖片展現速度、提升用戶體驗且為用戶節省流量。Flutter本身提供的Image Widget已經實現了加載網絡圖片的功能,且具備內存緩存的機制,接下來一起看一下Image的網絡圖片加載的實現。

重溫小部件Image

常用小部件Image中實現了幾種構造函數,已經足夠我們日常開發中各種場景下創建Image對象使用了。

有參構造函數:

Image(Key key, @required this.image, ...)

開發者可根據自定義的ImageProvider來創建Image。

命名構造函數:

Image.network(String src, ...)

src即是根據網絡獲取的圖片url地址。

Image.file(File file, ...)

file指本地一個圖片文件對象,安卓中需要android.permission.READ_EXTERNAL_STORAGE權限。

Image.asset(String name, ...)

name指項目中添加的圖片資源名,事先在pubspec.yaml文件中有聲明。

Image.memory(Uint8List bytes, ...)

bytes指內存中的圖片數據,將其轉化為圖片對象。

其中Image.network就是我們本篇分享的重點 -- 加載網絡圖片。

Image.network源碼分析

下面通過源碼我們來看下Image.network加載網絡圖片的具體實現。

 Image.network(String src, {  Key key,  double scale = 1.0,  .  . }) : image = NetworkImage(src, scale: scale, headers: headers),    assert(alignment != null),    assert(repeat != null),    assert(matchTextDirection != null),    super(key: key); /// The image to display. final ImageProvider image;

首先,使用Image.network命名構造函數創建Image對象時,會同時初始化實例變量image,image是一個ImageProvider對象,該ImageProvider就是我們所需要的圖片的提供者,它本身是一個抽象類,子類包括NetworkImage、FileImage、ExactAssetImage、AssetImage、MemoryImage等,網絡加載圖片使用的就是NetworkImage。

Image作為一個StatefulWidget其狀態由_ImageState控制,_ImageState繼承自State類,其生命周期方法包括initState()、didChangeDependencies()、build()、deactivate()、dispose()、didUpdateWidget()等。我們重點來_ImageState中函數的執行。

由于插入渲染樹時會先調用initState()函數,然后調用didChangeDependencies()函數,_ImageState中并沒有重寫initState()函數,所以didChangeDependencies()函數會執行,看下didChangeDependencies()里的內容

@override void didChangeDependencies() {  _invertColors = MediaQuery.of(context, nullOk: true)?.invertColors   ?? SemanticsBinding.instance.accessibilityFeatures.invertColors;  _resolveImage();  if (TickerMode.of(context))   _listenToStream();  else   _stopListeningToStream();  super.didChangeDependencies(); }_resolveImage()會被調用,函數內容如下 void _resolveImage() {  final ImageStream newStream =   widget.image.resolve(createLocalImageConfiguration(     context,     size: widget.width != null && widget.height != null ? Size(widget.width, widget.height) : null   ));  assert(newStream != null);  _updateSourceStream(newStream); }

函數中先創建了一個ImageStream對象,該對象是一個圖片資源的句柄,其持有著圖片資源加載完畢后的監聽回調和圖片資源的管理者。而其中的ImageStreamCompleter對象就是圖片資源的一個管理類,也就是說,_ImageState通過ImageStream和ImageStreamCompleter管理類建立了聯系。

再回頭看一下ImageStream對象是通過widget.image.resolve方法創建的,也就是對應NetworkImage的resolve方法,我們查看NetworkImage類的源碼發現并沒有resolve方法,于是查找其父類,在ImageProvider類中找到了。

 ImageStream resolve(ImageConfiguration configuration) {  assert(configuration != null);  final ImageStream stream = ImageStream();  T obtainedKey;  Future<void> handleError(dynamic exception, StackTrace stack) async {   .   .  }  obtainKey(configuration).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; }

ImageStream中的圖片管理者ImageStreamCompleter通過PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key), onError: handleError);方法創建,imageCache是Flutter框架中實現的用于圖片緩存的單例,查看其中的putIfAbsent方法

 ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) {  assert(key != null);  assert(loader != null);  ImageStreamCompleter result = _pendingImages[key]?.completer;  // Nothing needs to be done because the image hasn't loaded yet.  if (result != null)   return result;  // Remove the provider from the list so that we can move it to the  // recently used position below.  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) {   // Images that fail to load don't contribute to cache size.   final int imageSize = info?.image == null ? 0 : info.image.height * info.image.width * 4;   final _CachedImage image = _CachedImage(result, imageSize);   // If the image is bigger than the maximum cache size, and the cache size   // is not zero, then increase the cache size to the size of the image plus   // some change.   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) {   _pendingImages[key] = _PendingImage(result, listener);   result.addListener(listener);  }  return result; }

通過以上代碼可以看到會通過key來查找緩存中是否存在,如果存在則返回,如果不存在則會通過執行loader()方法創建圖片資源管理者,而后再將緩存圖片資源的監聽方法注冊到新建的圖片管理者中以便圖片加載完畢后做緩存處理。

根據上面的代碼調用PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key), onError: handleError);看出load()方法由ImageProvider對象實現,這里就是NetworkImage對象,看下其具體實現代碼

 @override ImageStreamCompleter load(NetworkImage key) {  return MultiFrameImageStreamCompleter(   codec: _loadAsync(key),   scale: key.scale,   informationCollector: (StringBuffer information) {    information.writeln('Image provider: $this');    information.write('Image key: $key');   }  ); }

代碼中其就是創建一個MultiFrameImageStreamCompleter對象并返回,這是一個多幀圖片管理器,表明Flutter是支持GIF圖片的。創建對象時的codec變量由_loadAsync方法的返回值初始化,查看該方法內容

 static final HttpClient _httpClient = HttpClient(); Future<ui.Codec> _loadAsync(NetworkImage key) async {  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 Exception('HTTP request failed, statusCode: ${response?.statusCode}, $resolved');  final Uint8List bytes = await consolidateHttpClientResponseBytes(response);  if (bytes.lengthInBytes == 0)   throw Exception('NetworkImage is an empty file: $resolved');  return PaintingBinding.instance.instantiateImageCodec(bytes); }

這里才是關鍵,就是通過HttpClient對象對指定的url進行下載操作,下載完成后根據圖片二進制數據實例化圖像編解碼器對象Codec,然后返回。

那么圖片下載完成后是如何顯示到界面上的呢,下面看下MultiFrameImageStreamCompleter的構造方法實現

 MultiFrameImageStreamCompleter({  @required Future<ui.Codec> codec,  @required double scale,  InformationCollector informationCollector }) : assert(codec != null),    _informationCollector = informationCollector,    _scale = scale,    _framesEmitted = 0,    _timer = null {  codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) {   reportError(    context: 'resolving an image codec',    exception: error,    stack: stack,    informationCollector: informationCollector,    silent: true,   );  }); }

看,構造方法中的代碼塊,codec的異步方法執行完成后會調用_handleCodecReady函數,函數內容如下

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

方法中會將codec對象保存起來,然后解碼圖片幀

 Future<void> _decodeNextFrameAndSchedule() async {  try {   _nextFrame = await _codec.getNextFrame();  } catch (exception, stack) {   reportError(    context: 'resolving an image frame',    exception: exception,    stack: stack,    informationCollector: _informationCollector,    silent: true,   );   return;  }  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));   return;  }  SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame); }

如果圖片是png或jpg只有一幀,則執行_emitFrame函數,從幀數據中拿到圖片幀對象根據縮放比例創建ImageInfo對象,然后設置顯示的圖片信息

 void _emitFrame(ImageInfo imageInfo) {  setImage(imageInfo);  _framesEmitted += 1; }  /// Calls all the registered listeners to notify them of a new image. @protected void setImage(ImageInfo image) {  _currentImage = image;  if (_listeners.isEmpty)   return;  final List<ImageListener> localListeners = _listeners.map<ImageListener>(   (_ImageListenerPair listenerPair) => listenerPair.listener  ).toList();  for (ImageListener listener in localListeners) {   try {    listener(image, false);   } catch (exception, stack) {    reportError(     context: 'by an image listener',     exception: exception,     stack: stack,    );   }  } }

這時就會根據添加的監聽器來通知一個新的圖片需要渲染。那么這個監聽器是什么時候添加的呢,我們回頭看一下_ImageState類中的didChangeDependencies()方法內容,執行完_resolveImage();后會執行_listenToStream();方法

 void _listenToStream() {  if (_isListeningToStream)   return;  _imageStream.addListener(_handleImageChanged);  _isListeningToStream = true; }

該方法就向ImageStream對象中添加了監聽器_handleImageChanged,監聽方法如下

 void _handleImageChanged(ImageInfo imageInfo, bool synchronousCall) {  setState(() {   _imageInfo = imageInfo;  }); }

最終就是調用setState方法來通知界面刷新,將下載到的圖片渲染到界面上來了。

實際問題

從以上源碼分析,我們應該清楚了整個網絡圖片從加載到顯示的過程,不過使用這種原生的方式我們發現網絡圖片只是進行了內存緩存,如果殺掉應用進程再重新打開后還是要重新下載圖片,這對于用戶而言,每次打開應用還是會消耗下載圖片的流量,不過我們可以從中學習到一些思路來自己設計網絡圖片加載框架,下面作者就簡單的基于Image.network來進行一下改造,增加圖片的磁盤緩存。

解決方案

我們通過源碼分析可知,圖片在緩存中未找到時,會通過網絡直接下載獲取,而下載的方法是在NetworkImage類中,于是我們可以參考NetworkImage來自定義一個ImageProvider。

代碼實現

拷貝一份NetworkImage的代碼到新建的network_image.dart文件中,在_loadAsync方法中我們加入磁盤緩存的代碼。

 static final CacheFileImage _cacheFileImage = CacheFileImage(); Future<ui.Codec> _loadAsync(NetworkImage key) async {  assert(key == this);/// 新增代碼塊start/// 從緩存目錄中查找圖片是否存在  final Uint8List cacheBytes = await _cacheFileImage.getFileBytes(key.url);  if(cacheBytes != null) {   return PaintingBinding.instance.instantiateImageCodec(cacheBytes);  }/// 新增代碼塊end  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('HTTP request failed, statusCode: ${response?.statusCode}, $resolved');/// 新增代碼塊start/// 將下載的圖片數據保存到指定緩存文件中  await _cacheFileImage.saveBytesToFile(key.url, bytes);/// 新增代碼塊end  return PaintingBinding.instance.instantiateImageCodec(bytes); }

代碼中注釋已經表明了基于原有代碼新增的代碼塊,CacheFileImage是自己定義的文件緩存類,完整代碼如下

import 'dart:convert';import 'dart:io';import 'dart:typed_data';import 'package:crypto/crypto.dart';import 'package:path_provider/path_provider.dart';class CacheFileImage { /// 獲取url字符串的MD5值 static String getUrlMd5(String url) {  var content = new Utf8Encoder().convert(url);  var digest = md5.convert(content);  return digest.toString(); } /// 獲取圖片緩存路徑 Future<String> getCachePath() async {  Directory dir = await getApplicationDocumentsDirectory();  Directory cachePath = Directory("${dir.path}/imagecache/");  if(!cachePath.existsSync()) {   cachePath.createSync();  }  return cachePath.path; } /// 判斷是否有對應圖片緩存文件存在 Future<Uint8List> getFileBytes(String url) async {  String cacheDirPath = await getCachePath();  String urlMd5 = getUrlMd5(url);  File file = File("$cacheDirPath/$urlMd5");  print("讀取文件:${file.path}");  if(file.existsSync()) {   return await file.readAsBytes();  }  return null; } /// 將下載的圖片數據緩存到指定文件 Future saveBytesToFile(String url, Uint8List bytes) async {  String cacheDirPath = await getCachePath();  String urlMd5 = getUrlMd5(url);  File file = File("$cacheDirPath/$urlMd5");  if(!file.existsSync()) {   file.createSync();   await file.writeAsBytes(bytes);  } }}

這樣就增加了文件緩存的功能,思路很簡單,就是在獲取網絡圖片之前先檢查一下本地文件緩存目錄中是否有緩存文件,如果有則不用再去下載,否則去下載圖片,下載完成后立即將下載到的圖片緩存到文件中供下次需要時使用。

工程的pubspec.yaml中需要增加以下依賴庫

dependencies: path_provider: ^0.4.1 crypto: ^2.0.6

自定義ImageProvider使用

在創建圖片Widget時使用帶參數的非命名構造函數,指定image參數為自定義ImageProvider對象即可,代碼示例如下

import 'imageloader/network_image.dart' as network; Widget getNetworkImage() {  return Container(   color: Colors.blue,   width: 200,   height: 200,   child: Image(image: network.NetworkImage("https://flutter.dev/images/flutter-mono-81x100.png")),  ); }

寫在最后

以上對Flutter中自帶的Image小部件的網絡圖片加載流程進行了源碼分析,了解了源碼的設計思路之后,我們新增了簡單的本地文件緩存功能,這使我們的網絡圖片加載同時具備了內存緩存和文件緩存兩種能力,大大提升了用戶體驗,如果其他同學有更好的方案可以給作者留言交流。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持武林網。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 苍梧县| 武隆县| 诏安县| 华阴市| 察雅县| 巍山| 同仁县| 盘锦市| 安丘市| 高安市| 江津市| 四会市| 河池市| 昂仁县| 九江市| 原平市| 大理市| 资阳市| 锦州市| 临泽县| 苏州市| 常宁市| 镇坪县| 乌什县| 临西县| 邹城市| 饶平县| 苍梧县| 依安县| 呼玛县| 济宁市| 克拉玛依市| 仲巴县| 新余市| 托克逊县| 彭山县| 台南市| 绥德县| 清水县| 海晏县| 五原县|