diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index e43756d..54c531f 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -524,7 +524,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 100; + CURRENT_PROJECT_VERSION = 161; DEVELOPMENT_TEAM = S9YG87C297; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -533,7 +533,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 1.6.1; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -715,7 +715,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 100; + CURRENT_PROJECT_VERSION = 161; DEVELOPMENT_TEAM = S9YG87C297; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -724,7 +724,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 1.6.1; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -744,7 +744,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 100; + CURRENT_PROJECT_VERSION = 161; DEVELOPMENT_TEAM = S9YG87C297; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -753,7 +753,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 1.6.1; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/lib/modules/room_game/bridge/baishun_js_bridge.dart b/lib/modules/room_game/bridge/baishun_js_bridge.dart index ad15a0f..7eeb385 100644 --- a/lib/modules/room_game/bridge/baishun_js_bridge.dart +++ b/lib/modules/room_game/bridge/baishun_js_bridge.dart @@ -7,7 +7,6 @@ class BaishunBridgeActions { static const String gameLoaded = 'gameLoaded'; static const String gameRecharge = 'gameRecharge'; static const String destroy = 'destroy'; - static const String debugLog = 'debugLog'; } class BaishunBridgeMessage { @@ -104,45 +103,6 @@ class BaishunJsBridge { $channelName.postMessage(JSON.stringify({ action: action, payload: payload })); return payload; } - function clipText(value, limit) { - const text = (value == null ? '' : String(value)).trim(); - if (!text) { - return ''; - } - return text.length > limit ? text.slice(0, limit) + '...' : text; - } - function stringifyForLog(value) { - if (value == null) { - return ''; - } - if (typeof value === 'string') { - return clipText(value, 320); - } - try { - return clipText(JSON.stringify(value), 320); - } catch (_) { - return clipText(String(value), 320); - } - } - function postDebug(tag, payload) { - $channelName.postMessage(JSON.stringify({ - action: '${BaishunBridgeActions.debugLog}', - payload: { - tag: tag, - message: stringifyForLog(payload) - } - })); - } - function maskText(value) { - const text = (value == null ? '' : String(value)).trim(); - if (!text) { - return ''; - } - if (text.length <= 10) { - return text; - } - return text.slice(0, 6) + '***' + text.slice(-4); - } function ensureChannelAlias(name, handler) { if (!window[name] || typeof window[name].postMessage !== 'function') { window[name] = { postMessage: handler }; @@ -156,30 +116,21 @@ class BaishunJsBridge { } } window.NativeBridge.getConfigSync = function() { - postDebug('bridge.getConfigSync', { - hasConfig: !!window.__baishunLastConfig, - hasNativeBridgeConfig: !!(window.NativeBridge && window.NativeBridge.config) - }); return window.__baishunLastConfig || null; }; window.NativeBridge.getConfig = function(params) { - postDebug('bridge.getConfig.call', toPayload(params)); postAction('${BaishunBridgeActions.getConfig}', params); return window.__baishunLastConfig || null; }; window.NativeBridge.destroy = function(payload) { - postDebug('bridge.destroy.call', toPayload(payload)); postAction('${BaishunBridgeActions.destroy}', payload); }; window.NativeBridge.gameRecharge = function(payload) { - postDebug('bridge.gameRecharge.call', toPayload(payload)); postAction('${BaishunBridgeActions.gameRecharge}', payload); }; window.NativeBridge.gameLoaded = function(payload) { - postDebug('bridge.gameLoaded.call', toPayload(payload)); postAction('${BaishunBridgeActions.gameLoaded}', payload); }; - window.__baishunDebugLog = postDebug; ensureChannelAlias('getConfig', function(message) { window.NativeBridge.getConfig(message); }); @@ -204,163 +155,6 @@ class BaishunJsBridge { ensureWebkitHandler('gameLoaded', function(message) { window.NativeBridge.gameLoaded(message); }); - if (!window.__baishunNetworkDebugReady) { - window.__baishunNetworkDebugReady = true; - if (typeof document !== 'undefined' && typeof document.addEventListener === 'function' && !window.__baishunInputDebugReady) { - window.__baishunInputDebugReady = true; - window.__baishunInputDebugCount = 0; - ['pointerdown', 'touchstart', 'mousedown', 'click'].forEach(function(eventName) { - document.addEventListener(eventName, function(event) { - if ((window.__baishunInputDebugCount || 0) >= 24) { - return; - } - window.__baishunInputDebugCount = (window.__baishunInputDebugCount || 0) + 1; - const target = event && event.target; - const touch = event && event.touches && event.touches.length ? event.touches[0] : null; - postDebug('input.' + eventName, { - x: touch ? touch.clientX : (event && event.clientX), - y: touch ? touch.clientY : (event && event.clientY), - pointerType: event && event.pointerType, - targetTag: target && target.tagName, - targetId: target && target.id, - targetClass: target && target.className - }); - }, true); - }); - } - if (typeof window.addEventListener === 'function') { - window.addEventListener('error', function(event) { - const target = event && event.target; - if (target && target !== window) { - postDebug('resource.error', { - tag: target.tagName || '', - source: target.currentSrc || target.src || target.href || '', - outerHTML: target.outerHTML || '' - }); - return; - } - postDebug('window.error', { - message: event && event.message, - source: event && event.filename, - line: event && event.lineno, - column: event && event.colno, - stack: event && event.error && event.error.stack - }); - }, true); - window.addEventListener('unhandledrejection', function(event) { - postDebug('promise.reject', { - reason: event && event.reason - }); - }); - } - if (window.console && !window.__baishunConsoleDebugReady) { - window.__baishunConsoleDebugReady = true; - ['log', 'info', 'warn', 'error'].forEach(function(level) { - const original = typeof window.console[level] === 'function' - ? window.console[level].bind(window.console) - : null; - window.console[level] = function() { - try { - postDebug('console.' + level, Array.prototype.slice.call(arguments)); - } catch (_) {} - if (original) { - return original.apply(window.console, arguments); - } - return undefined; - }; - }); - } - if (typeof window.fetch === 'function') { - const originalFetch = window.fetch.bind(window); - window.fetch = function(input, init) { - const url = typeof input === 'string' ? input : (input && input.url) || ''; - const method = (init && init.method) || 'GET'; - postDebug('fetch.start', { method: method, url: url }); - return originalFetch(input, init) - .then(function(response) { - const cloned = response && typeof response.clone === 'function' ? response.clone() : null; - if (cloned && typeof cloned.text === 'function') { - cloned.text().then(function(text) { - if (!response.ok || /failed|error|exception|get user info/i.test(text)) { - postDebug('fetch.end', { - method: method, - url: url, - status: response.status, - body: text - }); - } else { - postDebug('fetch.end', { - method: method, - url: url, - status: response.status - }); - } - }).catch(function(error) { - postDebug('fetch.readError', { - method: method, - url: url, - error: error && error.message ? error.message : String(error) - }); - }); - } else { - postDebug('fetch.end', { - method: method, - url: url, - status: response && response.status - }); - } - return response; - }) - .catch(function(error) { - postDebug('fetch.error', { - method: method, - url: url, - error: error && error.message ? error.message : String(error) - }); - throw error; - }); - }; - } - if (window.XMLHttpRequest && window.XMLHttpRequest.prototype) { - const xhrOpen = window.XMLHttpRequest.prototype.open; - const xhrSend = window.XMLHttpRequest.prototype.send; - window.XMLHttpRequest.prototype.open = function(method, url) { - this.__baishunMethod = method || 'GET'; - this.__baishunUrl = url || ''; - return xhrOpen.apply(this, arguments); - }; - window.XMLHttpRequest.prototype.send = function(body) { - const method = this.__baishunMethod || 'GET'; - const url = this.__baishunUrl || ''; - postDebug('xhr.start', { method: method, url: url }); - this.addEventListener('loadend', function() { - const responseText = typeof this.responseText === 'string' ? this.responseText : ''; - if (this.status >= 400 || /failed|error|exception|get user info/i.test(responseText)) { - postDebug('xhr.end', { - method: method, - url: url, - status: this.status, - body: responseText - }); - } else { - postDebug('xhr.end', { - method: method, - url: url, - status: this.status - }); - } - }); - this.addEventListener('error', function() { - postDebug('xhr.error', { - method: method, - url: url, - status: this.status - }); - }); - return xhrSend.apply(this, arguments); - }; - } - } window.baishunChannel = window.baishunChannel || {}; window.baishunChannel.walletUpdate = function(payload) { window.__baishunLastWalletPayload = payload || {}; @@ -377,10 +171,6 @@ class BaishunJsBridge { if (typeof window.dispatchEvent === 'function' && typeof CustomEvent === 'function') { window.dispatchEvent(new CustomEvent('baishunBridgeReady')); } - postDebug('bridge.ready', { - href: window.location && window.location.href, - ua: navigator && navigator.userAgent - }); })(); '''; } @@ -395,16 +185,6 @@ class BaishunJsBridge { (function() { const config = $payload; const explicitCallbackPath = $encodedCallbackPath; - function maskText(value) { - const text = (value == null ? '' : String(value)).trim(); - if (!text) { - return ''; - } - if (text.length <= 10) { - return text; - } - return text.slice(0, 6) + '***' + text.slice(-4); - } function resolvePath(path) { if (!path || typeof path !== 'string') { return null; @@ -444,20 +224,6 @@ class BaishunJsBridge { if (explicitCallbackPath) { window.__baishunLastJsCallback = explicitCallbackPath; } - if (typeof window.__baishunDebugLog === 'function') { - window.__baishunDebugLog('config.deliver', { - appId: config.appId, - appChannel: config.appChannel, - userId: config.userId, - roomId: config.roomId, - gameMode: config.gameMode, - language: config.language, - gsp: config.gsp, - code: maskText(config.code), - codeLength: config.code ? String(config.code).length : 0, - callbackPath: callbackPath - }); - } if (typeof window.__baishunGetConfigCallback === 'function') { window.__baishunGetConfigCallback(config); window.__baishunGetConfigCallback = null; @@ -465,14 +231,6 @@ class BaishunJsBridge { if (window.__baishunLastJsCallback) { callbackInvoked = invokeCallbackPath(window.__baishunLastJsCallback) || callbackInvoked; } - if (typeof window.__baishunDebugLog === 'function') { - window.__baishunDebugLog('config.callback', { - callbackPath: callbackPath, - callbackInvoked: callbackInvoked, - hasLegacyCallback: typeof window.__baishunGetConfigCallback === 'function', - hasOnBaishunConfig: typeof window.onBaishunConfig === 'function' - }); - } if (typeof window.onBaishunConfig === 'function') { window.onBaishunConfig(config); } @@ -494,9 +252,6 @@ class BaishunJsBridge { return ''' (function() { const payload = $safePayload; - if (typeof window.__baishunDebugLog === 'function') { - window.__baishunDebugLog('wallet.update', payload); - } if (window.baishunChannel && typeof window.baishunChannel.walletUpdate === 'function') { window.baishunChannel.walletUpdate(payload); } diff --git a/lib/modules/room_game/bridge/leader_js_bridge.dart b/lib/modules/room_game/bridge/leader_js_bridge.dart index 23effc1..a40c69a 100644 --- a/lib/modules/room_game/bridge/leader_js_bridge.dart +++ b/lib/modules/room_game/bridge/leader_js_bridge.dart @@ -5,7 +5,6 @@ class LeaderBridgeActions { static const String pay = 'pay'; static const String closeGame = 'closeGame'; static const String loadComplete = 'loadComplete'; - static const String debugLog = 'debugLog'; } class LeaderBridgeMessage { @@ -88,21 +87,6 @@ class LeaderJsBridge { return {}; } - function stringifyForLog(value) { - if (value == null) { - return ''; - } - if (typeof value === 'string') { - return value.length > 320 ? value.slice(0, 320) + '...' : value; - } - try { - const text = JSON.stringify(value); - return text.length > 320 ? text.slice(0, 320) + '...' : text; - } catch (_) { - return String(value); - } - } - function postMessage(action, input) { const payload = toPayload(input); $channelName.postMessage(JSON.stringify({ @@ -112,13 +96,6 @@ class LeaderJsBridge { return payload; } - function postDebug(tag, payload) { - postMessage('${LeaderBridgeActions.debugLog}', { - tag: tag, - message: stringifyForLog(payload) - }); - } - function invokeCallback(callback, payload) { if (typeof callback === 'function') { callback(payload); @@ -199,16 +176,6 @@ class LeaderJsBridge { }; if (payloadChanged) { dispatchConfigReady(normalizedPayload); - postDebug('config.ready', { - provider: normalizedPayload.provider || '', - gameId: normalizedPayload.gameId || '', - roomId: launchConfig.roomId || '', - code: launchConfig.code - ? String(launchConfig.code).slice(0, 6) + '***' - : '', - codeLength: launchConfig.code ? String(launchConfig.code).length : 0, - entryUrl: entry.entryUrl || '' - }); } } @@ -234,12 +201,6 @@ class LeaderJsBridge { window.NativeBridge.pay = pay; window.NativeBridge.closeGame = closeGame; window.NativeBridge.loadComplete = loadComplete; - - window.__leaderBridgeDebug = postDebug; - postDebug('bridge.ready', { - href: window.location && window.location.href, - hasUpdateCoin: typeof window.updateCoin === 'function' - }); } applyLaunchPayload(launchPayload); @@ -262,13 +223,6 @@ class LeaderJsBridge { window.onUpdateCoin(); return; } - $channelName.postMessage(JSON.stringify({ - action: '${LeaderBridgeActions.debugLog}', - payload: { - tag: 'game.updateCoin.missing', - message: 'No updateCoin handler found on game page.' - } - })); })(); '''; } diff --git a/lib/modules/room_game/views/baishun_game_page.dart b/lib/modules/room_game/views/baishun_game_page.dart index 00c1a13..ba291ae 100644 --- a/lib/modules/room_game/views/baishun_game_page.dart +++ b/lib/modules/room_game/views/baishun_game_page.dart @@ -299,12 +299,6 @@ class _BaishunGamePageState extends State { Future _dispatchBridgeMessage( BaishunBridgeMessage bridgeMessage, ) async { - if (bridgeMessage.action == BaishunBridgeActions.debugLog) { - final tag = bridgeMessage.payload['tag']?.toString().trim(); - final message = bridgeMessage.payload['message']?.toString() ?? ''; - _log('h5_debug tag=${tag ?? 'unknown'} message=${_clip(message, 800)}'); - return; - } final callbackPath = _extractCallbackPath(bridgeMessage.payload); _log( 'bridge_action action=${bridgeMessage.action} ' diff --git a/lib/modules/room_game/views/leader_game_page.dart b/lib/modules/room_game/views/leader_game_page.dart index 4fc1275..fa1f372 100644 --- a/lib/modules/room_game/views/leader_game_page.dart +++ b/lib/modules/room_game/views/leader_game_page.dart @@ -245,13 +245,6 @@ class _LeaderGamePageState extends State { } Future _dispatchBridgeMessage(LeaderBridgeMessage bridgeMessage) async { - if (bridgeMessage.action == LeaderBridgeActions.debugLog) { - final tag = bridgeMessage.payload['tag']?.toString().trim(); - final message = bridgeMessage.payload['message']?.toString() ?? ''; - _log('h5_debug tag=${tag ?? 'unknown'} message=${_clip(message, 800)}'); - return; - } - _didReceiveBridgeMessage = true; _stopBridgeBootstrap(reason: 'bridge_message_${bridgeMessage.action}'); _log( 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..9395813 100644 --- a/lib/modules/user/profile/profile_gift_wall_detail_page.dart +++ b/lib/modules/user/profile/profile_gift_wall_detail_page.dart @@ -568,6 +568,7 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay> late final SVGAAnimationController _controller; bool _loaded = false; bool _finished = false; + String? _retainedResource; @override void initState() { @@ -586,7 +587,10 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay> Future _loadAndPlay() async { try { final movieEntity = await _loadMovieEntity(widget.sourceUrl); - if (!mounted || _finished) return; + if (!mounted || _finished) { + _releaseRetainedResource(); + return; + } setState(() { _loaded = true; _controller.videoItem = movieEntity; @@ -600,9 +604,12 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay> } Future _loadMovieEntity(String sourceUrl) async { - final cache = SCGiftVapSvgaManager().videoItemCache; - final cached = cache[sourceUrl]; - if (cached != null) return cached; + final manager = SCGiftVapSvgaManager(); + final cached = manager.getCachedSvgaEntity(sourceUrl, retain: true); + if (cached != null) { + _replaceRetainedResource(sourceUrl); + return cached; + } final pathType = SCPathUtils.getPathType(sourceUrl); late final MovieEntity movieEntity; @@ -617,10 +624,35 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay> throw Exception("Unsupported SVGA source: $sourceUrl"); } movieEntity.autorelease = false; - cache[sourceUrl] = movieEntity; + if (!mounted || _finished) { + movieEntity.dispose(); + return movieEntity; + } + manager.cacheSvgaEntity(sourceUrl, movieEntity, retain: true); + _replaceRetainedResource(sourceUrl); return movieEntity; } + void _replaceRetainedResource(String resource) { + final previous = _retainedResource; + if (previous == resource) { + return; + } + if (previous != null) { + SCGiftVapSvgaManager().releaseCachedSvgaEntity(previous); + } + _retainedResource = resource; + } + + void _releaseRetainedResource() { + final resource = _retainedResource; + if (resource == null) { + return; + } + _retainedResource = null; + SCGiftVapSvgaManager().releaseCachedSvgaEntity(resource); + } + void _finish() { if (_finished) return; _finished = true; @@ -629,6 +661,7 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay> @override void dispose() { + _releaseRetainedResource(); _controller.removeStatusListener(_handleStatusChanged); _controller.dispose(); super.dispose(); diff --git a/lib/shared/tools/sc_entrance_vap_svga_manager.dart b/lib/shared/tools/sc_entrance_vap_svga_manager.dart index afd9c23..df88cbf 100644 --- a/lib/shared/tools/sc_entrance_vap_svga_manager.dart +++ b/lib/shared/tools/sc_entrance_vap_svga_manager.dart @@ -91,11 +91,13 @@ // if (_isDisposed) return; // if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") { // MovieEntity entity; -// if (SCGiftVapSvgaManager().videoItemCache.containsKey(task.path)) { -// entity = SCGiftVapSvgaManager().videoItemCache[task.path]!; +// final manager = SCGiftVapSvgaManager(); +// final cached = manager.getCachedSvgaEntity(task.path); +// if (cached != null) { +// entity = cached; // } else { // entity = await SVGAParser.shared.decodeFromAssets(task.path); -// SCGiftVapSvgaManager().videoItemCache[task.path] = entity; +// manager.cacheSvgaEntity(task.path, entity); // } // _roomGiftsvgaController?.videoItem = entity; // _roomGiftsvgaController?.reset(); @@ -116,14 +118,16 @@ // if (_isDisposed) return; // if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") { // MovieEntity? entity; -// if (SCGiftVapSvgaManager().videoItemCache.containsKey(task.path)) { -// entity = SCGiftVapSvgaManager().videoItemCache[task.path]!; +// final manager = SCGiftVapSvgaManager(); +// final cached = manager.getCachedSvgaEntity(task.path); +// if (cached != null) { +// entity = cached; // entity.autorelease = false; // } else { // try { // entity = await SVGAParser.shared.decodeFromURL(task.path); // entity.autorelease = false; -// SCGiftVapSvgaManager().videoItemCache[task.path] = entity; +// manager.cacheSvgaEntity(task.path, entity); // } catch (e) { // _isPlaying = false; // } diff --git a/lib/shared/tools/sc_gift_vap_svga_manager.dart b/lib/shared/tools/sc_gift_vap_svga_manager.dart index 5a2e902..a98160a 100644 --- a/lib/shared/tools/sc_gift_vap_svga_manager.dart +++ b/lib/shared/tools/sc_gift_vap_svga_manager.dart @@ -26,12 +26,13 @@ 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 = {}; static SCGiftVapSvgaManager? _inst; static const int _maxPreloadConcurrency = 1; static const int _maxPreloadQueueLength = 12; static const int _maxSvgaCacheEntries = 8; static const int _maxPlayablePathCacheEntries = 24; + final Map _svgaRetainCounts = {}; SCGiftVapSvgaManager._internal(); @@ -144,7 +145,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); @@ -302,20 +303,80 @@ class SCGiftVapSvgaManager { } } + MovieEntity? getCachedSvgaEntity(String path, {bool retain = false}) { + final normalizedPath = path.trim(); + if (normalizedPath.isEmpty) { + return null; + } + final entity = _touchCachedSvgaEntity(normalizedPath); + if (entity != null && retain) { + _retainCachedSvgaEntity(normalizedPath); + } + return entity; + } + + void cacheSvgaEntity(String path, MovieEntity entity, {bool retain = false}) { + final normalizedPath = path.trim(); + if (normalizedPath.isEmpty) { + _disposeSvgaEntityIfUnused(entity); + return; + } + _cacheSvgaEntity(normalizedPath, entity); + if (retain) { + _retainCachedSvgaEntity(normalizedPath); + } + } + + MovieEntity? removeCachedSvgaEntity(String path, {bool dispose = false}) { + final normalizedPath = path.trim(); + if (normalizedPath.isEmpty) { + return null; + } + final entity = _videoItemCache.remove(normalizedPath); + if (dispose && + entity != null && + !_isCachedSvgaEntityInUse(normalizedPath, entity)) { + _disposeSvgaEntityIfUnused(entity); + } + return entity; + } + + void releaseCachedSvgaEntity(String path) { + final normalizedPath = path.trim(); + if (normalizedPath.isEmpty) { + return; + } + final count = _svgaRetainCounts[normalizedPath] ?? 0; + if (count <= 1) { + _svgaRetainCounts.remove(normalizedPath); + _trimSvgaCache(); + return; + } + _svgaRetainCounts[normalizedPath] = count - 1; + } + + int get cachedSvgaEntityCount => _videoItemCache.length; + + void _retainCachedSvgaEntity(String path) { + _svgaRetainCounts[path] = (_svgaRetainCounts[path] ?? 0) + 1; + } + 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); - if (previous != null && !identical(previous, entity)) { + final previous = _videoItemCache.remove(path); + if (previous != null && + !identical(previous, entity) && + !_isCachedSvgaEntityInUse(path, previous)) { _disposeSvgaEntityIfUnused(previous); } - videoItemCache[path] = entity; + _videoItemCache[path] = entity; _trimSvgaCache(); } @@ -334,17 +395,19 @@ class SCGiftVapSvgaManager { } void _trimSvgaCache() { - while (videoItemCache.length > _maxSvgaCacheEntries) { - final removableEntry = videoItemCache.entries.firstWhere( - (entry) => !_isSvgaEntityInUse(entry.value), - orElse: () => videoItemCache.entries.first, - ); - if (_isSvgaEntityInUse(removableEntry.value)) { - videoItemCache.remove(removableEntry.key); - videoItemCache[removableEntry.key] = removableEntry.value; + while (_videoItemCache.length > _maxSvgaCacheEntries) { + MapEntry? removableEntry; + for (final entry in _videoItemCache.entries) { + if (!_isCachedSvgaEntityInUse(entry.key, entry.value)) { + removableEntry = entry; + break; + } + } + if (removableEntry == null) { break; } - videoItemCache.remove(removableEntry.key); + _svgaRetainCounts.remove(removableEntry.key); + _videoItemCache.remove(removableEntry.key); _disposeSvgaEntity(removableEntry.value); } } @@ -833,13 +896,15 @@ 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)) { + if (_isCachedSvgaEntityInUse(entry.key, entity) || + (!includeCurrent && _isSvgaEntityInUse(entity))) { continue; } - videoItemCache.remove(entry.key); + _svgaRetainCounts.remove(entry.key); + _videoItemCache.remove(entry.key); _disposeSvgaEntity(entity); } _playablePathCache.clear(); @@ -849,6 +914,10 @@ class SCGiftVapSvgaManager { return identical(_rsc?.videoItem, entity); } + bool _isCachedSvgaEntityInUse(String path, MovieEntity entity) { + return _isSvgaEntityInUse(entity) || (_svgaRetainCounts[path] ?? 0) > 0; + } + void _disposeSvgaEntityIfUnused(MovieEntity entity) { if (_isSvgaEntityInUse(entity)) { return; diff --git a/lib/ui_kit/widgets/headdress/headdress_widget.dart b/lib/ui_kit/widgets/headdress/headdress_widget.dart index e5f0868..5f31dfe 100644 --- a/lib/ui_kit/widgets/headdress/headdress_widget.dart +++ b/lib/ui_kit/widgets/headdress/headdress_widget.dart @@ -51,6 +51,7 @@ class _SVGAHeadwearWidgetState extends State bool _isLoading = true; bool _isNetworkResource = false; bool _hasError = false; + String? _retainedResource; @override void initState() { @@ -84,8 +85,15 @@ class _SVGAHeadwearWidgetState extends State try { // 检查缓存 MovieEntity? videoItem; + final manager = SCGiftVapSvgaManager(); + final shouldRetain = _retainedResource != widget.resource; + var retained = false; if (widget.useCache) { - videoItem = SCGiftVapSvgaManager().videoItemCache[widget.resource]; + videoItem = manager.getCachedSvgaEntity( + widget.resource, + retain: shouldRetain, + ); + retained = videoItem != null && shouldRetain; } // 如果没有缓存,则进行解析 @@ -97,11 +105,23 @@ class _SVGAHeadwearWidgetState extends State videoItem.autorelease = false; // 存入缓存 if (widget.useCache) { - SCGiftVapSvgaManager().videoItemCache[widget.resource] = videoItem; + if (!mounted) { + videoItem.dispose(); + return; + } + manager.cacheSvgaEntity( + widget.resource, + videoItem, + retain: shouldRetain, + ); + retained = shouldRetain; } } if (mounted) { + if (retained) { + _replaceRetainedResource(widget.resource); + } setState(() { _animationController?.videoItem = videoItem; _isLoading = false; @@ -164,13 +184,34 @@ class _SVGAHeadwearWidgetState extends State void reload() { if (widget.useCache) { // 清除缓存项 - SCGiftVapSvgaManager().videoItemCache.remove(widget.resource); + SCGiftVapSvgaManager().removeCachedSvgaEntity(widget.resource); } _loadAnimation(); } + void _replaceRetainedResource(String resource) { + final previous = _retainedResource; + if (previous == resource) { + return; + } + if (previous != null) { + SCGiftVapSvgaManager().releaseCachedSvgaEntity(previous); + } + _retainedResource = resource; + } + + void _releaseRetainedResource() { + final resource = _retainedResource; + if (resource == null) { + return; + } + _retainedResource = null; + SCGiftVapSvgaManager().releaseCachedSvgaEntity(resource); + } + @override void dispose() { + _releaseRetainedResource(); stop(); _animationController?.dispose(); super.dispose(); 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..2cea95c 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,7 @@ class _RoomEntranceWidgetState extends State bool _isLoading = false; bool _hasError = false; String? _currentResource; + String? _retainedResource; // 用于存储SVGAImage组件的key,以便刷新占位符 GlobalKey _svgaKey = GlobalKey(); @@ -354,9 +355,13 @@ class _RoomEntranceWidgetState extends State try { final isNetworkResource = resource.startsWith('http'); MovieEntity? videoItem; + final manager = SCGiftVapSvgaManager(); + final shouldRetain = _retainedResource != resource; + var retained = false; if (widget.useCache) { - videoItem = SCGiftVapSvgaManager().videoItemCache[resource]; + videoItem = manager.getCachedSvgaEntity(resource, retain: shouldRetain); + retained = videoItem != null && shouldRetain; } if (videoItem == null) { @@ -367,11 +372,19 @@ class _RoomEntranceWidgetState extends State videoItem.autorelease = false; if (widget.useCache) { - SCGiftVapSvgaManager().videoItemCache[resource] = videoItem; + if (!mounted) { + videoItem.dispose(); + return; + } + manager.cacheSvgaEntity(resource, videoItem, retain: shouldRetain); + retained = shouldRetain; } } if (mounted) { + if (retained) { + _replaceRetainedResource(resource); + } // 应用动态数据(文本、图片替换) await _applyDynamicData(videoItem, dynamicData); @@ -553,7 +566,7 @@ class _RoomEntranceWidgetState extends State void reload() { if (_currentResource != null) { if (widget.useCache) { - SCGiftVapSvgaManager().videoItemCache.remove(_currentResource); + SCGiftVapSvgaManager().removeCachedSvgaEntity(_currentResource!); } _loadAnimation( @@ -566,6 +579,7 @@ class _RoomEntranceWidgetState extends State @override void dispose() { + _releaseRetainedResource(); stop(); _animationController?.dispose(); // 从队列管理器中移除当前播放器 @@ -573,6 +587,26 @@ class _RoomEntranceWidgetState extends State super.dispose(); } + void _replaceRetainedResource(String resource) { + final previous = _retainedResource; + if (previous == resource) { + return; + } + if (previous != null) { + SCGiftVapSvgaManager().releaseCachedSvgaEntity(previous); + } + _retainedResource = resource; + } + + void _releaseRetainedResource() { + final resource = _retainedResource; + if (resource == null) { + return; + } + _retainedResource = null; + SCGiftVapSvgaManager().releaseCachedSvgaEntity(resource); + } + @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 d7a23e1..02e6d45 100644 --- a/lib/ui_kit/widgets/svga/sc_network_svga_widget.dart +++ b/lib/ui_kit/widgets/svga/sc_network_svga_widget.dart @@ -57,6 +57,7 @@ class _SCNetworkSvgaWidgetState extends State late final SVGAAnimationController _controller; String? _loadedResource; + String? _retainedResource; bool _hasError = false; @override @@ -82,6 +83,7 @@ class _SCNetworkSvgaWidgetState extends State Future _loadResource() async { final resource = widget.resource.trim(); if (resource.isEmpty || !SCNetworkSvgaWidget.isSvga(resource)) { + _releaseRetainedResource(); setState(() { _hasError = true; _loadedResource = null; @@ -96,23 +98,39 @@ class _SCNetworkSvgaWidgetState extends State try { _debugSvgaOnce(_loggedLoadingResources, 'loading', resource); - final cache = SCGiftVapSvgaManager().videoItemCache; - var movieEntity = cache[resource]; + final manager = SCGiftVapSvgaManager(); + final shouldRetain = _retainedResource != resource; + var movieEntity = manager.getCachedSvgaEntity( + resource, + retain: shouldRetain, + ); + var retained = movieEntity != null && shouldRetain; if (movieEntity == null) { movieEntity = resource.startsWith("http") ? await SVGAParser.shared.decodeFromURL(resource) : await SVGAParser.shared.decodeFromAssets(resource); movieEntity.autorelease = false; - cache[resource] = movieEntity; + if (!mounted || widget.resource.trim() != resource) { + movieEntity.dispose(); + return; + } + manager.cacheSvgaEntity(resource, movieEntity, retain: shouldRetain); + retained = shouldRetain; } if (widget.movieConfigurer != null) { movieEntity.dynamicItem.reset(); await widget.movieConfigurer!(movieEntity); } if (!mounted || widget.resource.trim() != resource) { + if (retained) { + manager.releaseCachedSvgaEntity(resource); + } return; } + if (retained) { + _replaceRetainedResource(resource); + } setState(() { _loadedResource = resource; _controller.videoItem = movieEntity; @@ -157,8 +175,29 @@ class _SCNetworkSvgaWidgetState extends State } } + void _replaceRetainedResource(String resource) { + final previous = _retainedResource; + if (previous == resource) { + return; + } + if (previous != null) { + SCGiftVapSvgaManager().releaseCachedSvgaEntity(previous); + } + _retainedResource = resource; + } + + void _releaseRetainedResource() { + final resource = _retainedResource; + if (resource == null) { + return; + } + _retainedResource = null; + SCGiftVapSvgaManager().releaseCachedSvgaEntity(resource); + } + @override void dispose() { + _releaseRetainedResource(); _controller.dispose(); super.dispose(); } diff --git a/pubspec.yaml b/pubspec.yaml index 655a755..457ee0e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.6.0+160 +version: 1.6.1+161 environment: