Flutter cached_network_image 圖片加載流程分析

前言

一天測試小姐姐拿着手機過來說,你這裏圖片下載有問題呀,爲什麼沒有網絡(開飛行模式)也彈Toast提示下載成功呀?

下意識反應,肯定是Toast提示彈早了,剛點擊按鈕,還沒開始下載就彈了Toast,趕緊拿手機過來操作驗證一波。確實沒有網絡,彈了下載完成提示,去相冊檢查一下,嗯?圖片下載成功了,還有這種操作?趕緊檢查一下代碼,發現項目中使用的cached_network_image三方庫加載的圖片,從名字上可以判斷,這是一個緩存網絡圖片的加載框架。所以應該是圖片顯示出來以後就被緩存到本地了,實際下載的流程並未走網絡請求,爲了驗證想法,看了下框架加載圖片流程,總結出下文。

使用

組件CachedNetworkImage可以支持直接使用或者通過ImageProvider

引入依賴

dependencies:
  cached_network_image: ^3.1.0

執行flutter pub get,項目中使用

Import it

import 'package:cached_network_image/cached_network_image.dart';

添加佔位圖

CachedNetworkImage(
        imageUrl: "http://via.placeholder.com/350x150",
        placeholder: (context, url) => CircularProgressIndicator(),
        errorWidget: (context, url, error) => Icon(Icons.error),
     ),

進度條展示

CachedNetworkImage(
        imageUrl: "http://via.placeholder.com/350x150",
        progressIndicatorBuilder: (context, url, downloadProgress) => 
                CircularProgressIndicator(value: downloadProgress.progress),
        errorWidget: (context, url, error) => Icon(Icons.error),
     ),

原生組件Image配合

Image(image: CachedNetworkImageProvider(url))

使用佔位圖並提供provider給其他組件使用

CachedNetworkImage(
  imageUrl: "http://via.placeholder.com/200x150",
  imageBuilder: (context, imageProvider) => Container(
    decoration: BoxDecoration(
      image: DecorationImage(
          image: imageProvider,
          fit: BoxFit.cover,
          colorFilter:
              ColorFilter.mode(Colors.red, BlendMode.colorBurn)),
    ),
  ),
  placeholder: (context, url) => CircularProgressIndicator(),
  errorWidget: (context, url, error) => Icon(Icons.error),
),

這樣就可以加載網絡圖片了,而且,圖片加載完成時,就被緩存到本地了,首先看下圖片的加載流程

官網說了,它現在不包含緩存,緩存功能實際上是另一個庫flutter_cache_manager中實現的

原理

加載&顯示

這裏我們僅梳理圖片加載和緩存的主流程,對於一些其他分支流程,或無關參數不做過多分析

首先,頁面上使用的構造函數接收了一個必傳參數imageUrl,用於生成ImageProvider提供圖片加載

class CachedNetworkImage extends StatelessWidget{
  /// image提供
  final CachedNetworkImageProvider _image;

  /// 構造函數
  CachedNetworkImage({
    Key key,
    @required this.imageUrl,
    /// 省略部分
    this.cacheManager,
    /// ...
  })  : assert(imageUrl != null),
        /// ...
        _image = CachedNetworkImageProvider(
          imageUrl,
          headers: httpHeaders,
          cacheManager: cacheManager,
          cacheKey: cacheKey,
          imageRenderMethodForWeb: imageRenderMethodForWeb,
          maxWidth: maxWidthDiskCache,
          maxHeight: maxHeightDiskCache,
        ),
        super(key: key);
  
  @override
  Widget build(BuildContext context) {
    var octoPlaceholderBuilder =
        placeholder != null ? _octoPlaceholderBuilder : null;
    var octoProgressIndicatorBuilder =
        progressIndicatorBuilder != null ? _octoProgressIndicatorBuilder : null;
    /// ...

    return OctoImage(
      image: _image,
      /// ...
    );
  }
}

這裏可以看到,構造函數初始化了一個本地變量_image 類型是CachedNetworkImageProvider,它繼承ImageProvider提供圖片加載,看下它的構造函數

/// 提供網絡圖片加載Provider並緩存
abstract class CachedNetworkImageProvider
    extends ImageProvider<CachedNetworkImageProvider> {
  /// Creates an object that fetches the image at the given URL.
  const factory CachedNetworkImageProvider(
    String url, {
    int maxHeight,
    int maxWidth,
    String cacheKey,
    double scale,
    @Deprecated('ErrorListener is deprecated, use listeners on the imagestream')
        ErrorListener errorListener,
    Map<String, String> headers,
    BaseCacheManager cacheManager,
    ImageRenderMethodForWeb imageRenderMethodForWeb,
  }) = image_provider.CachedNetworkImageProvider;

  /// 可選cacheManager. 默認使用 DefaultCacheManager()
  /// 當運行在web時,cacheManager沒有使用.
  BaseCacheManager get cacheManager;

  /// 請求url.
  String get url;

  /// 緩存key
  String get cacheKey;
  
  /// ...

  @override
  ImageStreamCompleter load(
      CachedNetworkImageProvider key, DecoderCallback decode);
}

它的構造函數調用了image_provider.CachedNetworkImageProvider的實例在_image_provider_io.dart中是加載的具體實現類

/// IO implementation of the CachedNetworkImageProvider; the ImageProvider to
/// load network images using a cache.
class CachedNetworkImageProvider
    extends ImageProvider<image_provider.CachedNetworkImageProvider>
    implements image_provider.CachedNetworkImageProvider {
  /// Creates an ImageProvider which loads an image from the [url], using the [scale].
  /// When the image fails to load [errorListener] is called.
  const CachedNetworkImageProvider(
    this.url, {
  /// ...
  })  : assert(url != null),
        assert(scale != null);

  @override
  final BaseCacheManager cacheManager;
    /// ...

  @override
  Future<CachedNetworkImageProvider> obtainKey(
      ImageConfiguration configuration) {
    return SynchronousFuture<CachedNetworkImageProvider>(this);
  }

  /// 核心方法加載圖片入口
  @override
  ImageStreamCompleter load(
      image_provider.CachedNetworkImageProvider key, DecoderCallback decode) {
    final chunkEvents = StreamController<ImageChunkEvent>();
    /// 多圖加載
    return MultiImageStreamCompleter(
      codec: _loadAsync(key, chunkEvents, decode),
      chunkEvents: chunkEvents.stream,
      scale: key.scale,
      informationCollector: () sync* {
        yield DiagnosticsProeperty<ImageProvider>(
          'Image provider: $this \n Image key: $key',
          this,
          style: DiagnosticsTreeStyle.errorProperty,
        );
      },
    );
  }

這裏的load方法即是圖片加載的啓動入口,它會在頁面可見時被調用

它返回了一個MultiImageStreamCompleter傳入_loadAsync,看下這個方法

 /// 異步加載
  Stream<ui.Codec> _loadAsync(
    CachedNetworkImageProvider key,
    StreamController<ImageChunkEvent> chunkEvents,
    DecoderCallback decode,
  ) async* {
    assert(key == this);
    try {
      /// 默認緩存管理器
      var mngr = cacheManager ?? DefaultCacheManager();
      assert(
          mngr is ImageCacheManager || (maxWidth == null && maxHeight == null),
          'To resize the image with a CacheManager the '
          'CacheManager needs to be an ImageCacheManager. maxWidth and '
          'maxHeight will be ignored when a normal CacheManager is used.');

      /// 下載邏輯放在ImageCacheManager,得到下載stream
      var stream = mngr is ImageCacheManager
          ? mngr.getImageFile(key.url,
              maxHeight: maxHeight,
              maxWidth: maxWidth,
              withProgress: true,
              headers: headers,
              key: key.cacheKey)
          : mngr.getFileStream(key.url,
              withProgress: true, headers: headers, key: key.cacheKey);

      await for (var result in stream) {
        if (result is FileInfo) {
          var file = result.file;
          var bytes = await file.readAsBytes();
          var decoded = await decode(bytes);
          /// 下載完成返回結果
          yield decoded;
        }
      }
    } catch (e) {
      /// ...
    } finally {
      await chunkEvents.close();
    }
  }
}

這裏我們看到了默認緩存管理器cacheManager創建的地方,爲DefaultCacheManager,那麼它如何緩存的呢,後邊再分析。

下載的邏輯也是放在了ImageCacheManager下了,返回結果是一個stream完成多圖下載的支持,下載完成通過yield 返回給ui解碼最終顯示。

MultiImageStreamCompleter支持多圖加載繼承自ImageStreamCompleter

/// An ImageStreamCompleter with support for loading multiple images.
class MultiImageStreamCompleter extends ImageStreamCompleter {
  /// The constructor to create an MultiImageStreamCompleter. The [codec]
  /// should be a stream with the images that should be shown. The
  /// [chunkEvents] should indicate the [ImageChunkEvent]s of the first image
  /// to show.
  MultiImageStreamCompleter({
    @required Stream<ui.Codec> codec,
    @required double scale,
    Stream<ImageChunkEvent> chunkEvents,
    InformationCollector informationCollector,
  })  : assert(codec != null),
        _informationCollector = informationCollector,
        _scale = scale {
    /// 顯示邏輯
    codec.listen((event) {
      if (_timer != null) {
        _nextImageCodec = event;
      } else {
        _handleCodecReady(event);
      }
    }, onError: (dynamic error, StackTrace stack) {
      reportError(
        context: ErrorDescription('resolving an image codec'),
        exception: error,
        stack: stack,
        informationCollector: informationCollector,
        silent: true,
      );
    });
    /// ...
    }
  }
  /// 處理解碼完成
  void _handleCodecReady(ui.Codec codec) {
    _codec = codec;
    assert(_codec != null);

    if (hasListeners) {
      _decodeNextFrameAndSchedule();
    }
  }
  /// 解碼下一幀並繪製
  Future<void> _decodeNextFrameAndSchedule() async {
    try {
      _nextFrame = await _codec.getNextFrame();
    } catch (exception, stack) {
      reportError(
        context: ErrorDescription('resolving an image frame'),
        exception: exception,
        stack: stack,
        informationCollector: _informationCollector,
        silent: true,
      );
      return;
    }
    if (_codec.frameCount == 1) {
      // ImageStreamCompleter listeners removed while waiting for next frame to
      // be decoded.
      // There's no reason to emit the frame without active listeners.
      if (!hasListeners) {
        return;
      }

      // This is not an animated image, just return it and don't schedule more
      // frames.
      _emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale));
      return;
    }
    _scheduleAppFrame();
  }
}

這裏做了顯示邏輯,和最終轉化成flutter上幀的處理,_scheduleAppFrame完成發送幀的處理

下載&緩存

上邊的mngr調用了ImageCacheManager中的getImageFile方法現在就到了flutter_cache_manager這個三方庫當中,它是被隱式依賴的,文件是image_cache_manager.dart

mixin ImageCacheManager on BaseCacheManager {
    Stream<FileResponse> getImageFile(
        String url, {
        String key,
        Map<String, String> headers,
        bool withProgress,
        int maxHeight,
        int maxWidth,
      }) async* {
        if (maxHeight == null && maxWidth == null) {
          yield* getFileStream(url,
              key: key, headers: headers, withProgress: withProgress);
          return;
        }
      /// ...
     }
  /// ...
}

getFileStream方法實現在子類cache_manager.dart文件中的CacheManager

class CacheManager implements BaseCacheManager {
     /// 緩存管理
  CacheStore _store;

  /// Get the underlying store helper
  CacheStore get store => _store;

  /// 下載管理
  WebHelper _webHelper;

  /// Get the underlying web helper
  WebHelper get webHelper => _webHelper;
  
  /// 從下載或者緩存讀取file返回stream
  @override
  Stream<FileResponse> getFileStream(String url,
      {String key, Map<String, String> headers, bool withProgress}) {
    key ??= url;
    final streamController = StreamController<FileResponse>();
    _pushFileToStream(
        streamController, url, key, headers, withProgress ?? false);
    return streamController.stream;
  }
  
  Future<void> _pushFileToStream(StreamController streamController, String url,
      String key, Map<String, String> headers, bool withProgress) async {
    key ??= url;
    FileInfo cacheFile;
    try {
      /// 緩存判斷
      cacheFile = await getFileFromCache(key);
      if (cacheFile != null) {
        /// 有緩存直接返回
        streamController.add(cacheFile);
        withProgress = false;
      }
    } catch (e) {
      print(
          'CacheManager: Failed to load cached file for $url with error:\n$e');
    }
    /// 沒有緩存或者過期下載
    if (cacheFile == null || cacheFile.validTill.isBefore(DateTime.now())) {
      try {
        await for (var response
            in _webHelper.downloadFile(url, key: key, authHeaders: headers)) {
          if (response is DownloadProgress && withProgress) {
            streamController.add(response);
          }
          if (response is FileInfo) {
            streamController.add(response);
          }
        }
      } catch (e) {
        assert(() {
          print(
              'CacheManager: Failed to download file from $url with error:\n$e');
          return true;
        }());
        if (cacheFile == null && streamController.hasListener) {
          streamController.addError(e);
        }
      }
    }
    unawaited(streamController.close());
  }
}

緩存判斷邏輯在CacheStore提供兩級緩存


class CacheStore {
  Duration cleanupRunMinInterval = const Duration(seconds: 10);
    /// 未下載完成緩存
  final _futureCache = <String, Future<CacheObject>>{};
  /// 已下載完緩存
  final _memCache = <String, CacheObject>{};

  /// ...

  Future<FileInfo> getFile(String key, {bool ignoreMemCache = false}) async {
    final cacheObject =
        await retrieveCacheData(key, ignoreMemCache: ignoreMemCache);
    if (cacheObject == null || cacheObject.relativePath == null) {
      return null;
    }
    final file = await fileSystem.createFile(cacheObject.relativePath);
    return FileInfo(
      file,
      FileSource.Cache,
      cacheObject.validTill,
      cacheObject.url,
    );
  }

  Future<void> putFile(CacheObject cacheObject) async {
    _memCache[cacheObject.key] = cacheObject;
    await _updateCacheDataInDatabase(cacheObject);
  }

  Future<CacheObject> retrieveCacheData(String key,
      {bool ignoreMemCache = false}) async {
    /// 判斷是否已緩存過
    if (!ignoreMemCache && _memCache.containsKey(key)) {
      if (await _fileExists(_memCache[key])) {
        return _memCache[key];
      }
    }
    /// 未緩存的 已加入futureCache中的key直接返回
    if (!_futureCache.containsKey(key)) {
      final completer = Completer<CacheObject>();
      /// 未加入的添加到futureCache
      unawaited(_getCacheDataFromDatabase(key).then((cacheObject) async {
        if (cacheObject != null && !await _fileExists(cacheObject)) {
          final provider = await _cacheInfoRepository;
          await provider.delete(cacheObject.id);
          cacheObject = null;
        }

        _memCache[key] = cacheObject;
        completer.complete(cacheObject);
        unawaited(_futureCache.remove(key));
      }));
      _futureCache[key] = completer.future;
    }
    return _futureCache[key];
  }
    /// ...
  /// 更新到數據庫
  Future<dynamic> _updateCacheDataInDatabase(CacheObject cacheObject) async {
    final provider = await _cacheInfoRepository;
    return provider.updateOrInsert(cacheObject);
  }
}

_cacheInfoRepository緩存倉庫是CacheObjectProvider使用的數據庫緩存對象

class CacheObjectProvider extends CacheInfoRepository
    with CacheInfoRepositoryHelperMethods {
  Database db;
  String _path;
  String databaseName;

  CacheObjectProvider({String path, this.databaseName}) : _path = path;

    /// 打開
  @override
  Future<bool> open() async {
    if (!shouldOpenOnNewConnection()) {
      return openCompleter.future;
    }
    var path = await _getPath();
    await File(path).parent.create(recursive: true);
    db = await openDatabase(path, version: 3,
        onCreate: (Database db, int version) async {
      await db.execute('''
      create table $_tableCacheObject ( 
        ${CacheObject.columnId} integer primary key, 
        ${CacheObject.columnUrl} text, 
        ${CacheObject.columnKey} text, 
        ${CacheObject.columnPath} text,
        ${CacheObject.columnETag} text,
        ${CacheObject.columnValidTill} integer,
        ${CacheObject.columnTouched} integer,
        ${CacheObject.columnLength} integer
        );
        create unique index $_tableCacheObject${CacheObject.columnKey} 
        ON $_tableCacheObject (${CacheObject.columnKey});
      ''');
    }, onUpgrade: (Database db, int oldVersion, int newVersion) async {
      /// ...
    return opened();
  }

  @override
  Future<dynamic> updateOrInsert(CacheObject cacheObject) {
    if (cacheObject.id == null) {
      return insert(cacheObject);
    } else {
      return update(cacheObject);
    }
  }

  @override
  Future<CacheObject> insert(CacheObject cacheObject,
      {bool setTouchedToNow = true}) async {
    var id = await db.insert(
      _tableCacheObject,
      cacheObject.toMap(setTouchedToNow: setTouchedToNow),
    );
    return cacheObject.copyWith(id: id);
  }

  @override
  Future<CacheObject> get(String key) async {
    List<Map> maps = await db.query(_tableCacheObject,
        columns: null, where: '${CacheObject.columnKey} = ?', whereArgs: [key]);
    if (maps.isNotEmpty) {
      return CacheObject.fromMap(maps.first.cast<String, dynamic>());
    }
    return null;
  }

  @override
  Future<int> delete(int id) {
    return db.delete(_tableCacheObject,
        where: '${CacheObject.columnId} = ?', whereArgs: [id]);
  }
}

可見數據庫緩存的是CacheObject對象,保存了url、key、relativePath等信息

class CacheObject {
  static const columnId = '_id';
  static const columnUrl = 'url';
  static const columnKey = 'key';
  static const columnPath = 'relativePath';
  static const columnETag = 'eTag';
  static const columnValidTill = 'validTill';
  static const columnTouched = 'touched';
  static const columnLength = 'length';
}

沒有緩存下調用了_webHelper.downloadFile方法

class WebHelper {
  WebHelper(this._store, FileService fileFetcher)
      : _memCache = {},
        fileFetcher = fileFetcher ?? HttpFileService();

  final CacheStore _store;
  @visibleForTesting
  final FileService fileFetcher;
  final Map<String, BehaviorSubject<FileResponse>> _memCache;
  final Queue<QueueItem> _queue = Queue();

  ///Download the file from the url
  Stream<FileResponse> downloadFile(String url,
      {String key,
      Map<String, String> authHeaders,
      bool ignoreMemCache = false}) {
    key ??= url;
    if (!_memCache.containsKey(key) || ignoreMemCache) {
      var subject = BehaviorSubject<FileResponse>();
      _memCache[key] = subject;
      /// 下載或者加入隊列
      unawaited(_downloadOrAddToQueue(url, key, authHeaders));
    }
    return _memCache[key].stream;
  }
  
  Future<void> _downloadOrAddToQueue(
    String url,
    String key,
    Map<String, String> authHeaders,
  ) async {
    //如果太多請求被執行,加入隊列等待
    if (concurrentCalls >= fileFetcher.concurrentFetches) {
      _queue.add(QueueItem(url, key, authHeaders));
      return;
    }

    concurrentCalls++;
    var subject = _memCache[key];
    try {
      await for (var result
          in _updateFile(url, key, authHeaders: authHeaders)) {
        subject.add(result);
      }
    } catch (e, stackTrace) {
      subject.addError(e, stackTrace);
    } finally {
      concurrentCalls--;
      await subject.close();
      _memCache.remove(key);
      _checkQueue();
    }
  }
  
   ///下載資源
  Stream<FileResponse> _updateFile(String url, String key,
      {Map<String, String> authHeaders}) async* {
    var cacheObject = await _store.retrieveCacheData(key);
    cacheObject = cacheObject == null
        ? CacheObject(url, key: key)
        : cacheObject.copyWith(url: url);
    /// 請求得到response
    final response = await _download(cacheObject, authHeaders);
    yield* _manageResponse(cacheObject, response);
  }
  
  
  Stream<FileResponse> _manageResponse(
      CacheObject cacheObject, FileServiceResponse response) async* {
    /// ...
    if (statusCodesNewFile.contains(response.statusCode)) {
      int savedBytes;
      await for (var progress in _saveFile(newCacheObject, response)) {
        savedBytes = progress;
        yield DownloadProgress(
            cacheObject.url, response.contentLength, progress);
      }
      newCacheObject = newCacheObject.copyWith(length: savedBytes);
    }
        /// 加入緩存
    unawaited(_store.putFile(newCacheObject).then((_) {
      if (newCacheObject.relativePath != oldCacheObject.relativePath) {
        _removeOldFile(oldCacheObject.relativePath);
      }
    }));

    final file = await _store.fileSystem.createFile(
      newCacheObject.relativePath,
    );
    yield FileInfo(
      file,
      FileSource.Online,
      newCacheObject.validTill,
      newCacheObject.url,
    );
  }
  
  Stream<int> _saveFile(CacheObject cacheObject, FileServiceResponse response) {
    var receivedBytesResultController = StreamController<int>();
    unawaited(_saveFileAndPostUpdates(
      receivedBytesResultController,
      cacheObject,
      response,
    ));
    return receivedBytesResultController.stream;
  }
  
  Future _saveFileAndPostUpdates(
      StreamController<int> receivedBytesResultController,
      CacheObject cacheObject,
      FileServiceResponse response) async {
    /// 根據路徑創建file
    final file = await _store.fileSystem.createFile(cacheObject.relativePath);

    try {
      var receivedBytes = 0;
      /// 寫文件
      final sink = file.openWrite();
      await response.content.map((s) {
        receivedBytes += s.length;
        receivedBytesResultController.add(receivedBytes);
        return s;
      }).pipe(sink);
    } catch (e, stacktrace) {
      receivedBytesResultController.addError(e, stacktrace);
    }
    await receivedBytesResultController.close();
  }

}

總結

cached_network_image圖片加載流程依賴ImageProvider,緩存和下載邏輯放在另一個庫flutter_cache_manager下載文件在WebHelper中提供隊列管理,依賴傳入FileService做具體獲取文件方便擴展默認實現HttpFileService,下載完成後路徑保存在CacheObject保存在sqflite數據庫

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章