相信 Flutter 的開(kāi)發(fā)者應(yīng)該遇到過(guò),對(duì)于大量數(shù)據(jù)的列表進(jìn)行圖片加載時(shí),在 iOS 上很容易出現(xiàn) OOM
的問(wèn)題,這是因?yàn)?Flutter 特殊的圖片加載流程造成。
在 Android 上 Flutter
Image
主要占用的內(nèi)存不是JVM
的內(nèi)存,而是Graphics
相關(guān)的內(nèi)存,這樣的內(nèi)存調(diào)用可以最大程度利用 Native 內(nèi)存。
一、默認(rèn)流程
Flutter 默認(rèn)在進(jìn)行圖片加載時(shí),會(huì)先通過(guò)對(duì)應(yīng)的 ImageProvider
去加載圖片數(shù)據(jù),然后通過(guò) PaintingBinding
對(duì)數(shù)據(jù)進(jìn)行編碼,之后返回包含編碼后圖片數(shù)據(jù)和信息的 ImageInfo
去實(shí)現(xiàn)繪制。
詳細(xì)圖片加載流程可見(jiàn):《十、 深入圖片加載流程)》
本身這個(gè)邏輯并沒(méi)有什么問(wèn)題,問(wèn)題就在于 Flutter 中對(duì)于圖片在內(nèi)存中的 Cache 對(duì)象是一個(gè) ImageStream
對(duì)象。
Flutter 中 ImageCache
緩存的是一個(gè)異步對(duì)象,緩存異步加載對(duì)象的一個(gè)問(wèn)題是:在圖片加載解碼完成之前,你無(wú)法知道到底將要消耗多少內(nèi)存,并且大量的圖片加載,會(huì)導(dǎo)致的解碼任務(wù)需要產(chǎn)生大量的IO。
所以一開(kāi)始最粗暴的情況是:通過(guò) PaintingBinding.instance
去設(shè)置 maximumSize
和 maximumSizeBytes
,但是這種簡(jiǎn)單粗爆的處理方法并不能解決長(zhǎng)列表圖片加載的溢出問(wèn)題,因?yàn)樵陂L(zhǎng)列表中,快速滑動(dòng)的情況下可能會(huì)在一瞬間“并發(fā)”出大量圖片加載需求。
所以在 1.17 版本上,官方針對(duì)這種情況提供了場(chǎng)景化的處理方式: ScrollAwareImageProvider
。
二、ScrollAwareImageProvider
1.17 中可以看到,在 Image
控件中原本 _resolveImage
方法所使用的 imageProvider
被 ScrollAwareImageProvider
所代理,并且多了一個(gè)叫 DisposableBuildContext<State<Image>>
的 context 參數(shù)。那 ScrollAwareImageProvider
的作用是什么呢?
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,
));
assert(newStream != null);
_updateSourceStream(newStream);
}
其實(shí) ScrollAwareImageProvider
對(duì)象最主要的使用就是在 resolveStreamForKey
方法中,通過(guò) Scrollable.recommendDeferredLoadingForContext
方法去判斷當(dāng)前是不是需要推遲當(dāng)前幀畫(huà)面的加載,換言之就是:是否處于快速滑動(dòng)的過(guò)程。
那 Scrollable.recommendDeferredLoadingForContext
作為一個(gè) static
方法,如何判斷當(dāng)前是不是處于列表的快速滑動(dòng)呢?
這就需要通過(guò)當(dāng)前 context
的 getElementForInheritedWidgetOfExactType
方法去獲取 Scrollable
內(nèi)的 _ScrollableScope
。
_ScrollableScope
是Scrollable
內(nèi)的一個(gè)InheritedWidget
,而 Flutter 中的可滑動(dòng)視圖內(nèi)必然會(huì)有Scrollable
,所以只要Image
是在列表內(nèi),就可以通過(guò)context.getElementForInheritedWidgetOfExactType<_ScrollableScope>()
去獲取到_ScrollableScope
。
獲取到 _ScrollableScope
就可以獲取到它內(nèi)部的 ScrollPosition
, 進(jìn)而它的 ScrollPhysics
對(duì)應(yīng)的 recommendDeferredLoading
方法,判斷列表是否處于快速滑動(dòng)狀態(tài)。所以判斷是否快速滑動(dòng)的邏輯其實(shí)是在 ScrollPhysics
。
bool recommendDeferredLoading(double velocity, ScrollMetrics metrics, BuildContext context) {
assert(velocity != null);
assert(metrics != null);
assert(context != null);
if (parent == null) {
final double maxPhysicalPixels = WidgetsBinding.instance.window.physicalSize.longestSide;
return velocity.abs() > maxPhysicalPixels;
}
return parent.recommendDeferredLoading(velocity, metrics, context);
}
關(guān)于
ScrollPhysics
的解釋可以看 《十八、 神奇的ScrollPhysics與Simulation》
然后回到 resolveStreamForKey
方法,可以看到當(dāng) Scrollable.recommendDeferredLoadingForContext
返回 true
時(shí)就等待,等待就是會(huì)通過(guò) SchedulerBinding
在下一幀繪制時(shí)再次調(diào)用 resolveStreamForKey
, 遞歸再走一遍 resolveStreamForKey
的邏輯,如果判斷此時(shí)不再是快速滑動(dòng),就走正常的圖片加載邏輯。
@override
void resolveStreamForKey(
ImageConfiguration configuration,
ImageStream stream,
T key,
ImageErrorListener handleError,
) {
if (stream.completer != null || PaintingBinding.instance.imageCache.containsKey(key)) {
imageProvider.resolveStreamForKey(configuration, stream, key, handleError);
return;
}
if (context.context == null) {
return;
}
if (Scrollable.recommendDeferredLoadingForContext(context.context)) {
SchedulerBinding.instance.scheduleFrameCallback((_) {
scheduleMicrotask(() => resolveStreamForKey(configuration, stream, key, handleError));
});
return;
}
imageProvider.resolveStreamForKey(configuration, stream, key, handleError);
}
如上代碼所示,可以看到在 ScrollAwareImageProvider
的 resolveStreamForKey
方法中,當(dāng) stream.completer != null
且存在緩存時(shí),直接就去加載原本已有的流程,如果快速滑動(dòng)過(guò)程中圖片還沒(méi)加載的,就先不加載。
Flutter 中為了防止
context
在圖片異步加載流程中持有導(dǎo)致內(nèi)存泄漏,又針對(duì)Image
封裝了一個(gè)DisposableBuildContext
。
DisposableBuildContext
是通過(guò)持有State
來(lái)持有context
的,并且在dispose
時(shí)將_state = null
設(shè)置為null
來(lái)清除對(duì)State
的持有。所以可以看到 上述代碼中,context.context == null
時(shí)直接就return
了。
另外前面介紹的 resolveStreamForKey
也是新增加的方法,在原本的 ImageProvider
進(jìn)行圖片加載時(shí),會(huì)通過(guò) ImageStream resolve
方法去得到并返回一個(gè) ImageStream
。
而 resolveStreamForKey
將原本 imageCache
和 ImageStreamCompleter
的流程抽象出來(lái),并且在 ScrollAwareImageProvider
中重寫(xiě)了 resolveStreamForKey
方法的執(zhí)行邏輯,這樣快速滑動(dòng)時(shí),圖片的下載和解碼可以被中斷,從而減少了不必要的內(nèi)存占用。
雖然這種方法不能100%解決圖片加載時(shí) OOM 的問(wèn)題,但是很大程度優(yōu)化了列表中的圖片內(nèi)存占用,官方提供的數(shù)據(jù)上看理論上可以在原本基礎(chǔ)上節(jié)省出 70% 的內(nèi)存。
相關(guān)推薦:Merged Defer image decoding when scrolling fast #49389
資源推薦
- Github : https://github.com/CarGuo
- 開(kāi)源 Flutter 完整項(xiàng)目:https://github.com/CarGuo/GSYGithubAppFlutter
- 開(kāi)源 Flutter 多案例學(xué)習(xí)型項(xiàng)目: https://github.com/CarGuo/GSYFlutterDemo
- 開(kāi)源 Fluttre 實(shí)戰(zhàn)電子書(shū)項(xiàng)目:https://github.com/CarGuo/GSYFlutterBook
- 開(kāi)源 React Native 項(xiàng)目:https://github.com/CarGuo/GSYGithubApp