svg资源释放

This commit is contained in:
roxy 2026-05-11 14:08:43 +08:00
parent 829914c58c
commit 92bbbcb0eb
7 changed files with 356 additions and 98 deletions

View File

@ -46,6 +46,7 @@ import 'services/auth/user_profile_manager.dart';
import 'ui_kit/theme/socialchat_theme.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/anim/room_gift_seat_flight_overlay.dart';
import 'ui_kit/widgets/room/effect/vapp_svga_layer_widget.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; bool _isCrashlyticsReady = false;
@ -108,6 +109,7 @@ void _configureImageCache() {
void _releaseVisualMemoryCaches({bool clearLiveImages = false}) { void _releaseVisualMemoryCaches({bool clearLiveImages = false}) {
SCGiftVapSvgaManager().clearMemoryCache(); SCGiftVapSvgaManager().clearMemoryCache();
SCSvgaAssetWidget.clearMemoryCache();
final imageCache = PaintingBinding.instance.imageCache; final imageCache = PaintingBinding.instance.imageCache;
imageCache.clear(); imageCache.clear();
if (clearLiveImages) { if (clearLiveImages) {

View File

@ -1,5 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_svga/flutter_svga.dart'; import 'package:flutter_svga/flutter_svga.dart';
@ -566,6 +564,7 @@ class _GiftWallSvgaEffectOverlay extends StatefulWidget {
class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay> class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late final SVGAAnimationController _controller; late final SVGAAnimationController _controller;
MovieEntity? _retainedMovieEntity;
bool _loaded = false; bool _loaded = false;
bool _finished = false; bool _finished = false;
@ -585,9 +584,15 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay>
Future<void> _loadAndPlay() async { Future<void> _loadAndPlay() async {
try { try {
final movieEntity = await _loadMovieEntity(widget.sourceUrl); final movieEntity = await SCGiftVapSvgaManager().acquireSvgaEntity(
if (!mounted || _finished) return; widget.sourceUrl,
);
if (!mounted || _finished) {
SCGiftVapSvgaManager().releaseSvgaEntity(movieEntity);
return;
}
setState(() { setState(() {
_retainedMovieEntity = movieEntity;
_loaded = true; _loaded = true;
_controller.videoItem = movieEntity; _controller.videoItem = movieEntity;
}); });
@ -599,28 +604,6 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay>
} }
} }
Future<MovieEntity> _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() { void _finish() {
if (_finished) return; if (_finished) return;
_finished = true; _finished = true;
@ -631,6 +614,8 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay>
void dispose() { void dispose() {
_controller.removeStatusListener(_handleStatusChanged); _controller.removeStatusListener(_handleStatusChanged);
_controller.dispose(); _controller.dispose();
SCGiftVapSvgaManager().releaseSvgaEntity(_retainedMovieEntity);
_retainedMovieEntity = null;
super.dispose(); super.dispose();
} }

View File

@ -23,7 +23,8 @@ class SCGiftVapSvgaManager {
static const int _maxConsecutiveEntryTasksBeforeGift = 2; static const int _maxConsecutiveEntryTasksBeforeGift = 2;
static const Duration _taskWatchdogTimeout = Duration(seconds: 20); static const Duration _taskWatchdogTimeout = Duration(seconds: 20);
static const Duration _entryTaskTtl = Duration(seconds: 6); static const Duration _entryTaskTtl = Duration(seconds: 6);
Map<String, MovieEntity> videoItemCache = {}; final Map<String, MovieEntity> _videoItemCache = <String, MovieEntity>{};
final Map<MovieEntity, int> _svgaEntityUseCounts = <MovieEntity, int>{};
static SCGiftVapSvgaManager? _inst; static SCGiftVapSvgaManager? _inst;
static const int _maxPreloadConcurrency = 1; static const int _maxPreloadConcurrency = 1;
static const int _maxPreloadQueueLength = 12; static const int _maxPreloadQueueLength = 12;
@ -79,6 +80,44 @@ class SCGiftVapSvgaManager {
return SCPathUtils.getFileExtension(path).toLowerCase() == ".svga"; return SCPathUtils.getFileExtension(path).toLowerCase() == ".svga";
} }
Future<MovieEntity> 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({ static int resolveEffectPriority({
int priority = 0, int priority = 0,
int type = giftEffectType, int type = giftEffectType,
@ -116,7 +155,7 @@ class SCGiftVapSvgaManager {
bool _isPreloadedOrLoading(String path) { bool _isPreloadedOrLoading(String path) {
if (_needsSvgaController(path)) { if (_needsSvgaController(path)) {
return videoItemCache.containsKey(path) || return _videoItemCache.containsKey(path) ||
_svgaLoadTasks.containsKey(path); _svgaLoadTasks.containsKey(path);
} }
final pathType = SCPathUtils.getPathType(path); final pathType = SCPathUtils.getPathType(path);
@ -256,19 +295,19 @@ class SCGiftVapSvgaManager {
} }
MovieEntity? _touchCachedSvgaEntity(String path) { MovieEntity? _touchCachedSvgaEntity(String path) {
final cached = videoItemCache.remove(path); final cached = _videoItemCache.remove(path);
if (cached != null) { if (cached != null) {
videoItemCache[path] = cached; _videoItemCache[path] = cached;
} }
return cached; return cached;
} }
void _cacheSvgaEntity(String path, MovieEntity entity) { void _cacheSvgaEntity(String path, MovieEntity entity) {
final previous = videoItemCache.remove(path); final previous = _videoItemCache.remove(path);
if (previous != null && !identical(previous, entity)) { if (previous != null && !identical(previous, entity)) {
_disposeSvgaEntityIfUnused(previous); _disposeSvgaEntityIfUnused(previous);
} }
videoItemCache[path] = entity; _videoItemCache[path] = entity;
_trimSvgaCache(); _trimSvgaCache();
} }
@ -287,17 +326,17 @@ class SCGiftVapSvgaManager {
} }
void _trimSvgaCache() { void _trimSvgaCache() {
while (videoItemCache.length > _maxSvgaCacheEntries) { while (_videoItemCache.length > _maxSvgaCacheEntries) {
final removableEntry = videoItemCache.entries.firstWhere( final removableEntry = _videoItemCache.entries.firstWhere(
(entry) => !_isSvgaEntityInUse(entry.value), (entry) => !_isSvgaEntityInUse(entry.value),
orElse: () => videoItemCache.entries.first, orElse: () => _videoItemCache.entries.first,
); );
if (_isSvgaEntityInUse(removableEntry.value)) { if (_isSvgaEntityInUse(removableEntry.value)) {
videoItemCache.remove(removableEntry.key); _videoItemCache.remove(removableEntry.key);
videoItemCache[removableEntry.key] = removableEntry.value; _videoItemCache[removableEntry.key] = removableEntry.value;
break; break;
} }
videoItemCache.remove(removableEntry.key); _videoItemCache.remove(removableEntry.key);
_disposeSvgaEntity(removableEntry.value); _disposeSvgaEntity(removableEntry.value);
} }
} }
@ -752,20 +791,21 @@ class SCGiftVapSvgaManager {
_preloadQueue.clear(); _preloadQueue.clear();
_queuedPreloadPaths.clear(); _queuedPreloadPaths.clear();
_activePreloadCount = 0; _activePreloadCount = 0;
final entries = videoItemCache.entries.toList(growable: false); final entries = _videoItemCache.entries.toList(growable: false);
for (final entry in entries) { for (final entry in entries) {
final entity = entry.value; final entity = entry.value;
if (!includeCurrent && _isSvgaEntityInUse(entity)) { if (!includeCurrent && _isSvgaEntityInUse(entity)) {
continue; continue;
} }
videoItemCache.remove(entry.key); _videoItemCache.remove(entry.key);
_disposeSvgaEntity(entity); _disposeSvgaEntity(entity);
} }
_playablePathCache.clear(); _playablePathCache.clear();
} }
bool _isSvgaEntityInUse(MovieEntity entity) { bool _isSvgaEntityInUse(MovieEntity entity) {
return identical(_rsc?.videoItem, entity); return identical(_rsc?.videoItem, entity) ||
(_svgaEntityUseCounts[entity] ?? 0) > 0;
} }
void _disposeSvgaEntityIfUnused(MovieEntity entity) { void _disposeSvgaEntityIfUnused(MovieEntity entity) {
@ -777,6 +817,7 @@ class SCGiftVapSvgaManager {
void _disposeSvgaEntity(MovieEntity entity) { void _disposeSvgaEntity(MovieEntity entity) {
try { try {
_svgaEntityUseCounts.remove(entity);
entity.dispose(); entity.dispose();
} catch (_) {} } catch (_) {}
} }

View File

@ -48,6 +48,8 @@ class SVGAHeadwearWidget extends StatefulWidget {
class _SVGAHeadwearWidgetState extends State<SVGAHeadwearWidget> class _SVGAHeadwearWidgetState extends State<SVGAHeadwearWidget>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
SVGAAnimationController? _animationController; SVGAAnimationController? _animationController;
MovieEntity? _retainedMovieEntity;
String? _retainedResource;
bool _isLoading = true; bool _isLoading = true;
bool _isNetworkResource = false; bool _isNetworkResource = false;
bool _hasError = false; bool _hasError = false;
@ -82,41 +84,53 @@ class _SVGAHeadwearWidgetState extends State<SVGAHeadwearWidget>
}); });
try { try {
//
MovieEntity? videoItem;
if (widget.useCache) { 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;
}
// setState(() {
if (videoItem == null) { _releaseRetainedMovieEntity(except: videoItem);
videoItem = _retainedMovieEntity = videoItem;
_retainedResource = widget.resource;
_animationController?.videoItem = videoItem;
_isLoading = false;
});
} else {
final videoItem =
_isNetworkResource _isNetworkResource
? await SVGAParser.shared.decodeFromURL(widget.resource) ? await SVGAParser.shared.decodeFromURL(widget.resource)
: await SVGAParser.shared.decodeFromAssets(widget.resource); : await SVGAParser.shared.decodeFromAssets(widget.resource);
videoItem.autorelease =false; if (!mounted) {
// videoItem.dispose();
if (widget.useCache) { return;
SCGiftVapSvgaManager().videoItemCache[widget.resource] = videoItem;
} }
}
if (mounted) {
setState(() { setState(() {
_animationController?.videoItem = videoItem; _animationController?.videoItem = videoItem;
_isLoading = false; _isLoading = false;
}); });
}
// //
if (widget.loops == 0) { if (widget.loops == 0) {
_animationController?.repeat(); _animationController?.repeat();
} else { } else {
_animationController?.repeat(count: widget.loops); _animationController?.repeat(count: widget.loops);
} }
if (widget.onFinishLoading != null) { if (widget.onFinishLoading != null) {
widget.onFinishLoading!(); widget.onFinishLoading!();
}
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
@ -157,14 +171,16 @@ class _SVGAHeadwearWidgetState extends State<SVGAHeadwearWidget>
_animationController?.stop(); _animationController?.stop();
if (widget.clearsAfterStop) { if (widget.clearsAfterStop) {
_animationController?.videoItem = null; _animationController?.videoItem = null;
_releaseRetainedMovieEntity();
} }
} }
// //
void reload() { void reload() {
if (widget.useCache) { if (widget.useCache) {
// _animationController?.videoItem = null;
SCGiftVapSvgaManager().videoItemCache.remove(widget.resource); _releaseRetainedMovieEntity();
SCGiftVapSvgaManager().evictSvgaEntity(widget.resource);
} }
_loadAnimation(); _loadAnimation();
} }
@ -173,9 +189,20 @@ class _SVGAHeadwearWidgetState extends State<SVGAHeadwearWidget>
void dispose() { void dispose() {
stop(); stop();
_animationController?.dispose(); _animationController?.dispose();
_releaseRetainedMovieEntity();
super.dispose(); super.dispose();
} }
void _releaseRetainedMovieEntity({MovieEntity? except}) {
final entity = _retainedMovieEntity;
if (entity == null || identical(entity, except)) {
return;
}
_retainedMovieEntity = null;
_retainedResource = null;
SCGiftVapSvgaManager().releaseSvgaEntity(entity);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isLoading && !widget.autoPlay) { if (_isLoading && !widget.autoPlay) {

View File

@ -270,6 +270,8 @@ class _RoomEntranceWidgetState extends State<RoomEntranceWidget>
bool _isLoading = false; bool _isLoading = false;
bool _hasError = false; bool _hasError = false;
String? _currentResource; String? _currentResource;
MovieEntity? _retainedMovieEntity;
String? _retainedResource;
// SVGAImage组件的key便 // SVGAImage组件的key便
GlobalKey _svgaKey = GlobalKey(); GlobalKey _svgaKey = GlobalKey();
@ -351,45 +353,79 @@ class _RoomEntranceWidgetState extends State<RoomEntranceWidget>
_hasError = false; _hasError = false;
}); });
MovieEntity? acquiredMovieEntity;
try { try {
final isNetworkResource = resource.startsWith('http');
MovieEntity? videoItem;
if (widget.useCache) { 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 isNetworkResource
? await SVGAParser.shared.decodeFromURL(resource) ? await SVGAParser.shared.decodeFromURL(resource)
: await SVGAParser.shared.decodeFromAssets(resource); : await SVGAParser.shared.decodeFromAssets(resource);
videoItem.autorelease = false; if (!mounted) {
videoItem.dispose();
if (widget.useCache) { return;
SCGiftVapSvgaManager().videoItemCache[resource] = videoItem;
} }
}
if (mounted) {
// //
await _applyDynamicData(videoItem, dynamicData); await _applyDynamicData(videoItem, dynamicData);
if (!mounted) {
videoItem.dispose();
return;
}
setState(() { setState(() {
_animationController?.videoItem = videoItem; _animationController?.videoItem = videoItem;
_isLoading = false; _isLoading = false;
}); });
}
// //
_startAnimation(); _startAnimation();
if (widget.onFinishLoading != null) { if (widget.onFinishLoading != null) {
widget.onFinishLoading!(); widget.onFinishLoading!();
}
} }
} catch (e) { } catch (e) {
SCGiftVapSvgaManager().releaseSvgaEntity(acquiredMovieEntity);
if (mounted) { if (mounted) {
setState(() { setState(() {
_animationController?.videoItem = null;
_releaseRetainedMovieEntity();
_isLoading = false; _isLoading = false;
_hasError = true; _hasError = true;
}); });
@ -543,6 +579,7 @@ class _RoomEntranceWidgetState extends State<RoomEntranceWidget>
_animationController?.stop(); _animationController?.stop();
if (widget.clearsAfterStop) { if (widget.clearsAfterStop) {
_animationController?.videoItem = null; _animationController?.videoItem = null;
_releaseRetainedMovieEntity();
setState(() { setState(() {
_currentResource = null; _currentResource = null;
}); });
@ -553,7 +590,9 @@ class _RoomEntranceWidgetState extends State<RoomEntranceWidget>
void reload() { void reload() {
if (_currentResource != null) { if (_currentResource != null) {
if (widget.useCache) { if (widget.useCache) {
SCGiftVapSvgaManager().videoItemCache.remove(_currentResource); _animationController?.videoItem = null;
_releaseRetainedMovieEntity();
SCGiftVapSvgaManager().evictSvgaEntity(_currentResource!);
} }
_loadAnimation( _loadAnimation(
@ -568,11 +607,22 @@ class _RoomEntranceWidgetState extends State<RoomEntranceWidget>
void dispose() { void dispose() {
stop(); stop();
_animationController?.dispose(); _animationController?.dispose();
_releaseRetainedMovieEntity();
// //
_queueManager.clearCurrentPlayer(); _queueManager.clearCurrentPlayer();
super.dispose(); super.dispose();
} }
void _releaseRetainedMovieEntity({MovieEntity? except}) {
final entity = _retainedMovieEntity;
if (entity == null || identical(entity, except)) {
return;
}
_retainedMovieEntity = null;
_retainedResource = null;
SCGiftVapSvgaManager().releaseSvgaEntity(entity);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// //

View File

@ -52,6 +52,8 @@ class _SCNetworkSvgaWidgetState extends State<SCNetworkSvgaWidget>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late final SVGAAnimationController _controller; late final SVGAAnimationController _controller;
String? _loadedResource; String? _loadedResource;
MovieEntity? _retainedMovieEntity;
String? _retainedResource;
bool _hasError = false; bool _hasError = false;
@override @override
@ -78,9 +80,10 @@ class _SCNetworkSvgaWidgetState extends State<SCNetworkSvgaWidget>
final resource = widget.resource.trim(); final resource = widget.resource.trim();
if (resource.isEmpty || !SCNetworkSvgaWidget.isSvga(resource)) { if (resource.isEmpty || !SCNetworkSvgaWidget.isSvga(resource)) {
setState(() { setState(() {
_controller.videoItem = null;
_releaseRetainedMovieEntity();
_hasError = true; _hasError = true;
_loadedResource = null; _loadedResource = null;
_controller.videoItem = null;
}); });
return; return;
} }
@ -89,37 +92,53 @@ class _SCNetworkSvgaWidgetState extends State<SCNetworkSvgaWidget>
_hasError = false; _hasError = false;
}); });
MovieEntity? acquiredMovieEntity;
try { try {
final cache = SCGiftVapSvgaManager().videoItemCache; final movieEntity =
var movieEntity = cache[resource]; _retainedResource == resource && _retainedMovieEntity != null
if (movieEntity == null) { ? _retainedMovieEntity!
movieEntity = : await SCGiftVapSvgaManager().acquireSvgaEntity(resource);
resource.startsWith("http") final acquiredNewEntity = !identical(movieEntity, _retainedMovieEntity);
? await SVGAParser.shared.decodeFromURL(resource) if (acquiredNewEntity) {
: await SVGAParser.shared.decodeFromAssets(resource); acquiredMovieEntity = movieEntity;
movieEntity.autorelease = false; }
cache[resource] = movieEntity; if (!mounted || widget.resource.trim() != resource) {
if (acquiredNewEntity) {
SCGiftVapSvgaManager().releaseSvgaEntity(movieEntity);
acquiredMovieEntity = null;
}
return;
} }
if (widget.movieConfigurer != null) { if (widget.movieConfigurer != null) {
movieEntity.dynamicItem.reset(); movieEntity.dynamicItem.reset();
await widget.movieConfigurer!(movieEntity); await widget.movieConfigurer!(movieEntity);
} }
if (!mounted || widget.resource.trim() != resource) { if (!mounted || widget.resource.trim() != resource) {
if (acquiredNewEntity) {
SCGiftVapSvgaManager().releaseSvgaEntity(movieEntity);
acquiredMovieEntity = null;
}
return; return;
} }
setState(() { setState(() {
_releaseRetainedMovieEntity(except: movieEntity);
_retainedMovieEntity = movieEntity;
_retainedResource = resource;
_loadedResource = resource; _loadedResource = resource;
_controller.videoItem = movieEntity; _controller.videoItem = movieEntity;
}); });
acquiredMovieEntity = null;
_syncPlayback(restartIfActive: true); _syncPlayback(restartIfActive: true);
} catch (error) { } catch (error) {
SCGiftVapSvgaManager().releaseSvgaEntity(acquiredMovieEntity);
if (!mounted || widget.resource.trim() != resource) { if (!mounted || widget.resource.trim() != resource) {
return; return;
} }
setState(() { setState(() {
_controller.videoItem = null;
_releaseRetainedMovieEntity();
_hasError = true; _hasError = true;
_loadedResource = null; _loadedResource = null;
_controller.videoItem = null;
}); });
} }
} }
@ -145,9 +164,20 @@ class _SCNetworkSvgaWidgetState extends State<SCNetworkSvgaWidget>
@override @override
void dispose() { void dispose() {
_controller.dispose(); _controller.dispose();
_releaseRetainedMovieEntity();
super.dispose(); super.dispose();
} }
void _releaseRetainedMovieEntity({MovieEntity? except}) {
final entity = _retainedMovieEntity;
if (entity == null || identical(entity, except)) {
return;
}
_retainedMovieEntity = null;
_retainedResource = null;
SCGiftVapSvgaManager().releaseSvgaEntity(entity);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final videoItem = _controller.videoItem; final videoItem = _controller.videoItem;

View File

@ -8,9 +8,11 @@ typedef SCSvgaMovieConfigurer =
FutureOr<void> Function(MovieEntity movieEntity); FutureOr<void> Function(MovieEntity movieEntity);
class SCSvgaAssetWidget extends StatefulWidget { class SCSvgaAssetWidget extends StatefulWidget {
static const int _maxCacheEntries = 8;
static final Map<String, MovieEntity> _cache = <String, MovieEntity>{}; static final Map<String, MovieEntity> _cache = <String, MovieEntity>{};
static final Map<String, Future<MovieEntity>> _loadingTasks = static final Map<String, Future<MovieEntity>> _loadingTasks =
<String, Future<MovieEntity>>{}; <String, Future<MovieEntity>>{};
static final Map<MovieEntity, int> _activeUseCounts = <MovieEntity, int>{};
const SCSvgaAssetWidget({ const SCSvgaAssetWidget({
super.key, super.key,
@ -49,8 +51,26 @@ class SCSvgaAssetWidget extends StatefulWidget {
return _obtainMovieEntity(assetPath); 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<MovieEntity> _acquireMovieEntity(String assetPath) async {
final entity = await _obtainMovieEntity(assetPath);
_retainMovieEntity(entity);
return entity;
}
static Future<MovieEntity> _obtainMovieEntity(String assetPath) async { static Future<MovieEntity> _obtainMovieEntity(String assetPath) async {
final cached = _cache[assetPath]; final cached = _touchCachedMovieEntity(assetPath);
if (cached != null) { if (cached != null) {
return cached; return cached;
} }
@ -71,7 +91,7 @@ class SCSvgaAssetWidget extends StatefulWidget {
); );
} }
entity.autorelease = false; entity.autorelease = false;
_cache[assetPath] = entity; _cacheMovieEntity(assetPath, entity);
return 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 @override
State<SCSvgaAssetWidget> createState() => _SCSvgaAssetWidgetState(); State<SCSvgaAssetWidget> createState() => _SCSvgaAssetWidgetState();
} }
@ -91,6 +182,8 @@ class _SCSvgaAssetWidgetState extends State<SCSvgaAssetWidget>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late final SVGAAnimationController _controller; late final SVGAAnimationController _controller;
String? _loadedAssetPath; String? _loadedAssetPath;
MovieEntity? _retainedMovieEntity;
String? _retainedAssetPath;
bool _hasError = false; bool _hasError = false;
@override @override
@ -131,25 +224,44 @@ class _SCSvgaAssetWidgetState extends State<SCSvgaAssetWidget>
setState(() { setState(() {
_hasError = false; _hasError = false;
}); });
MovieEntity? acquiredMovieEntity;
try { 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) { if (widget.movieConfigurer != null) {
movieEntity.dynamicItem.reset(); movieEntity.dynamicItem.reset();
await widget.movieConfigurer!(movieEntity); await widget.movieConfigurer!(movieEntity);
} }
if (!mounted || widget.assetPath != assetPath) { if (!mounted || widget.assetPath != assetPath) {
if (acquiredNewEntity) {
SCSvgaAssetWidget._releaseMovieEntity(movieEntity);
acquiredMovieEntity = null;
}
return; return;
} }
setState(() { setState(() {
_releaseRetainedMovieEntity(except: movieEntity);
_retainedMovieEntity = movieEntity;
_retainedAssetPath = assetPath;
_loadedAssetPath = assetPath; _loadedAssetPath = assetPath;
_controller.videoItem = movieEntity; _controller.videoItem = movieEntity;
}); });
acquiredMovieEntity = null;
_syncPlayback(restartIfActive: true); _syncPlayback(restartIfActive: true);
} catch (error) { } catch (error) {
SCSvgaAssetWidget._releaseMovieEntity(acquiredMovieEntity);
if (!mounted || widget.assetPath != assetPath) { if (!mounted || widget.assetPath != assetPath) {
return; return;
} }
setState(() { setState(() {
_controller.videoItem = null;
_releaseRetainedMovieEntity();
_hasError = true; _hasError = true;
}); });
} }
@ -185,9 +297,20 @@ class _SCSvgaAssetWidgetState extends State<SCSvgaAssetWidget>
void dispose() { void dispose() {
_controller.removeStatusListener(_handleAnimationStatusChanged); _controller.removeStatusListener(_handleAnimationStatusChanged);
_controller.dispose(); _controller.dispose();
_releaseRetainedMovieEntity();
super.dispose(); super.dispose();
} }
void _releaseRetainedMovieEntity({MovieEntity? except}) {
final entity = _retainedMovieEntity;
if (entity == null || identical(entity, except)) {
return;
}
_retainedMovieEntity = null;
_retainedAssetPath = null;
SCSvgaAssetWidget._releaseMovieEntity(entity);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_hasError) { if (_hasError) {