diff --git a/lib/main.dart b/lib/main.dart index 7aa93e0..fd4fb7c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,6 +26,7 @@ import 'app/routes/sc_routes.dart'; import 'app/routes/sc_lk_application.dart'; import 'shared/tools/sc_deep_link_handler.dart'; import 'shared/tools/sc_deviceId_utils.dart'; +import 'shared/tools/sc_gift_vap_svga_manager.dart'; import 'shared/data_sources/sources/local/user_manager.dart'; import 'modules/splash/splash_page.dart'; import 'services/general/sc_app_general_manager.dart'; @@ -105,6 +106,15 @@ void _configureImageCache() { imageCache.maximumSize = SCGlobalConfig.recommendedImageCacheEntries; } +void _releaseVisualMemoryCaches({bool clearLiveImages = false}) { + SCGiftVapSvgaManager().clearMemoryCache(); + final imageCache = PaintingBinding.instance.imageCache; + imageCache.clear(); + if (clearLiveImages) { + imageCache.clearLiveImages(); + } +} + void _installErrorHandlers() { FlutterError.onError = (details) { FlutterError.presentError(details); @@ -303,6 +313,14 @@ class _YumiApplicationState extends State @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); + if (state == AppLifecycleState.paused || + state == AppLifecycleState.inactive || + state == AppLifecycleState.hidden) { + SCGiftVapSvgaManager().stopPlayback(); + _releaseVisualMemoryCaches( + clearLiveImages: state == AppLifecycleState.paused, + ); + } if (state == AppLifecycleState.detached) { unawaited( context.read().releaseRtcEngineForAppTermination(), @@ -310,6 +328,13 @@ class _YumiApplicationState extends State } } + @override + void didHaveMemoryPressure() { + super.didHaveMemoryPressure(); + SCGiftVapSvgaManager().stopPlayback(); + _releaseVisualMemoryCaches(clearLiveImages: true); + } + Future _initLink() async { // 初始化,并传递一个回调函数用于处理链接 await _deepLinkHandler.initDeepLinks( diff --git a/lib/modules/auth/account/sc_login_with_account_page.dart b/lib/modules/auth/account/sc_login_with_account_page.dart index 742fec2..da07961 100644 --- a/lib/modules/auth/account/sc_login_with_account_page.dart +++ b/lib/modules/auth/account/sc_login_with_account_page.dart @@ -1,3 +1,4 @@ +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -364,7 +365,7 @@ class SCLoginWithAccountPageState extends State } ///登录 - _login() async { + Future _login() async { String account = accountController.text; String pass = passController.text; if (account.isEmpty || pass.isEmpty) { @@ -394,15 +395,47 @@ class SCLoginWithAccountPageState extends State } AccountStorage().setCurrentUser(user); SCLoadingManager.hide(); - DataPersistence.setString("Login_Account", account); - DataPersistence.setString("Login_Pwd", pass); + try { + await Future.wait([ + DataPersistence.setString("Login_Account", account), + DataPersistence.setString("Login_Pwd", pass), + ]); + } catch (_) {} if (mounted) { SCNavigatorUtils.push(context, SCRoutes.home, clearStack: true); } } catch (e) { SCLoadingManager.hide(); - // 可以添加错误处理逻辑 - rethrow; + final message = _loginErrorMessage(e); + if (message.isNotEmpty) { + SCTts.show(message); + } } } + + String _loginErrorMessage(Object error) { + if (error is DioException) { + final responseData = error.response?.data; + if (responseData is Map) { + final serverMessage = responseData["errorMsg"]?.toString().trim(); + if (serverMessage != null && serverMessage.isNotEmpty) { + return serverMessage; + } + } + final dioError = error.error?.toString().trim(); + if (dioError != null && dioError.isNotEmpty) { + return dioError.replaceFirst("Exception: ", ""); + } + final message = error.message?.trim(); + if (message != null && message.isNotEmpty) { + return message; + } + } + + final message = error.toString().replaceFirst("Exception: ", "").trim(); + if (message.isNotEmpty) { + return message; + } + return "Login failed. Please try again."; + } } diff --git a/lib/modules/room/chat/all/all_chat_page.dart b/lib/modules/room/chat/all/all_chat_page.dart index 4fb00ce..6f3ce3e 100644 --- a/lib/modules/room/chat/all/all_chat_page.dart +++ b/lib/modules/room/chat/all/all_chat_page.dart @@ -21,6 +21,7 @@ class _AllChatPageState extends State { bool _isDisposed = false; final List _msgList = []; late RtmProvider provider; + late final RoomNewMsgListener _newMsgListener = _onNewMsg; @override void initState() { @@ -28,7 +29,7 @@ class _AllChatPageState extends State { _controller.addListener(_scrollListener); provider = Provider.of(context, listen: false); final msgList = provider.roomAllMsgList; - provider.msgAllListener = _onNewMsg; + provider.msgAllListener = _newMsgListener; _msgList.addAll(msgList); // 使用安全的方式初始化滚动位置 _initScrollPosition(); @@ -81,7 +82,9 @@ class _AllChatPageState extends State { @override void dispose() { _isDisposed = true; - provider.msgAllListener = null; + if (identical(provider.msgAllListener, _newMsgListener)) { + provider.msgAllListener = null; + } _controller.removeListener(_scrollListener); _controller.dispose(); super.dispose(); diff --git a/lib/modules/room/chat/chat/chat_page.dart b/lib/modules/room/chat/chat/chat_page.dart index cc0fecb..dcf2179 100644 --- a/lib/modules/room/chat/chat/chat_page.dart +++ b/lib/modules/room/chat/chat/chat_page.dart @@ -21,6 +21,7 @@ class _ChatPageState extends State { bool _isDisposed = false; List _msgList = []; late RtmProvider provider; + late final RoomNewMsgListener _newMsgListener = _onNewMsg; @override void initState() { @@ -28,7 +29,7 @@ class _ChatPageState extends State { _controller.addListener(_scrollListener); provider = Provider.of(context, listen: false); var msgList = provider.roomChatMsgList; - provider.msgChatListener = _onNewMsg; + provider.msgChatListener = _newMsgListener; _msgList.addAll(msgList ??= []); // 使用安全的方式初始化滚动位置 _initScrollPosition(); @@ -80,7 +81,9 @@ class _ChatPageState extends State { @override void dispose() { _isDisposed = true; - provider.msgChatListener = null; + if (identical(provider.msgChatListener, _newMsgListener)) { + provider.msgChatListener = null; + } _controller.removeListener(_scrollListener); _controller.dispose(); super.dispose(); diff --git a/lib/modules/room/chat/gift/gift_chat_page.dart b/lib/modules/room/chat/gift/gift_chat_page.dart index 6ad635c..63cb977 100644 --- a/lib/modules/room/chat/gift/gift_chat_page.dart +++ b/lib/modules/room/chat/gift/gift_chat_page.dart @@ -21,6 +21,7 @@ class _GiftChatPageState extends State { bool _isDisposed = false; final List _msgList = []; late RtmProvider provider; + late final RoomNewMsgListener _newMsgListener = _onNewMsg; @override void initState() { @@ -28,7 +29,7 @@ class _GiftChatPageState extends State { _controller.addListener(_scrollListener); provider = Provider.of(context, listen: false); final msgList = provider.roomGiftMsgList; - provider.msgGiftListener = _onNewMsg; + provider.msgGiftListener = _newMsgListener; _msgList.addAll(msgList); // 使用安全的方式初始化滚动位置 _initScrollPosition(); @@ -81,7 +82,9 @@ class _GiftChatPageState extends State { @override void dispose() { _isDisposed = true; - provider.msgGiftListener = null; + if (identical(provider.msgGiftListener, _newMsgListener)) { + provider.msgGiftListener = null; + } _controller.removeListener(_scrollListener); _controller.dispose(); super.dispose(); diff --git a/lib/shared/data_sources/sources/local/data_persistence.dart b/lib/shared/data_sources/sources/local/data_persistence.dart index 7905294..c99ad10 100644 --- a/lib/shared/data_sources/sources/local/data_persistence.dart +++ b/lib/shared/data_sources/sources/local/data_persistence.dart @@ -18,6 +18,8 @@ class DataPersistence { static bool _isInitialized = false; static Completer? _initializationCompleter; + static bool get isInitialized => _isInitialized && _prefs != null; + // 初始化 SharedPreferences static Future initialize() async { if (_isInitialized) return; @@ -48,9 +50,17 @@ class DataPersistence { } } + static Future _ensureInitialized() async { + if (isInitialized) { + return; + } + await initialize(); + _checkInitialized(); + } + // 存储字符串 static Future setString(String key, String value) async { - _checkInitialized(); + await _ensureInitialized(); return await _prefs!.setString(key, value); } @@ -62,7 +72,7 @@ class DataPersistence { // 存储整数 static Future setInt(String key, int value) async { - _checkInitialized(); + await _ensureInitialized(); return await _prefs!.setInt(key, value); } @@ -74,7 +84,7 @@ class DataPersistence { // 存储布尔值 static Future setBool(String key, bool value) async { - _checkInitialized(); + await _ensureInitialized(); return await _prefs!.setBool(key, value); } @@ -86,7 +96,7 @@ class DataPersistence { // 存储双精度浮点数 static Future setDouble(String key, double value) async { - _checkInitialized(); + await _ensureInitialized(); return await _prefs!.setDouble(key, value); } diff --git a/lib/shared/data_sources/sources/local/user_manager.dart b/lib/shared/data_sources/sources/local/user_manager.dart index dc08151..a81bb01 100644 --- a/lib/shared/data_sources/sources/local/user_manager.dart +++ b/lib/shared/data_sources/sources/local/user_manager.dart @@ -38,7 +38,10 @@ class AccountStorage { } var userJson = DataPersistence.getCurrentUser(); if (userJson.isNotEmpty) { - _currentUser = SocialChatLoginRes.fromJson(jsonDecode(userJson)); + _currentUser = _decodeCurrentUser(userJson); + if (_currentUser == null) { + unawaited(_clearPersistedSession(clearDurable: false)); + } } return _currentUser; } @@ -46,16 +49,10 @@ class AccountStorage { ///同步优化数据 void setCurrentUser(SocialChatLoginRes user) { _currentUser = user; - setToken(_currentUser!.token ?? ""); + token = _currentUser!.token ?? ""; String userJson = jsonEncode(_currentUser?.toJson()); if (userJson.isNotEmpty) { - DataPersistence.setCurrentUser(userJson); - unawaited( - DurableAuthStorage.saveSession( - token: _currentUser!.token ?? "", - currentUserJson: userJson, - ), - ); + unawaited(_persistSession(token: token, currentUserJson: userJson)); } } @@ -130,54 +127,110 @@ class AccountStorage { Future restoreDurableSessionIfNeeded() async { final localToken = DataPersistence.getToken().trim(); final localUserJson = DataPersistence.getCurrentUser().trim(); - if (localToken.isNotEmpty && localUserJson.isNotEmpty) { + final localUser = _decodeCurrentUser(localUserJson); + if (localToken.isNotEmpty && localUser != null) { + token = localToken; + _currentUser = localUser; return; } final session = await DurableAuthStorage.readSession(); if (session == null) { + if (localToken.isNotEmpty || localUserJson.isNotEmpty) { + unawaited(_clearPersistedSession(clearDurable: false)); + } return; } token = session.token; - await DataPersistence.setToken(session.token); - await DataPersistence.setCurrentUser(session.currentUserJson); - try { - _currentUser = SocialChatLoginRes.fromJson( - jsonDecode(session.currentUserJson), - ); - } catch (_) { - _currentUser = null; + _currentUser = _decodeCurrentUser(session.currentUserJson); + if (_currentUser == null) { + token = ""; + await _clearPersistedSession(); + return; } + await _persistSession( + token: session.token, + currentUserJson: session.currentUserJson, + ); } void setToken(String tk) { token = tk; - DataPersistence.setToken(token); + unawaited(_persistToken(token)); } void _cleanUser() { token = ""; _currentUser = null; - DataPersistence.setToken(""); - DataPersistence.setCurrentUser(""); + unawaited(_clearPersistedSession()); unawaited(DataPersistence.clearPendingChannelAuth()); - unawaited(DurableAuthStorage.clearSession()); } ///退出登录 void logout(BuildContext context) { _cleanUser(); SCHeartbeatUtils.cancelTimer(); - Provider.of( - context, - listen: false, - ).exitCurrentVoiceRoomSession(true); - Provider.of(context, listen: false).logout(); + try { + Provider.of( + context, + listen: false, + ).exitCurrentVoiceRoomSession(true); + } catch (_) {} + try { + Provider.of(context, listen: false).logout(); + } catch (_) {} SCNavigatorUtils.pushLoginIfNeeded(context, clearStack: true); - SCRoomUtils.closeAllDialogs(); + try { + SCRoomUtils.closeAllDialogs(); + } catch (_) {} SCMessageUtils.redPacketFutureCache.clear(); SCGlobalConfig.resetVisualEffectSwitchesToRecommendedDefaults(); - OverlayManager().dispose(); + try { + OverlayManager().dispose(); + } catch (_) {} + } + + SocialChatLoginRes? _decodeCurrentUser(String userJson) { + if (userJson.trim().isEmpty) { + return null; + } + try { + final decoded = jsonDecode(userJson); + if (decoded is Map) { + return SocialChatLoginRes.fromJson(Map.from(decoded)); + } + } catch (_) {} + return null; + } + + Future _persistToken(String value) async { + try { + await DataPersistence.setToken(value); + } catch (_) {} + } + + Future _persistSession({ + required String token, + required String currentUserJson, + }) async { + try { + await DataPersistence.setToken(token); + await DataPersistence.setCurrentUser(currentUserJson); + } catch (_) {} + await DurableAuthStorage.saveSession( + token: token, + currentUserJson: currentUserJson, + ); + } + + Future _clearPersistedSession({bool clearDurable = true}) async { + try { + await DataPersistence.setToken(""); + await DataPersistence.setCurrentUser(""); + } catch (_) {} + if (clearDurable) { + await DurableAuthStorage.clearSession(); + } } } diff --git a/lib/shared/tools/sc_gift_vap_svga_manager.dart b/lib/shared/tools/sc_gift_vap_svga_manager.dart index 812c00f..67f02c5 100644 --- a/lib/shared/tools/sc_gift_vap_svga_manager.dart +++ b/lib/shared/tools/sc_gift_vap_svga_manager.dart @@ -170,8 +170,9 @@ class SCGiftVapSvgaManager { 'start preload path=$path active=$_activePreloadCount ' 'queueRemaining=${_preloadQueue.length}', ); - _warmupPath(path).whenComplete(() { - _activePreloadCount--; + _warmupPath(path).catchError((_) {}).whenComplete(() { + _activePreloadCount = + _activePreloadCount > 0 ? _activePreloadCount - 1 : 0; _log( 'finish preload path=$path active=$_activePreloadCount ' 'queueRemaining=${_preloadQueue.length}', @@ -263,9 +264,12 @@ class SCGiftVapSvgaManager { } void _cacheSvgaEntity(String path, MovieEntity entity) { - videoItemCache.remove(path); + final previous = videoItemCache.remove(path); + if (previous != null && !identical(previous, entity)) { + _disposeSvgaEntityIfUnused(previous); + } videoItemCache[path] = entity; - _trimResolvedCache(videoItemCache, _maxSvgaCacheEntries); + _trimSvgaCache(); } String? _touchCachedPlayablePath(String path) { @@ -282,6 +286,22 @@ class SCGiftVapSvgaManager { _trimResolvedCache(_playablePathCache, _maxPlayablePathCacheEntries); } + 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; + break; + } + videoItemCache.remove(removableEntry.key); + _disposeSvgaEntity(removableEntry.value); + } + } + void _trimResolvedCache(Map cache, int maxEntries) { while (cache.length > maxEntries) { final oldestKey = cache.keys.first; @@ -554,7 +574,7 @@ class SCGiftVapSvgaManager { } else { _finishCurrentTask(); } - } catch (e, s) { + } catch (_) { _finishCurrentTask(); } } @@ -728,6 +748,39 @@ class SCGiftVapSvgaManager { SCRoomEffectScheduler().clearHighCostTasks(reason: 'stop_playback'); } + void clearMemoryCache({bool includeCurrent = false}) { + _preloadQueue.clear(); + _queuedPreloadPaths.clear(); + _activePreloadCount = 0; + 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); + _disposeSvgaEntity(entity); + } + _playablePathCache.clear(); + } + + bool _isSvgaEntityInUse(MovieEntity entity) { + return identical(_rsc?.videoItem, entity); + } + + void _disposeSvgaEntityIfUnused(MovieEntity entity) { + if (_isSvgaEntityInUse(entity)) { + return; + } + _disposeSvgaEntity(entity); + } + + void _disposeSvgaEntity(MovieEntity entity) { + try { + entity.dispose(); + } catch (_) {} + } + // 释放资源 void dispose() { _log('dispose queue=$_queueSummary currentPath=${_currentTask?.path}'); @@ -735,7 +788,7 @@ class SCGiftVapSvgaManager { stopPlayback(); _svgaLoadTasks.clear(); _playablePathTasks.clear(); - videoItemCache.clear(); + clearMemoryCache(includeCurrent: true); _playablePathCache.clear(); _rgc?.dispose(); _rgc = null; diff --git a/lib/shared/tools/sc_heartbeat_utils.dart b/lib/shared/tools/sc_heartbeat_utils.dart index b48bb0a..ba652d1 100644 --- a/lib/shared/tools/sc_heartbeat_utils.dart +++ b/lib/shared/tools/sc_heartbeat_utils.dart @@ -55,14 +55,17 @@ class SCHeartbeatUtils { } ///上麦主播发送心跳 - static void scheduleAnchorHeartbeat(String roomId) async { + static void scheduleAnchorHeartbeat(String roomId) { cancelAnchorTimer(); - scheduleHeartbeat(_c, true, roomId: roomId); - SCAccountRepository().anchorHeartbeat(roomId).whenComplete(() { - _a ??= Timer.periodic(Duration(seconds: 60), (timer) { - SCAccountRepository().anchorHeartbeat(roomId).catchError((_) => {}); - }); - }); + unawaited(scheduleHeartbeat(_c, true, roomId: roomId)); + SCAccountRepository() + .anchorHeartbeat(roomId) + .then((_) { + _a ??= Timer.periodic(Duration(seconds: 60), (timer) { + SCAccountRepository().anchorHeartbeat(roomId).catchError((_) => {}); + }); + }) + .catchError((_) {}); } static void cancelAnchorTimer() { diff --git a/test/account_storage_session_test.dart b/test/account_storage_session_test.dart new file mode 100644 index 0000000..db59b33 --- /dev/null +++ b/test/account_storage_session_test.dart @@ -0,0 +1,26 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + tearDown(() { + DataPersistence.reset(); + }); + + test('corrupted cached user session is ignored and cleared', () async { + SharedPreferences.setMockInitialValues({ + 'token': 'stale-token', + 'currentUser': '{bad json', + }); + await DataPersistence.initialize(); + + expect(AccountStorage().getCurrentUser(), isNull); + await Future.delayed(Duration.zero); + + expect(DataPersistence.getToken(), isEmpty); + expect(DataPersistence.getCurrentUser(), isEmpty); + }); +}