主頁 > 移動端開發 > Flutter圖片加載和快取機制探究

Flutter圖片加載和快取機制探究

2021-11-09 08:26:29 移動端開發

今天來學習一下 Flutter 自身是如何加載圖片和管理圖片的, Flutter 提供了一個圖片控制元件 ImageImage 定義了若干中加載圖片的方式,包括 Image.asset、Image.file、Image.network、Image.memory, Image內部維護了一個 ImageProvider物件,ImageProvider則真正維護整個圖片加載的作業,Widget 本身內部是體現在 RawImage中: ?

圖片控制元件

?

// Image
Widget result = RawImage(
      image: _imageInfo?.image,
      debugImageLabel: _imageInfo?.debugLabel,
      width: widget.width,
      height: widget.height,
      scale: _imageInfo?.scale ?? 1.0,
      color: widget.color,
      colorBlendMode: widget.colorBlendMode,
      fit: widget.fit,
      alignment: widget.alignment,
      repeat: widget.repeat,
      centerSlice: widget.centerSlice,
      matchTextDirection: widget.matchTextDirection,
      invertColors: _invertColors,
      isAntiAlias: widget.isAntiAlias,
      filterQuality: widget.filterQuality,
    );
return result;

這里可以看到 _imageInfo 決定 RawImage如何展示圖片, _imageInfo 則會在圖片的每一幀進行重新賦值: ?

// image.dart
void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {
 	setState(() {
    _imageInfo = imageInfo;
  }
}

那么圖片資訊是從哪里來的呢,它是由 _resolveImage 這個方法發起的,這個方法會在 _ImageState 的 didChangeDependenciesdidUpdateWidgetreassemble方法進行呼叫, 也就是控制元件發生變化重繪狀態的時候,就會重新去決議圖片, ?

圖片決議

_resolveImage 邏輯如下:

void _resolveImage() {
	final ScrollAwareImageProvider provider = ScrollAwareImageProvider<dynamic>(
      context: _scrollAwareContext,
      imageProvider: widget.image,
    );
	final ImageStream newStream =
      provider.resolve(createLocalImageConfiguration(
        context,
        size: widget.width != null && widget.height != null ? Size(widget.width, widget.height) : null,
      ));
	_updateSourceStream(newStream);
}

這里會用 ScrollAwareImageProvider 包裝一下,ScrollAwareImageProvider 的功能我們后面會介紹,這里先跳過, ?

//ImageProvider# resolve
ImageStream resolve(ImageConfiguration configuration) {
	_createErrorHandlerAndKey(configuration,(T key, ImageErrorListener errorHandler) {
        resolveStreamForKey(configuration, stream, key, errorHandler);
      },
		(T? key, dynamic exception, StackTrace? stack) async {
        await null; // wait an event turn in case a listener has been added to the image stream.
        final _ErrorImageCompleter imageCompleter = _ErrorImageCompleter();
        stream.setCompleter(imageCompleter);
        InformationCollector? collector;
        assert(() {
          collector = () sync* {
            yield DiagnosticsProperty<ImageProvider>('Image provider', this);
            yield DiagnosticsProperty<ImageConfiguration>('Image configuration', configuration);
            yield DiagnosticsProperty<T>('Image key', key, defaultValue: null);
          };
          return true;
        }());
        imageCompleter.setError(
          exception: exception,
          stack: stack,
          context: ErrorDescription('while resolving an image'),
          silent: true, // could be a network error or whatnot
          informationCollector: collector,
        );
      }
	);
}

resolve 方法呼叫 _createErrorHandlerAndKey 來處理圖片加載的例外情況,當圖片正常加載的時候,會執行 resolveStreamForKey, ?

//resolveStreamForKey
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) {
	if (stream.completer != null) {
		final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent(
        key,
        () => stream.completer!,
        one rror: handleError,
      );
		return;
	}

	final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent(
		key,
      () => load(key, PaintingBinding.instance!.instantiateImageCodec),
      one rror: handleError,
		);

	if (completer != null) {
      stream.setCompleter(completer);
    }
}

Flutter 會把圖片快取相關的邏輯維護在 ImageCache這個物件,

快取管理

ImageCache里面有 3 個 map:

image.png

分別表示

  • 正在加載的圖片
  • 快取在記憶體的圖片
  • 表示正活躍的圖片,Widget 狀態變化后可能會清空

?

新增快取

新增快取的時候會設定 map 的 key, key 由 ImageProvider 物件提供,例如:

  • AssetImage 當包名和bundle一樣的時候,key可以認為是一樣的,
  • NetworkImage 當圖片 url 和比例一樣的時候,key可以認為是一樣的,

ImageCache 實際上是一個單例物件,也就是 Flutter 的圖片快取管理是全域的,ImageCache 最重要的方法就是 putIfAbsent:

// 整理過核心邏輯的代碼
ImageStreamCompleter? putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener? one rror }) {
  // 根據key從正在加載的map里獲取快取,如果有直接回傳
  	ImageStreamCompleter? result = _pendingImages[key]?.completer;
  	if (result != null) {
      return result;
    }
  
  // 檢查記憶體快取,存在的話更新存活map
  final _CachedImage? image = _cache.remove(key);
  if (image != null) {
    _trackLiveImage(key, _LiveImage(image.completer, image.sizeBytes, () => _liveImages.remove(key)));
    _cache[key] = image;
    return image.completer;
  }
  
  // 沒有快取,從 _live 里面取
  final _CachedImage? liveImage = _liveImages[key];
  if (liveImage != null) {
    // 更新快取
    _touch(key, liveImage, timelineTask);
    return liveImage.completer;
  }
  
  // 3 個 map 都沒有獲取到快取的圖片
  result = loader(); // 加載
  _trackLiveImage(key, _LiveImage(result, null, () => _liveImages.remove(key)));
  
	_PendingImage? untrackedPendingImage;

  //定義一個listener
	void listener(ImageInfo? info, bool syncCall) {
		// 加載的監聽
	}
  
  // 包裝一個listener
	final ImageStreamListener streamListener = ImageStreamListener(listener);
	if (maximumSize > 0 && maximumSizeBytes > 0) {
		// 放入快取
		_pendingImages[key] = _PendingImage(result, streamListener);
	} else {
		untrackedPendingImage = _PendingImage(result, streamListener);
	}
	// 添加監聽
	result.addListener(streamListener);
	return result;
}

listener 回呼的邏輯: 在 Image 狀態改變的時候,會觸發對 liveImages 的修改: ?

// Image
_imageStream.removeListener(_getListener());

// ImageStream
void removeListener(ImageStreamListener listener) {
  for (final VoidCallback callback in _onLastListenerRemovedCallbacks) {
    callback();
  }
  _onLastListenerRemovedCallbacks.clear();
}

而在 _trackLiveImage 的時候,_LiveImage 都注冊了上面的這個 callback:

_trackLiveImage(key, _LiveImage(image.completer, image.sizeBytes, () => _liveImages.remove(key)));

這時候改圖片會從 _liveImages 里面移除,

由此可見,快取的優先級為 pending -> cache -> live -> load,圖片快取和獲取的流程如下圖所示:

快取清理

在更新快取大小的時候,還會進行快取大小的檢查:

void _checkCacheSize(TimelineTask? timelineTask) {
  while (_currentSizeBytes > _maximumSizeBytes || _cache.length > _maximumSize) {
    final Object key = _cache.keys.first;
    final _CachedImage image = _cache[key]!;
    _currentSizeBytes -= image.sizeBytes!;
    _cache.remove(key);
  }
}

當當前快取總容量大于最大容量或者快取數量大于最大數量的時候,就會進行快取的清理, 所以上面使用快取的程序中,多次訪問的快取就會把key往后放,避免一上來就被清理掉, 所以 Flutter 自身的快取清理演算法也是遵循了 “最近最少使用” 的, 圖片快取的邏輯如下圖所示:

排麥 (2).png

圖片加載

圖片加載主要依賴上面的 load方法進行,不同的 ImageProvider 子類有自己的實作,例如

  • AssetImage
return MultiFrameImageStreamCompleter(
      codec: _loadAsync(key, decode),
      scale: key.scale,
      debugLabel: key.name,
      informationCollector: collector
    );
  • NetworkImage
final StreamController<ImageChunkEvent> chunkEvents =
        StreamController<ImageChunkEvent>();

    return MultiFrameImageStreamCompleter(
        chunkEvents: chunkEvents.stream,
        codec: _loadAsync(key as NetworkImage, decode, chunkEvents),
        scale: key.scale,
        debugLabel: key.url,
        informationCollector: _imageStreamInformationCollector(key));

邏輯基本一樣,具體特異的流程體現在 loadAsync里面: ?

// AssetImage _loadAsync
try {
      data = await key.bundle.load(key.name);
    } on FlutterError {
      PaintingBinding.instance!.imageCache!.evict(key);
      rethrow;
    }

if (data == null) {
// 加載資料是null,清掉這個key的快取
	PaintingBinding.instance!.imageCache!.evict(key);
	throw StateError('Unable to read data');
}

return await decode(data.buffer.asUint8List());


/// NetworkImage _loadAsync
Future<ui.Codec> _loadAsync(
      NetworkImage key,
      image_provider.DecoderCallback decode,
      StreamController<ImageChunkEvent> chunkEvents) {

	final Uri resolved = Uri.base.resolve(key.url);
	return ui.webOnlyInstantiateImageCodecFromUrl(resolved, // ignore: undefined_function
        chunkCallback: (int bytes, int total) {
      chunkEvents.add(ImageChunkEvent(
          cumulativeBytesLoaded: bytes, expectedTotalBytes: total));
    }) as Future<ui.Codec>;
}

這里分別會從 bundle 里加載圖片和從網路拉取圖片, ?

滑動中處理

還記得上面提到的 ScrollAwareImageProvider嗎,這里會有一個關于滑動中的判斷: ?

if (Scrollable.recommendDeferredLoadingForContext(context.context)) {
  SchedulerBinding.instance.scheduleFrameCallback((_) {
		scheduleMicrotask(() => resolveStreamForKey(configuration, stream, key, handleError));
	 });
	return;
}

當 if 里的邏輯成立,就把決議圖片的作業放到下一幀,recommendDeferredLoadingForContext 的具體邏輯: ?

static bool recommendDeferredLoadingForContext(BuildContext context) {
	
	final _ScrollableScope widget =
		context.getElementForInheritedWidgetOfExactType<_ScrollableScope>()?.widget as _ScrollableScope;
	if (widget == null) {
      return false;
    }
	// 存在滑動的widget
	return widget.position.recommendDeferredLoading(context);
}

這個會找到 Widget 樹里面最近的 _ScrollableScope,如果 ScrollableScope 處于快速滑動的時候,就回傳true,所以 flutter 在快速滑動的串列中是不會加載圖片的, ?

總結

到這里 Flutter 圖片的加載和快取管理就介紹完了,我們可以認識到幾個問題

  • Flutter 本身是有圖片的記憶體快取,也是按照 LRU 的演算法去管理快取的,并且快取池有閾值,我們可以自己去設定我們想要的記憶體閾值,
  • Flutter 本身沒有提供圖片的磁盤快取,APP 重啟之后圖片加載流程是會重新走的

Flutter學習資料以及Android進階學習大全可以找我免費領取哦
鏈接直達:https://jq.qq.com/?_wv=1027&k=7vXTe5CZ

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/353447.html

標籤:其他

上一篇:從用戶輸入創建物件

下一篇:Android FrameWork 原生開發現狀分析

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more