banner
jzman

jzman

Coding、思考、自觉。
github

Flutter系列之圖片加載詳解

PS:自律實踐起來真的沒有想像那麼簡單。

Flutter 支持加載的圖片類型:JPEG、PNG、GIF、WebP、BMP 和 WBMP,Flutter Image 的組件的必須參數是一個 ImageProviderImageProvider 是一個抽象類,具體獲取圖片由子類實現,本文將從如下幾個方面學習 Flutter 中的圖片加載:

  1. 圖片加載
  2. 圖片預加載
  3. 圖片緩存
  4. 清除圖片緩存
  5. 圖片加載進度監聽
  6. 加載圖片案例

圖片加載#

Flutter 本身實現了圖片加載,可以加載網絡、SD 卡、Asset、內存裡面的圖片,可以通過如下方式生成圖片資源對應的 Image:

Image.network(String src,{...});
Image.file(File file,{...});
Image.asset(String name,{...});
Image.memory(Uint8List bytes,{...});

下面以加載網絡圖片為例介紹 Flutter 中圖片加載的流程,查看 Image.network() 源碼如下:

Image.network(
// ...
}) : image = NetworkImage(src, scale: scale, headers: headers),
   assert(alignment != null),
   assert(repeat != null),
   assert(matchTextDirection != null),
   super(key: key);

使用 Image.network 生成 Image 的時候創建了 NetworkImageNetworkImage 類是 ImageProvider 的子類,ImageProvider 是一個抽象類,裡面提供了解析圖片資源的 resolve 方法、將圖片緩存移除的 evict 方法以及加載圖片的抽象方法 load 等,load 方法由子類具體實現,ImageProvider 源碼分析如下:

/// ImageProvider是一個抽象類,具體加載由子類實現
abstract class ImageProvider<T> {
  const ImageProvider();

  /// 使用提供的ImageConfiguration對象生成ImageStream
  ImageStream resolve(ImageConfiguration configuration) {
    assert(configuration != null);
    final ImageStream stream = ImageStream();
    T obtainedKey;

    //...代碼

    dangerZone.runGuarded(() {
      Future<T> key;
      try {
        // 獲取圖片資源對應的key
        key = obtainKey(configuration);
      } catch (error, stackTrace) {
        handleError(error, stackTrace);
        return;
      }
      key.then<void>((T key) {
        // 獲取到圖片資源對應的key
        obtainedKey = key;
        // 獲取key對應的ImageStreamCompleter,如果緩存中沒有則調用傳入的loader回調
        // 去加載並將其添加到緩存中
        final ImageStreamCompleter completer = PaintingBinding
            .instance.imageCache
            .putIfAbsent(key, () => load(key), onError: handleError);
        if (completer != null) {
          stream.setCompleter(completer);
        }
      }).catchError(handleError);
    });
    return stream;
  }

  /// 將圖片從緩存中移除,返回值為true表示移除成功
  Future<bool> evict(
      {ImageCache cache,
      ImageConfiguration configuration = ImageConfiguration.empty}) async {
    cache ??= imageCache;
    final T key = await obtainKey(configuration);
    return cache.evict(key);
  }

  /// 獲取對應圖片資源key,具體由子類實現
  Future<T> obtainKey(ImageConfiguration configuration);

  /// 根據key加載圖片並將其轉換為ImageStreamCompleter,具體由子類實現
  @protected
  ImageStreamCompleter load(T key);

  @override
  String toString() => '$runtimeType()';
}

resolve 方法中解析圖片資源使用 PaintingBinding 的單例獲取圖片緩存 imageCache 並調用 putIfAbsent 方法,裡面實現了 LRU 緩存基本邏輯,根據是否有緩存進行處理,如果有緩存則從緩存中獲取與之對應的圖片資源,反之則調用傳入的 loader 進行圖片加載並將加載的圖片添加到緩存 ImageCache 中。

繼續查看最終加載網絡圖片的 ImageProvider 的實現類 NetworkImageload 方法實現如下:

@override
ImageStreamCompleter load(image_provider.NetworkImage key) {

    final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
    // _loadAsync方法
    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),
        ];
      },
    );
}

load 方法中調用了 _loadAsync,這也是真正的下載圖片的方法,同時要對圖片進行解碼返回,_loadAsync 方法源碼如下:

/// 下載圖片並解碼圖片
Future<ui.Codec> _loadAsync(
    NetworkImage key,
    StreamController<ImageChunkEvent> chunkEvents,
    ) async {
  try {
    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 image_provider.NetworkImageLoadException(statusCode: response.statusCode, uri: resolved);

    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');
    // 將圖片解碼成二進制Codec對象
    return PaintingBinding.instance.instantiateImageCodec(bytes);
  } finally {
    chunkEvents.close();
  }
}

下載完圖片會將圖片解碼成二進制對應的 Codec 對象,該 Codec 對象具體由 Flutter-engine 中原生方法進行解碼,如下:

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

上面這個過程我們知道了圖片是由 Flutter-engine 中原生方法進行解碼最終返回一個 ImageStreamCompleter,這個 ImageStreamCompleterresolve 方法中被設置給 ImageStream,resolve 方法返回這個 ImageStream,我們可以通過這個 ImageStream 進行圖片加載進度的監聽,ImageStream 源碼如下:

/// ImageStream用來處理圖片資源,表示該圖片資源還未加載完成,圖片資源一旦加載完成,
/// ImageStream的真正數據對象就是由dart:ui.Image和scale構成的ImageInfo,
class ImageStream extends Diagnosticable {
  ImageStream();

  /// 管理正在加載中的圖片資源,監聽圖片資源加載,如加載成功、加載中、加載失敗
  ImageStreamCompleter get completer => _completer;
  ImageStreamCompleter _completer;

  List<ImageStreamListener> _listeners;

  /// 設置一個圖片加載監聽器,通常由創建ImageStream的ImageProvider自動設置,且每個ImageStream中只能設置一次
  void setCompleter(ImageStreamCompleter value) {
    assert(_completer == null);
    _completer = value;
    if (_listeners != null) {
      final List<ImageStreamListener> initialListeners = _listeners;
      _listeners = null;
      initialListeners.forEach(_completer.addListener);
    }
  }

  /// 添加圖片加載監聽器
  void addListener(ImageStreamListener listener) {
    if (_completer != null) return _completer.addListener(listener);
    _listeners ??= <ImageStreamListener>[];
    _listeners.add(listener);
  }

  /// 移除圖片加載監聽器
  void removeListener(ImageStreamListener listener) {
    if (_completer != null) return _completer.removeListener(listener);
    assert(_listeners != null);
    for (int i = 0; i < _listeners.length; i += 1) {
      if (_listeners[i] == listener) {
        _listeners.removeAt(i);
        break;
      }
    }
  }

  Object get key => _completer ?? this;

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    // ...
  }
}

至此,我們知道了圖片資源最終會被轉換成 ImageStreamresolve 方法會在 Image 組件的對應生命週期方法中被調用,如 didChangeDependenciesdidUpdateWidget等方法,在組件構建的時候會創建 RawImage,繼續跟蹤源碼則是 RenderImage,在其 paint 方法中調用了 paintImage 方法,裡面通過 canvas 完成圖片配置信息的繪製。

圖片預加載#

在 Flutter 中可以通過 precacheImage 方法來對圖片進行預加載,也就是將圖片提前添加到緩存中,當需要加載圖片的時候就直接從緩存中獲取,precacheImage 方法還是通過 ImageProviderresolve 方法解析圖片資源並將其添加到圖片緩存中,其方法源碼如下:

/// precacheImage
Future<void> precacheImage(
  ImageProvider provider,
  BuildContext context, {
  Size size,
  ImageErrorListener onError,
}) {
  final ImageConfiguration config = createLocalImageConfiguration(context, size: size);
  final Completer<void> completer = Completer<void>();
  // 解析圖片資源將其添加到緩存中
  final ImageStream stream = provider.resolve(config);
  ImageStreamListener listener;
  listener = ImageStreamListener(
    // 省略...
    },
  );
  stream.addListener(listener);
  return completer.future;
}

使用時根據圖片來源選擇不同的 ImageProvider 來緩存對應的圖片,使用方式如下:

// 預緩存圖片
precacheImage(new AssetImage("images/cat.jpg"), context);

圖片緩存#

ImageCache 是 flutter 提供的基於 LRU 算法的緩存實現,默認可以緩存 1000 張圖片,緩存大小最大 100 M,當緩存超過任意一個緩存限制都會從緩存中移除最近最少使用的緩存項,當然,也可以根據項目需要設置最大緩存項 _maximumSize 的值和最大緩存大小 _maximumSizeBytes 的值,具體查看 ImageCache 源碼相關註釋,如下:

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

/// 使用LRU實現的圖片緩存。最多100張圖片,緩存大小最大100M,緩存由ImageProvider及其子類實現
/// 其緩存實例由PaintingBinding單例持有
class ImageCache {
  // 正在加載的圖片隊列
  final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};

  // 緩存隊列
  final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};

  /// 緩存項的最大數量,
  int get maximumSize => _maximumSize;
  int _maximumSize = _kDefaultSize;

  /// 設置緩存項最大數量
  set maximumSize(int value) {
    assert(value != null);
    assert(value >= 0);
    if (value == maximumSize) return;
    _maximumSize = value;
    if (maximumSize == 0) {
      clear();
    } else {
      _checkCacheSize();
    }
  }

  /// 當前緩存項的數量
  int get currentSize => _cache.length;

  /// 最大緩存大小(字節)
  int get maximumSizeBytes => _maximumSizeBytes;
  int _maximumSizeBytes = _kDefaultSizeBytes;

  /// 設置緩存大小
  set maximumSizeBytes(int value) {
    assert(value != null);
    assert(value >= 0);
    if (value == _maximumSizeBytes) return;
    _maximumSizeBytes = value;
    if (_maximumSizeBytes == 0) {
      clear();
    } else {
      _checkCacheSize();
    }
  }

  /// 緩存當前大小(字節)
  int get currentSizeBytes => _currentSizeBytes;
  int _currentSizeBytes = 0;

  /// 清除緩存
  void clear() {
    _cache.clear();
    _pendingImages.clear();
    _currentSizeBytes = 0;
  }

  /// 根據對應key移除緩存,移除成功返回true,反之,還在加載完成的圖片也會被移出同時移除
  /// 對應圖片加載監聽器,避免將其添加到緩存中
  bool evict(Object key) {
    final _PendingImage pendingImage = _pendingImages.remove(key);
    if (pendingImage != null) {
      pendingImage.removeListener();
      return true;
    }
    final _CachedImage image = _cache.remove(key);
    if (image != null) {
      _currentSizeBytes -= image.sizeBytes;
      return true;
    }
    return false;
  }

  /// 緩存API的入口
  ///
  /// 如果緩存可用,從給定key從緩存中獲取ImageStreamCompleter返回,反之則使用
  /// 提供的回調loader()獲取ImageStreamCompleter並返回,都会將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;
    // 如果有對應緩存,先從緩存中移除,然後再添加最近使用的位置
    final _CachedImage image = _cache.remove(key);
    if (image != null) {
      _cache[key] = image;
      return image.completer;
    }
    // 如果獲取不到對應緩存,直接使用對應ImageProvider中的load方法加載圖片
    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);
      // 如果圖片的大小大於緩存大小,且緩存大小不為0,則增加緩存到小到圖片緩存大小
      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();
    }

    // 添加正在加載的圖片到_pendingImages中,並設置加載圖片的監聽
    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);
    }
    assert(_currentSizeBytes >= 0);
    assert(_cache.length <= maximumSize);
    assert(_currentSizeBytes <= maximumSizeBytes);
  }
}
// 緩存圖片類
class _CachedImage {
  _CachedImage(this.completer, this.sizeBytes);

  final ImageStreamCompleter completer;
  final int sizeBytes;
}

// 正在加載的圖片類
class _PendingImage {
  _PendingImage(this.completer, this.listener);

  final ImageStreamCompleter completer;
  final ImageStreamListener listener;

  void removeListener() {
    completer.removeListener(listener);
  }
}

上述代碼是整個緩存邏輯,當 resolve 方法調用的時候會調用 putIfAbsent 方法,該方法是緩存的入口,如果已經有緩存則從緩存中獲取,否則調用對應 ImageProviderload 方法去加載圖片,並將其添加到緩存中。

清除圖片緩存#

清除圖片緩存直接通過 PaintingBinding 的單例獲取到 ImageCache 調用其 clear 方法,如下:

/// 清理緩存
_clearCache(BuildContext context) {
  PaintingBinding.instance.imageCache.clear();
  Toast.show("緩存已清除", context);
}

圖片加載進度監聽#

從前面我們知道 resolve 方法返回對應圖片的 ImageStream,我們可以通過這個 ImageStream 設置圖片加載監聽,實際上添加的就是 ImageStreamListener,如下:

/// Image
Image image = Image.network(
  "https://cdn.nlark.com/yuque/0/2019/jpeg/644330/1576812507787-bdaeaf42-8317-4e06-a489-251686bf7b91.jpeg",
  width: 100,
  height: 100,
  alignment: Alignment.topLeft,
);

// 圖片加載監聽
image.image.resolve(ImageConfiguration()).addListener(
        ImageStreamListener((ImageInfo imageInfo, bool synchronousCall) {
      completer.complete(imageInfo.image);
    }, onChunk: (event) {
      int currentLength = event.cumulativeBytesLoaded;
      int totalLength = event.expectedTotalBytes;
      print("$currentLength/$totalLength from network");
    }, onError: (e, trace) {
      print(e.toString());
    }));

開發中最常用的是下面這種方式,通過 loadingBuilder 屬性添加對圖片加載進度的監聽,實際上最終設置的也是 ImageStreamListener,如下:

/// 圖片進度監聽
class ImageLoadListenerSamplePage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _ImageState();
  }
}
/// _ImageState
class _ImageState extends State<ImageLoadListenerSamplePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Image Load Listener"),
        centerTitle: true,
      ),
      body: Image.network(
        "https://cdn.nlark.com/yuque/0/2019/jpeg/644330/1576812507787-bdaeaf42-8317-4e06-a489-251686bf7b91.jpeg",
        width: 100,
        height: 100,
        loadingBuilder: (BuildContext context, Widget child,
            ImageChunkEvent loadingProgress) {
          if (loadingProgress == null) return child;
          int currentLength = loadingProgress.cumulativeBytesLoaded;
          int totalLength = loadingProgress.expectedTotalBytes;
          print("$currentLength/$totalLength from network");
          return CircularProgressIndicator(
            value: loadingProgress.expectedTotalBytes != null
                ? loadingProgress.cumulativeBytesLoaded /
                    loadingProgress.expectedTotalBytes
                : null,
          );
        },
      ),
    );
  }
}

加載圖片案例#

前面提到,Flutter 默認實現了加載網絡、SD 卡、Asset、內存裡面的圖片,其中 SD 卡、內存獲取圖片使用 FutureBuilder 來處理異步任務返回 Image,不多說直接看代碼:

/// 加載圖片
class ImageLoadSamplePage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _ImageSampleSate();
  }
}
/// _ImageSampleSate
class _ImageSampleSate extends State<ImageLoadSamplePage> {

  Future<Directory> _externalStorageDirectory;
  Future<Uint8List> _imageUint8List;

  /// 獲取文件目錄
  void _requestExternalStorageDirectory() {
    setState(() {
      _externalStorageDirectory = getExternalStorageDirectory();
    });
  }

  /// 把文件轉換成字節
  void _requestBytes() {
    setState(() {
      File file = new File("/storage/emulated/0/owl.jpg");
      _imageUint8List = file.readAsBytes();
    });
  }

  @override
  Widget build(BuildContext context) {
  
    _requestExternalStorageDirectory();
    _requestBytes();

    return Scaffold(
      appBar: AppBar(
        title: Text("Image Sample"),
        centerTitle: true,
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _clearCache(context);
        },
        child: Icon(Icons.clear),
      ),
      body: ListView(
        scrollDirection: Axis.vertical,
        children: <Widget>[
          Text(
            "from network...",
            style: TextStyle(fontSize: 16),
          ),
          Image.network(
            "https://cdn.nlark.com/yuque/0/2019/jpeg/644330/1576812507787-bdaeaf42-8317-4e06-a489-251686bf7b91.jpeg",
            width: 100,
            height: 100,
            alignment: Alignment.topLeft,
          ),
          Text(
            "from file...",
            style: TextStyle(fontSize: 16),
          ),
          FutureBuilder<Directory>(
            future: _externalStorageDirectory,
            builder: _buildFileDirectory,
          ),
          Text(
            "from asset...",
            style: TextStyle(fontSize: 16),
          ),
          Image.asset(
            'images/cat.jpg',
            width: 100,
            height: 100,
            alignment: Alignment.topLeft,
          ),
          Text(
            "from memory...",
            style: TextStyle(fontSize: 16),
          ),
          FutureBuilder<Uint8List>(
            future: _imageUint8List,
            builder: _buildMemoryDirectory,
          ),
        ],
      ),
    );
  }

  /// 異步獲取SD卡圖片
  Widget _buildFileDirectory(
      BuildContext context, AsyncSnapshot<Directory> snapshot) {
    Text text = new Text("default");
    if (snapshot.connectionState == ConnectionState.done) {
      if (snapshot.hasData) {
        File file = new File("${snapshot.data.path}/owl.jpg");
        return Image.file(
          file,
          width: 100,
          height: 100,
          alignment: Alignment.topLeft,
        );
      } else if (snapshot.hasError) {
        text = new Text(snapshot.error);
      } else {
        text = const Text("unknow");
      }
    }
    print(text.data);
    return text;
  }


  /// 異步獲取內存中圖片
  Widget _buildMemoryDirectory(
      BuildContext context, AsyncSnapshot<Uint8List> snapshot) {
    Text text = new Text("default");
    if (snapshot.connectionState == ConnectionState.done) {
      if (snapshot.hasData) {
        return Image.memory(
          snapshot.data,
          width: 100,
          height: 100,
          alignment: Alignment.topLeft,
        );
      } else if (snapshot.hasError) {
        text = new Text(snapshot.error);
      } else {
        text = const Text("unknow");
      }
    }
    return text;
  }

  /// 清理緩存(為了測試緩存)
  _clearCache(BuildContext context) {
    PaintingBinding.instance.imageCache.clear();
    print("---_clearCache-->");
    Toast.show("緩存已清除", context);
  }
}

上述代碼執行效果如下:

image

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。