From 92bbbcb0eb1d6fc1db1c79a93fdf4356bd1da6db Mon Sep 17 00:00:00 2001 From: roxy Date: Mon, 11 May 2026 14:08:43 +0800 Subject: [PATCH] =?UTF-8?q?svg=E8=B5=84=E6=BA=90=E9=87=8A=E6=94=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/main.dart | 2 + .../profile_gift_wall_detail_page.dart | 37 ++--- .../tools/sc_gift_vap_svga_manager.dart | 71 ++++++++-- .../widgets/headdress/headdress_widget.dart | 75 ++++++---- .../room/anim/room_entrance_widget.dart | 88 +++++++++--- .../widgets/svga/sc_network_svga_widget.dart | 52 +++++-- .../widgets/svga/sc_svga_asset_widget.dart | 129 +++++++++++++++++- 7 files changed, 356 insertions(+), 98 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index fd4fb7c..b7ba99e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -46,6 +46,7 @@ import 'services/auth/user_profile_manager.dart'; import 'ui_kit/theme/socialchat_theme.dart'; import 'ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart'; import 'ui_kit/widgets/room/effect/vapp_svga_layer_widget.dart'; +import 'ui_kit/widgets/svga/sc_svga_asset_widget.dart'; bool _isCrashlyticsReady = false; @@ -108,6 +109,7 @@ void _configureImageCache() { void _releaseVisualMemoryCaches({bool clearLiveImages = false}) { SCGiftVapSvgaManager().clearMemoryCache(); + SCSvgaAssetWidget.clearMemoryCache(); final imageCache = PaintingBinding.instance.imageCache; imageCache.clear(); if (clearLiveImages) { diff --git a/lib/modules/user/profile/profile_gift_wall_detail_page.dart b/lib/modules/user/profile/profile_gift_wall_detail_page.dart index d24dcfb..4b0865e 100644 --- a/lib/modules/user/profile/profile_gift_wall_detail_page.dart +++ b/lib/modules/user/profile/profile_gift_wall_detail_page.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_svga/flutter_svga.dart'; @@ -566,6 +564,7 @@ class _GiftWallSvgaEffectOverlay extends StatefulWidget { class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay> with SingleTickerProviderStateMixin { late final SVGAAnimationController _controller; + MovieEntity? _retainedMovieEntity; bool _loaded = false; bool _finished = false; @@ -585,9 +584,15 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay> Future _loadAndPlay() async { try { - final movieEntity = await _loadMovieEntity(widget.sourceUrl); - if (!mounted || _finished) return; + final movieEntity = await SCGiftVapSvgaManager().acquireSvgaEntity( + widget.sourceUrl, + ); + if (!mounted || _finished) { + SCGiftVapSvgaManager().releaseSvgaEntity(movieEntity); + return; + } setState(() { + _retainedMovieEntity = movieEntity; _loaded = true; _controller.videoItem = movieEntity; }); @@ -599,28 +604,6 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay> } } - Future _loadMovieEntity(String sourceUrl) async { - final cache = SCGiftVapSvgaManager().videoItemCache; - final cached = cache[sourceUrl]; - if (cached != null) return cached; - - final pathType = SCPathUtils.getPathType(sourceUrl); - late final MovieEntity movieEntity; - if (pathType == PathType.network) { - movieEntity = await SVGAParser.shared.decodeFromURL(sourceUrl); - } else if (pathType == PathType.asset) { - movieEntity = await SVGAParser.shared.decodeFromAssets(sourceUrl); - } else if (pathType == PathType.file) { - final bytes = await File(sourceUrl).readAsBytes(); - movieEntity = await SVGAParser.shared.decodeFromBuffer(bytes); - } else { - throw Exception("Unsupported SVGA source: $sourceUrl"); - } - movieEntity.autorelease = false; - cache[sourceUrl] = movieEntity; - return movieEntity; - } - void _finish() { if (_finished) return; _finished = true; @@ -631,6 +614,8 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay> void dispose() { _controller.removeStatusListener(_handleStatusChanged); _controller.dispose(); + SCGiftVapSvgaManager().releaseSvgaEntity(_retainedMovieEntity); + _retainedMovieEntity = null; super.dispose(); } diff --git a/lib/shared/tools/sc_gift_vap_svga_manager.dart b/lib/shared/tools/sc_gift_vap_svga_manager.dart index 67f02c5..a5c35c5 100644 --- a/lib/shared/tools/sc_gift_vap_svga_manager.dart +++ b/lib/shared/tools/sc_gift_vap_svga_manager.dart @@ -23,7 +23,8 @@ class SCGiftVapSvgaManager { static const int _maxConsecutiveEntryTasksBeforeGift = 2; static const Duration _taskWatchdogTimeout = Duration(seconds: 20); static const Duration _entryTaskTtl = Duration(seconds: 6); - Map videoItemCache = {}; + final Map _videoItemCache = {}; + final Map _svgaEntityUseCounts = {}; static SCGiftVapSvgaManager? _inst; static const int _maxPreloadConcurrency = 1; static const int _maxPreloadQueueLength = 12; @@ -79,6 +80,44 @@ class SCGiftVapSvgaManager { return SCPathUtils.getFileExtension(path).toLowerCase() == ".svga"; } + Future acquireSvgaEntity(String path) async { + final entity = await _loadSvgaEntity(path); + _retainSvgaEntity(entity); + return entity; + } + + void releaseSvgaEntity(MovieEntity? entity) { + if (entity == null) { + return; + } + final count = _svgaEntityUseCounts[entity] ?? 0; + if (count <= 1) { + _svgaEntityUseCounts.remove(entity); + if (!_videoItemCache.containsValue(entity) && + !_isSvgaEntityInUse(entity)) { + _disposeSvgaEntity(entity); + } else { + _trimSvgaCache(); + } + return; + } + _svgaEntityUseCounts[entity] = count - 1; + } + + void evictSvgaEntity(String path, {bool includeCurrent = false}) { + final entity = _videoItemCache.remove(path); + if (entity == null) { + return; + } + if (includeCurrent || !_isSvgaEntityInUse(entity)) { + _disposeSvgaEntity(entity); + } + } + + void _retainSvgaEntity(MovieEntity entity) { + _svgaEntityUseCounts[entity] = (_svgaEntityUseCounts[entity] ?? 0) + 1; + } + static int resolveEffectPriority({ int priority = 0, int type = giftEffectType, @@ -116,7 +155,7 @@ class SCGiftVapSvgaManager { bool _isPreloadedOrLoading(String path) { if (_needsSvgaController(path)) { - return videoItemCache.containsKey(path) || + return _videoItemCache.containsKey(path) || _svgaLoadTasks.containsKey(path); } final pathType = SCPathUtils.getPathType(path); @@ -256,19 +295,19 @@ class SCGiftVapSvgaManager { } MovieEntity? _touchCachedSvgaEntity(String path) { - final cached = videoItemCache.remove(path); + final cached = _videoItemCache.remove(path); if (cached != null) { - videoItemCache[path] = cached; + _videoItemCache[path] = cached; } return cached; } void _cacheSvgaEntity(String path, MovieEntity entity) { - final previous = videoItemCache.remove(path); + final previous = _videoItemCache.remove(path); if (previous != null && !identical(previous, entity)) { _disposeSvgaEntityIfUnused(previous); } - videoItemCache[path] = entity; + _videoItemCache[path] = entity; _trimSvgaCache(); } @@ -287,17 +326,17 @@ class SCGiftVapSvgaManager { } void _trimSvgaCache() { - while (videoItemCache.length > _maxSvgaCacheEntries) { - final removableEntry = videoItemCache.entries.firstWhere( + while (_videoItemCache.length > _maxSvgaCacheEntries) { + final removableEntry = _videoItemCache.entries.firstWhere( (entry) => !_isSvgaEntityInUse(entry.value), - orElse: () => videoItemCache.entries.first, + orElse: () => _videoItemCache.entries.first, ); if (_isSvgaEntityInUse(removableEntry.value)) { - videoItemCache.remove(removableEntry.key); - videoItemCache[removableEntry.key] = removableEntry.value; + _videoItemCache.remove(removableEntry.key); + _videoItemCache[removableEntry.key] = removableEntry.value; break; } - videoItemCache.remove(removableEntry.key); + _videoItemCache.remove(removableEntry.key); _disposeSvgaEntity(removableEntry.value); } } @@ -752,20 +791,21 @@ class SCGiftVapSvgaManager { _preloadQueue.clear(); _queuedPreloadPaths.clear(); _activePreloadCount = 0; - final entries = videoItemCache.entries.toList(growable: false); + final entries = _videoItemCache.entries.toList(growable: false); for (final entry in entries) { final entity = entry.value; if (!includeCurrent && _isSvgaEntityInUse(entity)) { continue; } - videoItemCache.remove(entry.key); + _videoItemCache.remove(entry.key); _disposeSvgaEntity(entity); } _playablePathCache.clear(); } bool _isSvgaEntityInUse(MovieEntity entity) { - return identical(_rsc?.videoItem, entity); + return identical(_rsc?.videoItem, entity) || + (_svgaEntityUseCounts[entity] ?? 0) > 0; } void _disposeSvgaEntityIfUnused(MovieEntity entity) { @@ -777,6 +817,7 @@ class SCGiftVapSvgaManager { void _disposeSvgaEntity(MovieEntity entity) { try { + _svgaEntityUseCounts.remove(entity); entity.dispose(); } catch (_) {} } diff --git a/lib/ui_kit/widgets/headdress/headdress_widget.dart b/lib/ui_kit/widgets/headdress/headdress_widget.dart index b9691c8..e4f97a7 100644 --- a/lib/ui_kit/widgets/headdress/headdress_widget.dart +++ b/lib/ui_kit/widgets/headdress/headdress_widget.dart @@ -48,6 +48,8 @@ class SVGAHeadwearWidget extends StatefulWidget { class _SVGAHeadwearWidgetState extends State with SingleTickerProviderStateMixin { SVGAAnimationController? _animationController; + MovieEntity? _retainedMovieEntity; + String? _retainedResource; bool _isLoading = true; bool _isNetworkResource = false; bool _hasError = false; @@ -82,41 +84,53 @@ class _SVGAHeadwearWidgetState extends State }); try { - // 检查缓存 - MovieEntity? videoItem; if (widget.useCache) { - videoItem = SCGiftVapSvgaManager().videoItemCache[widget.resource]; - } + final videoItem = + _retainedResource == widget.resource && _retainedMovieEntity != null + ? _retainedMovieEntity! + : await SCGiftVapSvgaManager().acquireSvgaEntity( + widget.resource, + ); + final acquiredNewEntity = !identical(videoItem, _retainedMovieEntity); + if (!mounted) { + if (acquiredNewEntity) { + SCGiftVapSvgaManager().releaseSvgaEntity(videoItem); + } + return; + } - // 如果没有缓存,则进行解析 - if (videoItem == null) { - videoItem = + setState(() { + _releaseRetainedMovieEntity(except: videoItem); + _retainedMovieEntity = videoItem; + _retainedResource = widget.resource; + _animationController?.videoItem = videoItem; + _isLoading = false; + }); + } else { + final videoItem = _isNetworkResource ? await SVGAParser.shared.decodeFromURL(widget.resource) : await SVGAParser.shared.decodeFromAssets(widget.resource); - videoItem.autorelease =false; - // 存入缓存 - if (widget.useCache) { - SCGiftVapSvgaManager().videoItemCache[widget.resource] = videoItem; + if (!mounted) { + videoItem.dispose(); + return; } - } - if (mounted) { setState(() { _animationController?.videoItem = videoItem; _isLoading = false; }); + } - // 根据循环次数设置播放方式 - if (widget.loops == 0) { - _animationController?.repeat(); - } else { - _animationController?.repeat(count: widget.loops); - } + // 根据循环次数设置播放方式 + if (widget.loops == 0) { + _animationController?.repeat(); + } else { + _animationController?.repeat(count: widget.loops); + } - if (widget.onFinishLoading != null) { - widget.onFinishLoading!(); - } + if (widget.onFinishLoading != null) { + widget.onFinishLoading!(); } } catch (e) { if (mounted) { @@ -157,14 +171,16 @@ class _SVGAHeadwearWidgetState extends State _animationController?.stop(); if (widget.clearsAfterStop) { _animationController?.videoItem = null; + _releaseRetainedMovieEntity(); } } // 重新加载动画 void reload() { if (widget.useCache) { - // 清除缓存项 - SCGiftVapSvgaManager().videoItemCache.remove(widget.resource); + _animationController?.videoItem = null; + _releaseRetainedMovieEntity(); + SCGiftVapSvgaManager().evictSvgaEntity(widget.resource); } _loadAnimation(); } @@ -173,9 +189,20 @@ class _SVGAHeadwearWidgetState extends State void dispose() { stop(); _animationController?.dispose(); + _releaseRetainedMovieEntity(); super.dispose(); } + void _releaseRetainedMovieEntity({MovieEntity? except}) { + final entity = _retainedMovieEntity; + if (entity == null || identical(entity, except)) { + return; + } + _retainedMovieEntity = null; + _retainedResource = null; + SCGiftVapSvgaManager().releaseSvgaEntity(entity); + } + @override Widget build(BuildContext context) { if (_isLoading && !widget.autoPlay) { diff --git a/lib/ui_kit/widgets/room/anim/room_entrance_widget.dart b/lib/ui_kit/widgets/room/anim/room_entrance_widget.dart index 11b57c0..6c954fc 100644 --- a/lib/ui_kit/widgets/room/anim/room_entrance_widget.dart +++ b/lib/ui_kit/widgets/room/anim/room_entrance_widget.dart @@ -270,6 +270,8 @@ class _RoomEntranceWidgetState extends State bool _isLoading = false; bool _hasError = false; String? _currentResource; + MovieEntity? _retainedMovieEntity; + String? _retainedResource; // 用于存储SVGAImage组件的key,以便刷新占位符 GlobalKey _svgaKey = GlobalKey(); @@ -351,45 +353,79 @@ class _RoomEntranceWidgetState extends State _hasError = false; }); + MovieEntity? acquiredMovieEntity; try { - final isNetworkResource = resource.startsWith('http'); - MovieEntity? videoItem; - if (widget.useCache) { - videoItem = SCGiftVapSvgaManager().videoItemCache[resource]; - } + final videoItem = + _retainedResource == resource && _retainedMovieEntity != null + ? _retainedMovieEntity! + : await SCGiftVapSvgaManager().acquireSvgaEntity(resource); + final acquiredNewEntity = !identical(videoItem, _retainedMovieEntity); + if (acquiredNewEntity) { + acquiredMovieEntity = videoItem; + } + if (!mounted) { + if (acquiredNewEntity) { + SCGiftVapSvgaManager().releaseSvgaEntity(videoItem); + acquiredMovieEntity = null; + } + return; + } - if (videoItem == null) { - videoItem = + // 应用动态数据(文本、图片替换) + await _applyDynamicData(videoItem, dynamicData); + if (!mounted) { + if (acquiredNewEntity) { + SCGiftVapSvgaManager().releaseSvgaEntity(videoItem); + acquiredMovieEntity = null; + } + return; + } + + setState(() { + _releaseRetainedMovieEntity(except: videoItem); + _retainedMovieEntity = videoItem; + _retainedResource = resource; + _animationController?.videoItem = videoItem; + _isLoading = false; + }); + acquiredMovieEntity = null; + } else { + final isNetworkResource = resource.startsWith('http'); + final videoItem = isNetworkResource ? await SVGAParser.shared.decodeFromURL(resource) : await SVGAParser.shared.decodeFromAssets(resource); - videoItem.autorelease = false; - - if (widget.useCache) { - SCGiftVapSvgaManager().videoItemCache[resource] = videoItem; + if (!mounted) { + videoItem.dispose(); + return; } - } - if (mounted) { // 应用动态数据(文本、图片替换) await _applyDynamicData(videoItem, dynamicData); + if (!mounted) { + videoItem.dispose(); + return; + } setState(() { _animationController?.videoItem = videoItem; _isLoading = false; }); + } - // 开始播放动画 - _startAnimation(); + // 开始播放动画 + _startAnimation(); - if (widget.onFinishLoading != null) { - widget.onFinishLoading!(); - } + if (widget.onFinishLoading != null) { + widget.onFinishLoading!(); } } catch (e) { + SCGiftVapSvgaManager().releaseSvgaEntity(acquiredMovieEntity); if (mounted) { setState(() { + _animationController?.videoItem = null; + _releaseRetainedMovieEntity(); _isLoading = false; _hasError = true; }); @@ -543,6 +579,7 @@ class _RoomEntranceWidgetState extends State _animationController?.stop(); if (widget.clearsAfterStop) { _animationController?.videoItem = null; + _releaseRetainedMovieEntity(); setState(() { _currentResource = null; }); @@ -553,7 +590,9 @@ class _RoomEntranceWidgetState extends State void reload() { if (_currentResource != null) { if (widget.useCache) { - SCGiftVapSvgaManager().videoItemCache.remove(_currentResource); + _animationController?.videoItem = null; + _releaseRetainedMovieEntity(); + SCGiftVapSvgaManager().evictSvgaEntity(_currentResource!); } _loadAnimation( @@ -568,11 +607,22 @@ class _RoomEntranceWidgetState extends State void dispose() { stop(); _animationController?.dispose(); + _releaseRetainedMovieEntity(); // 从队列管理器中移除当前播放器 _queueManager.clearCurrentPlayer(); super.dispose(); } + void _releaseRetainedMovieEntity({MovieEntity? except}) { + final entity = _retainedMovieEntity; + if (entity == null || identical(entity, except)) { + return; + } + _retainedMovieEntity = null; + _retainedResource = null; + SCGiftVapSvgaManager().releaseSvgaEntity(entity); + } + @override Widget build(BuildContext context) { // 显示加载状态 diff --git a/lib/ui_kit/widgets/svga/sc_network_svga_widget.dart b/lib/ui_kit/widgets/svga/sc_network_svga_widget.dart index a693f6b..d497d0f 100644 --- a/lib/ui_kit/widgets/svga/sc_network_svga_widget.dart +++ b/lib/ui_kit/widgets/svga/sc_network_svga_widget.dart @@ -52,6 +52,8 @@ class _SCNetworkSvgaWidgetState extends State with SingleTickerProviderStateMixin { late final SVGAAnimationController _controller; String? _loadedResource; + MovieEntity? _retainedMovieEntity; + String? _retainedResource; bool _hasError = false; @override @@ -78,9 +80,10 @@ class _SCNetworkSvgaWidgetState extends State final resource = widget.resource.trim(); if (resource.isEmpty || !SCNetworkSvgaWidget.isSvga(resource)) { setState(() { + _controller.videoItem = null; + _releaseRetainedMovieEntity(); _hasError = true; _loadedResource = null; - _controller.videoItem = null; }); return; } @@ -89,37 +92,53 @@ class _SCNetworkSvgaWidgetState extends State _hasError = false; }); + MovieEntity? acquiredMovieEntity; try { - final cache = SCGiftVapSvgaManager().videoItemCache; - var movieEntity = cache[resource]; - if (movieEntity == null) { - movieEntity = - resource.startsWith("http") - ? await SVGAParser.shared.decodeFromURL(resource) - : await SVGAParser.shared.decodeFromAssets(resource); - movieEntity.autorelease = false; - cache[resource] = movieEntity; + final movieEntity = + _retainedResource == resource && _retainedMovieEntity != null + ? _retainedMovieEntity! + : await SCGiftVapSvgaManager().acquireSvgaEntity(resource); + final acquiredNewEntity = !identical(movieEntity, _retainedMovieEntity); + if (acquiredNewEntity) { + acquiredMovieEntity = movieEntity; + } + if (!mounted || widget.resource.trim() != resource) { + if (acquiredNewEntity) { + SCGiftVapSvgaManager().releaseSvgaEntity(movieEntity); + acquiredMovieEntity = null; + } + return; } if (widget.movieConfigurer != null) { movieEntity.dynamicItem.reset(); await widget.movieConfigurer!(movieEntity); } if (!mounted || widget.resource.trim() != resource) { + if (acquiredNewEntity) { + SCGiftVapSvgaManager().releaseSvgaEntity(movieEntity); + acquiredMovieEntity = null; + } return; } setState(() { + _releaseRetainedMovieEntity(except: movieEntity); + _retainedMovieEntity = movieEntity; + _retainedResource = resource; _loadedResource = resource; _controller.videoItem = movieEntity; }); + acquiredMovieEntity = null; _syncPlayback(restartIfActive: true); } catch (error) { + SCGiftVapSvgaManager().releaseSvgaEntity(acquiredMovieEntity); if (!mounted || widget.resource.trim() != resource) { return; } setState(() { + _controller.videoItem = null; + _releaseRetainedMovieEntity(); _hasError = true; _loadedResource = null; - _controller.videoItem = null; }); } } @@ -145,9 +164,20 @@ class _SCNetworkSvgaWidgetState extends State @override void dispose() { _controller.dispose(); + _releaseRetainedMovieEntity(); super.dispose(); } + void _releaseRetainedMovieEntity({MovieEntity? except}) { + final entity = _retainedMovieEntity; + if (entity == null || identical(entity, except)) { + return; + } + _retainedMovieEntity = null; + _retainedResource = null; + SCGiftVapSvgaManager().releaseSvgaEntity(entity); + } + @override Widget build(BuildContext context) { final videoItem = _controller.videoItem; diff --git a/lib/ui_kit/widgets/svga/sc_svga_asset_widget.dart b/lib/ui_kit/widgets/svga/sc_svga_asset_widget.dart index 84be820..2d19343 100644 --- a/lib/ui_kit/widgets/svga/sc_svga_asset_widget.dart +++ b/lib/ui_kit/widgets/svga/sc_svga_asset_widget.dart @@ -8,9 +8,11 @@ typedef SCSvgaMovieConfigurer = FutureOr Function(MovieEntity movieEntity); class SCSvgaAssetWidget extends StatefulWidget { + static const int _maxCacheEntries = 8; static final Map _cache = {}; static final Map> _loadingTasks = >{}; + static final Map _activeUseCounts = {}; const SCSvgaAssetWidget({ super.key, @@ -49,8 +51,26 @@ class SCSvgaAssetWidget extends StatefulWidget { return _obtainMovieEntity(assetPath); } + static void clearMemoryCache({bool includeActive = false}) { + final entries = _cache.entries.toList(growable: false); + for (final entry in entries) { + final entity = entry.value; + if (!includeActive && _isActive(entity)) { + continue; + } + _cache.remove(entry.key); + _disposeMovieEntity(entity); + } + } + + static Future _acquireMovieEntity(String assetPath) async { + final entity = await _obtainMovieEntity(assetPath); + _retainMovieEntity(entity); + return entity; + } + static Future _obtainMovieEntity(String assetPath) async { - final cached = _cache[assetPath]; + final cached = _touchCachedMovieEntity(assetPath); if (cached != null) { return cached; } @@ -71,7 +91,7 @@ class SCSvgaAssetWidget extends StatefulWidget { ); } entity.autorelease = false; - _cache[assetPath] = entity; + _cacheMovieEntity(assetPath, entity); return entity; }(); @@ -83,6 +103,77 @@ class SCSvgaAssetWidget extends StatefulWidget { } } + static MovieEntity? _touchCachedMovieEntity(String assetPath) { + final cached = _cache.remove(assetPath); + if (cached != null) { + _cache[assetPath] = cached; + } + return cached; + } + + static void _cacheMovieEntity(String assetPath, MovieEntity entity) { + final previous = _cache.remove(assetPath); + if (previous != null && !identical(previous, entity)) { + _disposeMovieEntityIfInactive(previous); + } + _cache[assetPath] = entity; + _trimCache(); + } + + static void _trimCache() { + while (_cache.length > _maxCacheEntries) { + final removableEntry = _cache.entries.firstWhere( + (entry) => !_isActive(entry.value), + orElse: () => _cache.entries.first, + ); + if (_isActive(removableEntry.value)) { + _cache.remove(removableEntry.key); + _cache[removableEntry.key] = removableEntry.value; + break; + } + _cache.remove(removableEntry.key); + _disposeMovieEntity(removableEntry.value); + } + } + + static void _retainMovieEntity(MovieEntity entity) { + _activeUseCounts[entity] = (_activeUseCounts[entity] ?? 0) + 1; + } + + static void _releaseMovieEntity(MovieEntity? entity) { + if (entity == null) { + return; + } + final count = _activeUseCounts[entity] ?? 0; + if (count <= 1) { + _activeUseCounts.remove(entity); + if (!_cache.containsValue(entity)) { + _disposeMovieEntity(entity); + } else { + _trimCache(); + } + return; + } + _activeUseCounts[entity] = count - 1; + } + + static bool _isActive(MovieEntity entity) => + (_activeUseCounts[entity] ?? 0) > 0; + + static void _disposeMovieEntityIfInactive(MovieEntity entity) { + if (_isActive(entity)) { + return; + } + _disposeMovieEntity(entity); + } + + static void _disposeMovieEntity(MovieEntity entity) { + try { + _activeUseCounts.remove(entity); + entity.dispose(); + } catch (_) {} + } + @override State createState() => _SCSvgaAssetWidgetState(); } @@ -91,6 +182,8 @@ class _SCSvgaAssetWidgetState extends State with SingleTickerProviderStateMixin { late final SVGAAnimationController _controller; String? _loadedAssetPath; + MovieEntity? _retainedMovieEntity; + String? _retainedAssetPath; bool _hasError = false; @override @@ -131,25 +224,44 @@ class _SCSvgaAssetWidgetState extends State setState(() { _hasError = false; }); + MovieEntity? acquiredMovieEntity; try { - final movieEntity = await SCSvgaAssetWidget._obtainMovieEntity(assetPath); + final movieEntity = + _retainedAssetPath == assetPath && _retainedMovieEntity != null + ? _retainedMovieEntity! + : await SCSvgaAssetWidget._acquireMovieEntity(assetPath); + final acquiredNewEntity = !identical(movieEntity, _retainedMovieEntity); + if (acquiredNewEntity) { + acquiredMovieEntity = movieEntity; + } if (widget.movieConfigurer != null) { movieEntity.dynamicItem.reset(); await widget.movieConfigurer!(movieEntity); } if (!mounted || widget.assetPath != assetPath) { + if (acquiredNewEntity) { + SCSvgaAssetWidget._releaseMovieEntity(movieEntity); + acquiredMovieEntity = null; + } return; } setState(() { + _releaseRetainedMovieEntity(except: movieEntity); + _retainedMovieEntity = movieEntity; + _retainedAssetPath = assetPath; _loadedAssetPath = assetPath; _controller.videoItem = movieEntity; }); + acquiredMovieEntity = null; _syncPlayback(restartIfActive: true); } catch (error) { + SCSvgaAssetWidget._releaseMovieEntity(acquiredMovieEntity); if (!mounted || widget.assetPath != assetPath) { return; } setState(() { + _controller.videoItem = null; + _releaseRetainedMovieEntity(); _hasError = true; }); } @@ -185,9 +297,20 @@ class _SCSvgaAssetWidgetState extends State void dispose() { _controller.removeStatusListener(_handleAnimationStatusChanged); _controller.dispose(); + _releaseRetainedMovieEntity(); super.dispose(); } + void _releaseRetainedMovieEntity({MovieEntity? except}) { + final entity = _retainedMovieEntity; + if (entity == null || identical(entity, except)) { + return; + } + _retainedMovieEntity = null; + _retainedAssetPath = null; + SCSvgaAssetWidget._releaseMovieEntity(entity); + } + @override Widget build(BuildContext context) { if (_hasError) {