Flutter加載圖片流程MultiFrameImageStreamCompleter解析
MultiFrameImageStreamCompleter
MultiFrameImageStreamCompleter 是一個可組合的 ImageStreamCompleter 類,用于將多個 ImageStreamCompleter 對象合并為一個單獨的 ImageStream 對象,通常用于動畫效果。每當子 ImageStreamCompleter 接收到一個新的 ImageInfo 對象,它會立即通知其所有的監(jiān)聽器,并將對象傳遞給它們。
當 MultiFrameImageStreamCompleter 的 addListener() 方法被調(diào)用時,它將傳入的 ImageStreamListener 添加到其內(nèi)部的子 ImageStreamCompleter 的監(jiān)聽器列表中。如果 MultiFrameImageStreamCompleter 本身接收到一個 ImageInfo 對象,它會將它傳遞給其所有的監(jiān)聽器。但是,它不會自己管理這些幀,而是委托給每個子 ImageStreamCompleter 來完成。
MultiFrameImageStreamCompleter 還支持漸進式 JPEG,并實現(xiàn)了 addListener()、removeListener() 和 dispose() 方法,以及一個名為 getNextFrame() 的方法,用于從圖像流中獲取下一幀。
當所有幀都加載完畢后,MultiFrameImageStreamCompleter 將使用 dart:ui.Codec 解碼器將它們合并為一個單獨的 dart:ui.Image 對象,并將其傳遞給 setImage() 方法。最后,它將通知所有監(jiān)聽器,并將它們傳遞給 ImageStreamListener.onImage() 回調(diào)函數(shù),以通知它們新的 ImageInfo 已經(jīng)可用。
當 MultiFrameImageStreamCompleter 的 dispose() 方法被調(diào)用時,它會將其所有子 ImageStreamCompleter 的 dispose() 方法依次調(diào)用,以釋放所有資源,并取消所有未處理的幀請求。同時,它還會確保在釋放資源之前將所有錯誤通知給其監(jiān)聽器。
_handleCodecReady
void _handleCodecReady(ui.Codec codec) {
_codec = codec;
assert(_codec != null);
if (hasListeners) {
_decodeNextFrameAndSchedule();
}
}
在_handleCodecReady方法中,首先將傳入的codec對象賦值給成員變量_codec,然后使用assert語句來確保該變量不為空。接著,如果當前對象有監(jiān)聽器,則調(diào)用_decodeNextFrameAndSchedule方法來解碼下一幀并將其調(diào)度執(zhí)行。這里的目的是為了盡早地開始解碼下一幀圖像,以盡快展示出完整的動畫效果。如果沒有監(jiān)聽器,則不需要解碼下一幀圖像,因為沒有地方可以展示它。
_decodeNextFrameAndSchedule
Future<void> _decodeNextFrameAndSchedule() async {
// This will be null if we gave it away. If not, it's still ours and it
// must be disposed of.
_nextFrame?.image.dispose();
_nextFrame = null;
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.clone(),
scale: _scale,
debugLabel: debugLabel,
));
_nextFrame!.image.dispose();
_nextFrame = null;
return;
}
_scheduleAppFrame();
}
- 這個方法的作用是獲取下一幀并在獲取成功后調(diào)度下一幀的解碼,如果幀數(shù)為1,即這是一個靜態(tài)圖片,則只需返回該幀,并在沒有監(jiān)聽器時直接返回,如果幀數(shù)大于1,則調(diào)度下一幀的解碼。
- 在獲取下一幀之前,方法會清除上一幀并將_nextFrame置為null,以便準備下一幀。
- 如果解碼下一幀時發(fā)生異常,則會記錄錯誤并返回。如果在等待下一幀的解碼期間移除了監(jiān)聽器,則在沒有活動的監(jiān)聽器時不會發(fā)出幀,否則會發(fā)出幀并調(diào)度下一幀的解碼。
_emitFrame 方法的作用是向 ImageStreamCompleter 發(fā)送新的 ImageInfo。具體實現(xiàn)是通過調(diào)用 setImage 方法將 ImageInfo 設(shè)置為 ImageStreamCompleter 的當前值,同時更新 _framesEmitted 計數(shù)器。
_codec!.getNextFrame()
_nextFrame = await _codec!.getNextFrame();
/// Fetches the next animation frame.
///
/// Wraps back to the first frame after returning the last frame.
///
/// The returned future can complete with an error if the decoding has failed.
///
/// The caller of this method is responsible for disposing the
/// [FrameInfo.image] on the returned object.
Future<FrameInfo> getNextFrame() async {
final Completer<FrameInfo> completer = Completer<FrameInfo>.sync();
final String? error = _getNextFrame((_Image? image, int durationMilliseconds) {
if (image == null) {
completer.completeError(Exception('Codec failed to produce an image, possibly due to invalid image data.'));
} else {
completer.complete(FrameInfo._(
image: Image._(image, image.width, image.height),
duration: Duration(milliseconds: durationMilliseconds),
));
}
});
if (error != null) {
throw Exception(error);
}
return completer.future;
}
/// Returns an error message on failure, null on success.
String? _getNextFrame(void Function(_Image?, int) callback) native 'Codec_getNextFrame';
getNextFrame() 是 Codec 類的一個方法,用于獲取解碼后的幀。具體來說,它會在 Codec 內(nèi)部解碼圖像幀,返回一個 FrameInfo 對象,其中包含了解碼后的 Image 對象以及該幀的時間戳和持續(xù)時間等信息。由于 Codec 可能會支持動畫圖像,因此 getNextFrame() 方法可能會返回多個幀。
在 MultiFrameImageStreamCompleter 中,_decodeNextFrameAndSchedule() 方法會調(diào)用 _codec.getNextFrame() 方法獲取下一幀圖像,然后將其保存在 _nextFrame 屬性中。如果 _codec 的 frameCount 屬性為 1,說明這是一個靜態(tài)圖像,直接使用 _emitFrame() 方法發(fā)布該幀圖像;否則,調(diào)用 _scheduleAppFrame() 方法安排下一幀的發(fā)布。
_emitFrame(重要方法, 通知監(jiān)聽器觸發(fā)回調(diào),更新UI)
void _emitFrame(ImageInfo imageInfo) {
setImage(imageInfo);
_framesEmitted += 1;
}
這個方法在 _decodeNextFrameAndSchedule 中被調(diào)用,用于處理已解碼的下一幀圖像。如果當前幀是非動畫圖像,它會直接調(diào)用 setImage 方法更新 ImageStreamCompleter 的值,如果是動畫圖像,它會計劃下一幀的顯示并等待下一幀的解碼。
_scheduleAppFrame
void _scheduleAppFrame() {
if (_frameCallbackScheduled) {
return;
}
_frameCallbackScheduled = true;
SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame);
}
函數(shù) _scheduleAppFrame() 的作用是調(diào)度一個Flutter引擎幀回調(diào),在回調(diào)中會調(diào)用 _handleAppFrame() 函數(shù)。
具體來說,這個函數(shù)的實現(xiàn)包含以下步驟:
1、檢查 _frameCallbackScheduled 標志,如果為 true,則說明幀回調(diào)已經(jīng)被調(diào)度過,直接返回。
2、將 _frameCallbackScheduled 標志設(shè)置為 true,表示幀回調(diào)已經(jīng)被調(diào)度。
3、調(diào)用 SchedulerBinding.instance.scheduleFrameCallback() 函數(shù),向Flutter引擎注冊一個幀回調(diào)?;卣{(diào)函數(shù)為 _handleAppFrame()。
4、在 _handleAppFrame() 函數(shù)中,將會根據(jù)當前動畫播放的幀率和幀數(shù),計算下一幀應(yīng)該在何時被顯示,然后再次調(diào)用 _decodeNextFrameAndSchedule() 函數(shù),以獲取并顯示下一幀圖像。這樣就完成了一次動畫播放的循環(huán)。
_handleAppFrame
void _handleAppFrame(Duration timestamp) {
_frameCallbackScheduled = false;
if (!hasListeners) {
return;
}
assert(_nextFrame != null);
if (_isFirstFrame() || _hasFrameDurationPassed(timestamp)) {
_emitFrame(ImageInfo(
image: _nextFrame!.image.clone(),
scale: _scale,
debugLabel: debugLabel,
));
_shownTimestamp = timestamp;
_frameDuration = _nextFrame!.duration;
_nextFrame!.image.dispose();
_nextFrame = null;
final int completedCycles = _framesEmitted ~/ _codec!.frameCount;
if (_codec!.repetitionCount == -1 || completedCycles <= _codec!.repetitionCount) {
_decodeNextFrameAndSchedule();
}
return;
}
final Duration delay = _frameDuration! - (timestamp - _shownTimestamp);
_timer = Timer(delay * timeDilation, () {
_scheduleAppFrame();
});
}
函數(shù) _handleAppFrame 是 MultiFrameImageStreamCompleter 的核心函數(shù),用于處理多幀圖像的邏輯。下面是對該函數(shù)的詳細解讀:
1、
_frameCallbackScheduled = false;- 將
_frameCallbackScheduled設(shè)為 false,表示下一幀還沒有調(diào)度。
- 將
2、
if (!hasListeners) { return; }- 如果沒有監(jiān)聽器,則直接返回。
3、
assert(_nextFrame != null);- 斷言
_nextFrame不為空。
- 斷言
4、
_isFirstFrame() || _hasFrameDurationPassed(timestamp)- 如果是第一幀或者幀時間已經(jīng)超過了
_frameDuration,則進行以下操作:
- 如果是第一幀或者幀時間已經(jīng)超過了
5、
_emitFrame(ImageInfo(image: _nextFrame!.image.clone(), scale: _scale, debugLabel: debugLabel));- 發(fā)出
ImageInfo事件,將_nextFrame的圖像信息作為參數(shù)傳入。
- 發(fā)出
6、
_shownTimestamp = timestamp;- 更新
_shownTimestamp為當前時間戳。
- 更新
7、
_frameDuration = _nextFrame!.duration;- 更新
_frameDuration為_nextFrame的幀間隔時間。
- 更新
8、
_nextFrame!.image.dispose(); _nextFrame = null;- 釋放
_nextFrame的圖像資源并將_nextFrame設(shè)為 null。
- 釋放
9、
final int completedCycles = _framesEmitted ~/ _codec!.frameCount;- 計算已經(jīng)完成的循環(huán)次數(shù)。
10、
_codec!.repetitionCount == -1 || completedCycles <= _codec!.repetitionCount- 如果循環(huán)次數(shù)為 -1(表示無限循環(huán))或者已經(jīng)完成的循環(huán)次數(shù)小于等于
_codec的循環(huán)次數(shù),則進行以下操作:
- 如果循環(huán)次數(shù)為 -1(表示無限循環(huán))或者已經(jīng)完成的循環(huán)次數(shù)小于等于
11、
_decodeNextFrameAndSchedule();- 解碼下一幀并調(diào)度下一幀的繪制。
12、
final Duration delay = _frameDuration! - (timestamp - _shownTimestamp);- 計算下一幀需要延遲的時間。
13、
_timer = Timer(delay * timeDilation, () { _scheduleAppFrame(); });- 使用計時器來實現(xiàn)下一幀的延遲繪制。延遲時間為
delay乘以timeDilation(可以通過調(diào)用timeDilation = x來改變時間流逝的速度)。當計時器觸發(fā)時,將調(diào)用_scheduleAppFrame來調(diào)度下一幀的繪制。
- 使用計時器來實現(xiàn)下一幀的延遲繪制。延遲時間為
addListener
void addListener(ImageStreamListener listener) {
if (!hasListeners && _codec != null && (_currentImage == null || _codec!.frameCount > 1)) {
_decodeNextFrameAndSchedule();
}
super.addListener(listener);
}
這個方法是 ImageStreamCompleter 類的方法,用于向 ImageStreamCompleter 添加監(jiān)聽器。當?shù)谝粋€監(jiān)聽器被添加到 ImageStreamCompleter 上時,會檢查 _codec 是否為 null,如果不為 null 并且有多幀圖像或者當前圖像為 null,則會調(diào)用 _decodeNextFrameAndSchedule() 方法開始解碼下一幀圖像并計劃渲染。這樣做是為了確保在第一個監(jiān)聽器被添加到 ImageStreamCompleter 上時就開始解碼下一幀圖像并在下一幀渲染完成后通知所有監(jiān)聽器。如果 _codec 為 null 或者當前圖像為單幀圖像,則不會調(diào)用 _decodeNextFrameAndSchedule() 方法。在這個方法中,調(diào)用了 super.addListener(listener) 將監(jiān)聽器添加到監(jiān)聽器列表中。
removeListener
void removeListener(ImageStreamListener listener) {
super.removeListener(listener);
if (!hasListeners) {
_timer?.cancel();
_timer = null;
}
}
removeListener 方法用于從 MultiFrameImageStreamCompleter 中移除給定的 ImageStreamListener。當移除后,如果該對象不再有任何監(jiān)聽器,就會取消定時器 _timer。
具體來說,該方法會先調(diào)用父類的 removeListener 方法,將該監(jiān)聽器從監(jiān)聽器列表中移除。接著,如果此時 hasListeners 為 false,說明沒有任何監(jiān)聽器,就會取消 _timer 定時器,以便釋放資源。
_maybeDispose
void _maybeDispose() {
super._maybeDispose();
if (_disposed) {
_chunkSubscription?.onData(null);
_chunkSubscription?.cancel();
_chunkSubscription = null;
}
}
_maybeDispose()是一個用來釋放資源的方法,當圖片流不再被監(jiān)聽時調(diào)用。它首先調(diào)用父類的_maybeDispose()方法,以處理父類中的一些釋放資源的邏輯。然后它會檢查_disposed屬性是否為true,如果是,則取消并置空_chunkSubscription,這個對象是用來訂閱圖像數(shù)據(jù)塊的流。這樣做是為了釋放相關(guān)的資源,以防止內(nèi)存泄漏。
總結(jié)
MultiFrameImageStreamCompleter 是 Flutter 中用于處理多幀圖片的類,主要用于將多幀動畫圖片的每一幀渲染到屏幕上。
該類內(nèi)部主要維護了一個 Codec 對象,用于解碼圖片,同時也有一個 ImageInfo 對象用于存儲當前幀的信息,并且該類也實現(xiàn)了 ImageStreamCompleter 類,可以作為 Image 對象的 ImageStream。
在 MultiFrameImageStreamCompleter 的初始化過程中,會創(chuàng)建 Codec 對象,并且在該對象準備好后進行處理,并且在添加監(jiān)聽器時,如果該類當前沒有監(jiān)聽器,并且已經(jīng)獲取了第一幀圖像,那么該類會進行后續(xù)幀的解碼和渲染。如果該類被銷毀,則會清空 Codec 對象。
在該類的主要方法中,_handleCodecReady() 方法會在初始化時進行調(diào)用,用于設(shè)置解碼后的 Codec 對象,并在有監(jiān)聽器的情況下開始解碼和渲染下一幀圖片。
_decodeNextFrameAndSchedule() 方法用于解碼和渲染下一幀圖片,通過 _codec!.getNextFrame() 方法獲取下一幀圖像,并進行渲染處理,如果當前只有一幀圖片,則直接渲染該幀圖片并停止。
_handleAppFrame() 方法用于處理渲染下一幀圖片的邏輯,會根據(jù)時間戳計算出下一幀圖片的渲染時間,并設(shè)置延時定時器,定時調(diào)用該方法。
addListener() 和 removeListener() 方法用于添加和刪除監(jiān)聽器,并在沒有監(jiān)聽器時停止解碼和渲染。
最后,_maybeDispose() 方法會在該類被銷毀時進行調(diào)用,用于清空內(nèi)部緩存。
參考鏈接
以上就是Flutter加載圖片流程MultiFrameImageStreamCompleter解析的詳細內(nèi)容,更多關(guān)于Flutter MultiFrameImageStreamCompleter的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android中ScrollView嵌套GridView的解決辦法
前些日子在開發(fā)中用到了需要ScrollView嵌套GridView的情況,由于這兩款控件都自帶滾動條,當他們碰到一起的時候便會出問題,即GridView會顯示不全。下面小編給大家分享下解決方案,需要的朋友可以參考下2017-04-04
Android實現(xiàn)系統(tǒng)狀態(tài)欄的隱藏和顯示功能
這篇文章主要介紹了Android實現(xiàn)系統(tǒng)狀態(tài)欄的隱藏和顯示功能,文中還給大家?guī)硭姆N方法,大家可以根據(jù)自己需要參考下2018-07-07
AndroidStudio Gradle第三依賴統(tǒng)一管理的實現(xiàn)方法
這篇文章主要介紹了AndroidStudio Gradle第三依賴統(tǒng)一管理的實現(xiàn)方法,需要的朋友可以參考下2017-09-09
Android自定義ViewGroup之WaterfallLayout(二)
這篇文章主要為大家詳細介紹了Android自定義ViewGroup之WaterfallLayout,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-09-09
Android Handler中的休眠喚醒實現(xiàn)詳解
這篇文章主要為大家介紹了Android Handler中的休眠喚醒實現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-01-01
Flutter的鍵值存儲數(shù)據(jù)庫使用示例詳解
這篇文章主要為大家介紹了Flutter的鍵值存儲數(shù)據(jù)庫使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-08-08

