1. 程式人生 > >Flutter入門系列(四)---Flutter圖片快取

Flutter入門系列(四)---Flutter圖片快取

Flutter圖片快取 | Image.network原始碼分析

原創: 郭海生 京東技術  

原文地址:https://mp.weixin.qq.com/s/W5iu3VsNuvIygFbKM-giqA

隨著手機裝置硬體水平的飛速發展,使用者對於圖片的顯示要求也越來越高,稍微處理不好就會容易造成記憶體溢位等問題。所以我們在使用Image的時候,建立一個圖片快取機制已經是一個常態。Android目前提供了很豐富的圖片框架,像ImageLoader、Glide、Fresco等。對於Flutter而言,為了探其快取機制或者定製自己的快取框架,特從其Image入手進行突破。

>>>>

Image 的用法

Image是Flutter裡提供的顯示圖片的控制元件,類似Android裡ImageView,不過其用法有點類似Glide等圖片框架。

我們先看Image的用法。Flutter對Image控制元件提供了多種建構函式:

 

new Image         用於從ImageProvider獲取影象

new Image.asset  用於使用key從AssetBundle獲取影象

new Image.network 用於從URL地址獲取影象

new Image.file    用於從File獲取影象

 

我們只分析Image.network原始碼,分析理解完這個之後,其他的也是一樣的思路。

我們先從Image.network的用法入手:顯示一個網路圖片很簡單,直接通過Image.network攜帶一個url引數即可。

範例:

 

return new Scaffold(

appBar: new AppBar(

 title: new Text("Image from Network"),

),

body: new Container(

  child: new Column(

  children: <Widget>[

  // Load image from network

  new Image.network(

        'https://flutter.io/images/flutter-mark-square-100.png'),

       ],

    )),

);

 

>>>>

Image結構UML類圖

 

我們首先看一下Image的UML類圖:

 

可以看到Image的框架結構還是有點兒複雜的,在你只調用一行程式碼的情況下,其實Flutter為你做了很多工作。

 

初步梳理下每個類概念

 

  1. StatefulWidget就是有狀態的Widget,是展示在頁面上的元素。

  2. Image繼承於StatefulWidget,是來顯示和載入圖片。

  3. State控制著StatefulWidget狀態改變的生命週期,當Widget被建立、Widget配置資訊改變或者Widget被銷燬等等,State的一系列方法會被呼叫。

  4. _ImageState繼承於State,處理State生命週期變化以及生成Widget。

  5. ImageProvider提供載入圖片的入口,不同的圖片資源載入方式不一樣,只要重寫其load方法即可。同樣,快取圖片的key值也有其生成。

  6. NetWorkImage負責下載網路圖片的,將下載完成的圖片轉化成ui.Codec物件交給ImageStreamCompleter去處理解析。

  7. ImageStreamCompleter就是逐幀解析圖片的。

  8. ImageStream是處理Image Resource的,ImageState通過ImageStream與ImageStreamCompleter建立聯絡。ImageStream裡也儲存著圖片載入完畢的監聽回撥。

  9. MultiFrameImageStreamCompleter就是多幀圖片解析器。

 

先把Image的框架結構瞭解一下,有助於下面我們更加清晰地分析程式碼。

 

>>>>

原始碼分析

 

我們看下Image.network都做了什麼:

 
  1. class Image extends StatefulWidget {

  2.    Image.network(String src, {

  3.    Key key,

  4.    double scale = 1.0,

  5.    this.width,

  6.    this.height,

  7.    this.color,

  8.    this.colorBlendMode,

  9.    this.fit,

  10.    this.alignment = Alignment.center,

  11.    this.repeat = ImageRepeat.noRepeat,

  12.    this.centerSlice,

  13.    this.matchTextDirection = false,

  14.    this.gaplessPlayback = false,

  15.    Map<String, String> headers,

  16.      }) : image = new NetworkImage(src, scale: scale, headers: headers),

  17.       assert(alignment != null),

  18.       assert(repeat != null),

  19.       assert(matchTextDirection != null),

  20.       super(key: key);

  21.   ......

 

我們看到Image是一個StatefulWidget物件,可以直接放到Container或者Column等容器裡,其屬性解釋如下:

 

  • width:widget的寬度

  • height:widget的高度

  • color:與colorBlendMode配合使用,將此顏色用BlendMode方式混合圖片

  • colorBlendMode:混合模式演算法

  • fit:與android:scaletype一樣,控制圖片如何resized/moved來匹對Widget的size

  • alignment:widget對齊方式

  • repeat:如何繪製未被影象覆蓋的部分

  • centerSlice:支援9patch,拉伸的中間的區域

  • matchTextDirection:繪製圖片的方向:是否從左到右

  • gaplessPlayback:圖片變化的時候是否展示老圖片或者什麼都不展示

  • headers:http請求頭

  • image:一個ImageProvide物件,在呼叫的時候已經例項化,這個類主要承擔了從網路載入圖片的功能。它是載入圖片的最重要的方法,不同的圖片載入方式(assert檔案載入、網路載入等等)也就是重寫ImageProvider載入圖片的方法(load())。

 

Image是一個StatefulWidget物件,所以我們看它的State物件:

 
  1. class _ImageState extends State<Image> {

  2.  ImageStream _imageStream;

  3.  ImageInfo _imageInfo;

  4.  bool _isListeningToStream = false;

  5. }

  6.  

  7. class ImageStream extends Diagnosticable {

  8.  ImageStreamCompleter get completer => _completer;

  9.  ImageStreamCompleter _completer;

  10.  

  11.  List<ImageListener> _listeners;

  12.  

  13.  /// Assigns a particular [ImageStreamCompleter] to this [ImageStream].

  14.   void setCompleter(ImageStreamCompleter value) {

  15.    assert(_completer == null);

  16.    _completer = value;

  17.    print("setCompleter:::"+(_listeners==null).toString());

  18.    if (_listeners != null) {

  19.      final List<ImageListener> initialListeners = _listeners;

  20.      _listeners = null;

  21.      initialListeners.forEach(_completer.addListener);

  22.    }

  23.  }

  24.  

  25.  /// Adds a listener callback that is called whenever a new concrete [ImageInfo]

  26.  void addListener(ImageListener listener) {

  27.    if (_completer != null)

  28.      return _completer.addListener(listener);

  29.    _listeners ??= <ImageListener>[];

  30.    _listeners.add(listener);

  31.  }

  32.  

  33.  /// Stop listening for new concrete [ImageInfo] objects.

  34.  void removeListener(ImageListener listener) {

  35.    if (_completer != null)

  36.      return _completer.removeListener(listener);

  37.    assert(_listeners != null);

  38.    _listeners.remove(listener);

  39.  }

  40. }

 

我們對_ImageState的兩個屬性物件解釋一下:

 

  • ImageStream是處理Image Resource的,ImageStream裡儲存著圖片載入完畢的監聽回撥,ImageStreamCompleter也是其成員,這樣ImageStream將圖片的解析流程交給了ImageStreamCompleter去處理。

  • ImageInfo包含了Image的資料來源資訊:width和height以及ui.Image。 將ImageInfo裡的ui.Image設定給RawImage就可以展示了。RawImage就是我們真正渲染的物件,是顯示ui.Image的一個控制元件,接下來我們會看到。

 

我們知道State的生命週期,首先State的initState執行,然後didChangeDependencies會執行,我們看到ImageState裡沒有重寫父類的initState,那我們看其didChangeDependencies():

 
  1. @override

  2. void didChangeDependencies() {

  3.    _resolveImage();

  4.  

  5.    if (TickerMode.of(context))

  6.      _listenToStream();

  7.    else

  8.      _stopListeningToStream();

  9.  

  10.    super.didChangeDependencies();

  11. }

 

>>>>

_resolveImage方法解析

 

我們看到首先呼叫了resolveImage(),我們看下resolveImage方法:

 
  1. void _resolveImage() {

  2.    final ImageStream newStream =

  3.      widget.image.resolve(createLocalImageConfiguration(

  4.          context,

  5.          size: widget.width != null && widget.height != null ? new Size(widget.width, widget.height) : null

  6.      ));

  7.    assert(newStream != null);

  8.    _updateSourceStream(newStream);

  9.  }

 

這個方法是處理圖片的入口。widget.image這個就是上面的建立的NetworkImage物件,是個ImageProvider物件,呼叫它的resolve並且傳進去預設的ImageConfiguration。 我們看下resolve方法,發現NetworkImage沒有,果不其然,我們在其父類ImageProvider找到了:

 
  1. ImageStream resolve(ImageConfiguration configuration) {

  2.    assert(configuration != null);

  3.    final ImageStream stream = new ImageStream();

  4.    T obtainedKey;

  5.    obtainKey(configuration).then<void>((T key) {

  6.      obtainedKey = key;

  7.      stream.setCompleter(PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key)));

  8.    }).catchError(

  9.      (dynamic exception, StackTrace stack) async {

  10.        FlutterError.reportError(new FlutterErrorDetails(

  11.          exception: exception,

  12.          stack: stack,

  13.          library: 'services library',

  14.          context: 'while resolving an image',

  15.          silent: true, // could be a network error or whatnot

  16.          informationCollector: (StringBuffer information) {

  17.            information.writeln('Image provider: $this');

  18.            information.writeln('Image configuration: $configuration');

  19.            if (obtainedKey != null)

  20.              information.writeln('Image key: $obtainedKey');

  21.          }

  22.        ));

  23.        return null;

  24.      }

  25.    );

  26.    return stream;

  27.  }    

 

我們看到這個方法建立了ImageStream並返回,呼叫obtainKey返回一個攜帶NetworkImage的future,以後會作為快取的key使用,並且呼叫ImageStream的setCompleter的方法:

 
  1. void setCompleter(ImageStreamCompleter value) {

  2.    assert(_completer == null);

  3.    _completer = value;

  4.    if (_listeners != null) {

  5.      final List<ImageListener> initialListeners = _listeners;

  6.      _listeners = null;

  7.      initialListeners.forEach(_completer.addListener);

  8.    }

  9.  }

 

這個方法就是給ImageStream設定一個ImageStreamCompleter物件,每一個ImageStream物件只能設定一次,ImageStreamCompleter是為了輔助ImageStream解析和管理Image圖片幀的,並且判斷是否有初始化監聽器,可以做一些初始化回撥工作。 我們繼續看下PaintingBinding.instance.imageCache.putIfAbsent方法:

 
  1. ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader()) {

  2.    assert(key != null);

  3.    assert(loader != null);

  4.    ImageStreamCompleter result = _pendingImages[key];

  5.    // Nothing needs to be done because the image hasn't loaded yet.

  6.    if (result != null)

  7.      return result;

  8.    // Remove the provider from the list so that we can move it to the

  9.    // recently used position below.

  10.    final _CachedImage image = _cache.remove(key);

  11.    if (image != null) {

  12.      _cache[key] = image;

  13.      return image.completer;

  14.    }

  15.    result = loader();

  16.    void listener(ImageInfo info, bool syncCall) {

  17.      // Images that fail to load don't contribute to cache size.

  18.      final int imageSize = info.image == null ? 0 : info.image.height * info.image.width * 4;

  19.      final _CachedImage image = new _CachedImage(result, imageSize);

  20.      _currentSizeBytes += imageSize;

  21.      _pendingImages.remove(key);

  22.      _cache[key] = image;

  23.      result.removeListener(listener);

  24.      _checkCacheSize();

  25.    }

  26.    if (maximumSize > 0 && maximumSizeBytes > 0) {

  27.      _pendingImages[key] = result;

  28.      result.addListener(listener);

  29.    }

  30.    return result;

  31.  }

 

這個是Flutter預設提供的記憶體快取api的入口方法,這個方法會先通過key獲取之前的ImageStreamCompleter物件,這個key就是NetworkImage物件,當然我們也可以重寫obtainKey方法自定義key,如果存在則直接返回,如果不存在則執行load方法載入ImageStreamCompleter物件,並將其放到首位(最少最近使用演算法)。

 

也就是說ImageProvider已經實現了記憶體快取:預設快取圖片的最大個數是1000,預設快取圖片的最大空間是10MiB。 第一次載入圖片肯定是沒有快取的,所以我們看下loader方法,我們看到ImageProvider是空方法,我們去看NetWorkImage,按照我們的預期確實在這裡:

 
  1. @override

  2.  ImageStreamCompleter load(NetworkImage key) {

  3.    return new MultiFrameImageStreamCompleter(

  4.      codec: _loadAsync(key),

  5.      scale: key.scale,

  6.      informationCollector: (StringBuffer information) {

  7.        information.writeln('Image provider: $this');

  8.        information.write('Image key: $key');

  9.      }

  10.    );

  11.  }

  12.  //網路請求載入圖片的方法

  13.  Future<ui.Codec> _loadAsync(NetworkImage key) async {

  14.    assert(key == this);

  15.  

  16.    final Uri resolved = Uri.base.resolve(key.url);

  17.    final HttpClientRequest request = await _httpClient.getUrl(resolved);

  18.    headers?.forEach((String name, String value) {

  19.      request.headers.add(name, value);

  20.    });

  21.    final HttpClientResponse response = await request.close();

  22.    if (response.statusCode != HttpStatus.ok)

  23.      throw new Exception('HTTP request failed, statusCode: ${response?.statusCode}, $resolved');

  24.  

  25.    final Uint8List bytes = await consolidateHttpClientResponseBytes(response);

  26.    if (bytes.lengthInBytes == 0)

  27.      throw new Exception('NetworkImage is an empty file: $resolved');

  28.  

  29.    return await ui.instantiateImageCodec(bytes);

  30.  }

 

這個方法為我們建立了一個MultiFrameImageStreamCompleter物件,根據名字我們也能知道它繼承於ImageStreamCompleter。還記得ImageStreamCompleter是做什麼的嗎,就是輔助ImageStream管理解析Image的。

 

引數解析

 

  • _loadAsync()是請求網路載入圖片的方法

  • scale是縮放係數

  • informationCollector是資訊收集物件的,提供錯誤或者其他日誌用

 

MultiFrameImageStreamCompleter是多幀的圖片處理載入器,我們知道Flutter的Image支援載入gif,通過MultiFrameImageStreamCompleter可以對gif檔案進行解析:

 
  1. MultiFrameImageStreamCompleter({

  2.    @required Future<ui.Codec> codec,

  3.    @required double scale,

  4.    InformationCollector informationCollector

  5.  }) : assert(codec != null),

  6.       _informationCollector = informationCollector,

  7.       _scale = scale,

  8.       _framesEmitted = 0,

  9.       _timer = null {

  10.    codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) {

  11.      FlutterError.reportError(new FlutterErrorDetails(

  12.        exception: error,

  13.        stack: stack,

  14.        library: 'services',

  15.        context: 'resolving an image codec',

  16.        informationCollector: informationCollector,

  17.        silent: true,

  18.      ));

  19.    });

  20.  }

  21.  

  22.  ui.Codec _codec;

  23.  final double _scale;

  24.  final InformationCollector _informationCollector;

  25.  ui.FrameInfo _nextFrame;

 

我們看到MultiFrameImageStreamCompleter拿到loadAsync返回的codec資料物件,通過handleCodecReady來處理資料,然後會呼叫_decodeNextFrameAndSchedule方法:

 
  1. Future<Null> _decodeNextFrameAndSchedule() async {

  2.    try {

  3.      _nextFrame = await _codec.getNextFrame();

  4.    } catch (exception, stack) {

  5.      FlutterError.reportError(new FlutterErrorDetails(

  6.          exception: exception,

  7.          stack: stack,

  8.          library: 'services',

  9.          context: 'resolving an image frame',

  10.          informationCollector: _informationCollector,

  11.          silent: true,

  12.      ));

  13.      return;

  14.    }

  15.    if (_codec.frameCount == 1) {

  16.      // This is not an animated image, just return it and don't schedule more

  17.      // frames.

  18.      _emitFrame(new ImageInfo(image: _nextFrame.image, scale: _scale));

  19.      return;

  20.    }

  21.    SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame);

  22.  }

 

通過codec.getNextFrame()去拿下一幀,對於靜態的圖片frameCount是1,直接用ImageInfo組裝image,交給emitFrame方法,這個方法裡會呼叫setImage,如下:

 
  1. @protected

  2.  void setImage(ImageInfo image) {

  3.    _current = image;

  4.    if (_listeners.isEmpty)

  5.      return;

  6.    final List<ImageListener> localListeners = new List<ImageListener>.from(_listeners);

  7.    for (ImageListener listener in localListeners) {

  8.      try {

  9.        listener(image, false);

  10.      } catch (exception, stack) {

  11.        _handleImageError('by an image listener', exception, stack);

  12.      }

  13.    }

  14.  }

 

setImage方法就是設定當前的ImageInfo並檢查監聽器列表,通知監聽器圖片已經載入完畢可以重新整理UI了。

 

對於動圖來說就是就是交給SchedulerBinding逐幀的去呼叫setImage,通知UI重新整理,程式碼就不貼了,有興趣的可以自行檢視下。 至此resolveImage呼叫流程我們算是講完了,接下來我們看listenToStream。

>>>>

_listenToStream方法解析

 

我們繼續分析didChangeDependencies方法,這個方法裡會判斷TickerMode.of(context)的值,這個值預設是true,和AnimationConrol有關,後續可以深入研究。然後呼叫_listenToStream()。 我們看下這個方法:

 
  1. void _listenToStream() {

  2.    if (_isListeningToStream)

  3.      return;

  4.    _imageStream.addListener(_handleImageChanged);

  5.    _isListeningToStream = true;

  6.  }

 

這個就是新增圖片載入完畢的回撥器。還記得嗎,當圖片載入並解析完畢的時候,MultiFrameImageStreamCompleter的setImage方法會呼叫這裡傳過去的回撥方法。我們看下這裡回撥方法裡做了什麼:

 
  1. void _handleImageChanged(ImageInfo imageInfo, bool synchronousCall) {

  2.    setState(() {

  3.      _imageInfo = imageInfo;

  4.    });

  5.  }

 

很顯然就是拿到上層傳過來ImageInfo,呼叫setState更新UI 我們看下build方法:

 
  1. Widget build(BuildContext context) {

  2.    return new RawImage(

  3.      image: _imageInfo?.image,

  4.      width: widget.width,

  5.      height: widget.height,

  6.      scale: _imageInfo?.scale ?? 1.0,

  7.      color: widget.color,

  8.      colorBlendMode: widget.colorBlendMode,

  9.      fit: widget.fit,

  10.      alignment: widget.alignment,

  11.      repeat: widget.repeat,

  12.      centerSlice: widget.centerSlice,

  13.      matchTextDirection: widget.matchTextDirection,

  14.    );

  15.  }

 

就是用imageInfo和widget的資訊來封裝RawImage,RawImage是RenderObjectWidget物件,是應用程式真正渲染的物件,將咱們的圖片顯示到介面上。

 

>>>>

總結

 

梳理下流程:

 

  1. 從入口開始,Image是繼承於StatefulWidget,它為咱們實現好了State:_ImageState,並且提供了一個已經例項化的NetWorkImage物件,它是繼承於ImageProvider物件的。

  2. ImageState建立完之後,ImageState通過呼叫resolveImage(),resolveImage()又會呼叫ImageProvider的resolve()方法返回一個ImageStream物件。_ImageState也註冊了監聽器給ImageStream,當圖片下載完畢後會執行回撥方法。

  3. 然後在ImageProvider的resolve()方法裡不僅建立了ImageStream還設定了ImageStream的setComplete方法去設定ImageStreamCompleter,在這裡去判斷是否有快取,沒有快取就呼叫load方法去建立ImageStreamCompleter並且新增監聽器為了執行載入完圖片之後的快取工作。ImageStreamCompleter是為了解析已經載入完成的Image的。

  4. NetWorkImage實現了ImageProvider的load方法,是真正下載圖片的地方,建立了MultiFrameImageStreamCompleter物件,並且呼叫_loadAsync去下載圖片。當圖片下載完成後就呼叫UI的回撥方法,通知UI重新整理。

 

>>>>

最後

 

至此,對Image.network的原始碼分析到這裡也結束了,你也可以返回去看下Image的結構圖了。怎麼樣,分析完之後是不是對Flutter載入網路圖片的流程已經很瞭解了,也找到了Flutter快取的突破口,Flutter自身已經提供了記憶體快取(雖然不太完美),接下來你就可以新增你的硬碟快取或者定製你的圖片框架了。