内存泄露闪退以及切房公屏显示问题

This commit is contained in:
roxy 2026-05-11 11:31:48 +08:00
parent fec672dfb6
commit 2a1b63de24
10 changed files with 269 additions and 57 deletions

View File

@ -26,6 +26,7 @@ import 'app/routes/sc_routes.dart';
import 'app/routes/sc_lk_application.dart'; import 'app/routes/sc_lk_application.dart';
import 'shared/tools/sc_deep_link_handler.dart'; import 'shared/tools/sc_deep_link_handler.dart';
import 'shared/tools/sc_deviceId_utils.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 'shared/data_sources/sources/local/user_manager.dart';
import 'modules/splash/splash_page.dart'; import 'modules/splash/splash_page.dart';
import 'services/general/sc_app_general_manager.dart'; import 'services/general/sc_app_general_manager.dart';
@ -105,6 +106,15 @@ void _configureImageCache() {
imageCache.maximumSize = SCGlobalConfig.recommendedImageCacheEntries; imageCache.maximumSize = SCGlobalConfig.recommendedImageCacheEntries;
} }
void _releaseVisualMemoryCaches({bool clearLiveImages = false}) {
SCGiftVapSvgaManager().clearMemoryCache();
final imageCache = PaintingBinding.instance.imageCache;
imageCache.clear();
if (clearLiveImages) {
imageCache.clearLiveImages();
}
}
void _installErrorHandlers() { void _installErrorHandlers() {
FlutterError.onError = (details) { FlutterError.onError = (details) {
FlutterError.presentError(details); FlutterError.presentError(details);
@ -303,6 +313,14 @@ class _YumiApplicationState extends State<YumiApplication>
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(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) { if (state == AppLifecycleState.detached) {
unawaited( unawaited(
context.read<RtcProvider>().releaseRtcEngineForAppTermination(), context.read<RtcProvider>().releaseRtcEngineForAppTermination(),
@ -310,6 +328,13 @@ class _YumiApplicationState extends State<YumiApplication>
} }
} }
@override
void didHaveMemoryPressure() {
super.didHaveMemoryPressure();
SCGiftVapSvgaManager().stopPlayback();
_releaseVisualMemoryCaches(clearLiveImages: true);
}
Future<void> _initLink() async { Future<void> _initLink() async {
// //
await _deepLinkHandler.initDeepLinks( await _deepLinkHandler.initDeepLinks(

View File

@ -1,3 +1,4 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
@ -364,7 +365,7 @@ class SCLoginWithAccountPageState extends State<SCLoginWithAccountPage>
} }
/// ///
_login() async { Future<void> _login() async {
String account = accountController.text; String account = accountController.text;
String pass = passController.text; String pass = passController.text;
if (account.isEmpty || pass.isEmpty) { if (account.isEmpty || pass.isEmpty) {
@ -394,15 +395,47 @@ class SCLoginWithAccountPageState extends State<SCLoginWithAccountPage>
} }
AccountStorage().setCurrentUser(user); AccountStorage().setCurrentUser(user);
SCLoadingManager.hide(); SCLoadingManager.hide();
DataPersistence.setString("Login_Account", account); try {
DataPersistence.setString("Login_Pwd", pass); await Future.wait([
DataPersistence.setString("Login_Account", account),
DataPersistence.setString("Login_Pwd", pass),
]);
} catch (_) {}
if (mounted) { if (mounted) {
SCNavigatorUtils.push(context, SCRoutes.home, clearStack: true); SCNavigatorUtils.push(context, SCRoutes.home, clearStack: true);
} }
} catch (e) { } catch (e) {
SCLoadingManager.hide(); SCLoadingManager.hide();
// final message = _loginErrorMessage(e);
rethrow; 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.";
}
} }

View File

@ -21,6 +21,7 @@ class _AllChatPageState extends State<AllChatPage> {
bool _isDisposed = false; bool _isDisposed = false;
final List<Msg> _msgList = []; final List<Msg> _msgList = [];
late RtmProvider provider; late RtmProvider provider;
late final RoomNewMsgListener _newMsgListener = _onNewMsg;
@override @override
void initState() { void initState() {
@ -28,7 +29,7 @@ class _AllChatPageState extends State<AllChatPage> {
_controller.addListener(_scrollListener); _controller.addListener(_scrollListener);
provider = Provider.of<RtmProvider>(context, listen: false); provider = Provider.of<RtmProvider>(context, listen: false);
final msgList = provider.roomAllMsgList; final msgList = provider.roomAllMsgList;
provider.msgAllListener = _onNewMsg; provider.msgAllListener = _newMsgListener;
_msgList.addAll(msgList); _msgList.addAll(msgList);
// 使 // 使
_initScrollPosition(); _initScrollPosition();
@ -81,7 +82,9 @@ class _AllChatPageState extends State<AllChatPage> {
@override @override
void dispose() { void dispose() {
_isDisposed = true; _isDisposed = true;
provider.msgAllListener = null; if (identical(provider.msgAllListener, _newMsgListener)) {
provider.msgAllListener = null;
}
_controller.removeListener(_scrollListener); _controller.removeListener(_scrollListener);
_controller.dispose(); _controller.dispose();
super.dispose(); super.dispose();

View File

@ -21,6 +21,7 @@ class _ChatPageState extends State<ChatPage> {
bool _isDisposed = false; bool _isDisposed = false;
List<Msg> _msgList = []; List<Msg> _msgList = [];
late RtmProvider provider; late RtmProvider provider;
late final RoomNewMsgListener _newMsgListener = _onNewMsg;
@override @override
void initState() { void initState() {
@ -28,7 +29,7 @@ class _ChatPageState extends State<ChatPage> {
_controller.addListener(_scrollListener); _controller.addListener(_scrollListener);
provider = Provider.of<RtmProvider>(context, listen: false); provider = Provider.of<RtmProvider>(context, listen: false);
var msgList = provider.roomChatMsgList; var msgList = provider.roomChatMsgList;
provider.msgChatListener = _onNewMsg; provider.msgChatListener = _newMsgListener;
_msgList.addAll(msgList ??= []); _msgList.addAll(msgList ??= []);
// 使 // 使
_initScrollPosition(); _initScrollPosition();
@ -80,7 +81,9 @@ class _ChatPageState extends State<ChatPage> {
@override @override
void dispose() { void dispose() {
_isDisposed = true; _isDisposed = true;
provider.msgChatListener = null; if (identical(provider.msgChatListener, _newMsgListener)) {
provider.msgChatListener = null;
}
_controller.removeListener(_scrollListener); _controller.removeListener(_scrollListener);
_controller.dispose(); _controller.dispose();
super.dispose(); super.dispose();

View File

@ -21,6 +21,7 @@ class _GiftChatPageState extends State<GiftChatPage> {
bool _isDisposed = false; bool _isDisposed = false;
final List<Msg> _msgList = []; final List<Msg> _msgList = [];
late RtmProvider provider; late RtmProvider provider;
late final RoomNewMsgListener _newMsgListener = _onNewMsg;
@override @override
void initState() { void initState() {
@ -28,7 +29,7 @@ class _GiftChatPageState extends State<GiftChatPage> {
_controller.addListener(_scrollListener); _controller.addListener(_scrollListener);
provider = Provider.of<RtmProvider>(context, listen: false); provider = Provider.of<RtmProvider>(context, listen: false);
final msgList = provider.roomGiftMsgList; final msgList = provider.roomGiftMsgList;
provider.msgGiftListener = _onNewMsg; provider.msgGiftListener = _newMsgListener;
_msgList.addAll(msgList); _msgList.addAll(msgList);
// 使 // 使
_initScrollPosition(); _initScrollPosition();
@ -81,7 +82,9 @@ class _GiftChatPageState extends State<GiftChatPage> {
@override @override
void dispose() { void dispose() {
_isDisposed = true; _isDisposed = true;
provider.msgGiftListener = null; if (identical(provider.msgGiftListener, _newMsgListener)) {
provider.msgGiftListener = null;
}
_controller.removeListener(_scrollListener); _controller.removeListener(_scrollListener);
_controller.dispose(); _controller.dispose();
super.dispose(); super.dispose();

View File

@ -18,6 +18,8 @@ class DataPersistence {
static bool _isInitialized = false; static bool _isInitialized = false;
static Completer<void>? _initializationCompleter; static Completer<void>? _initializationCompleter;
static bool get isInitialized => _isInitialized && _prefs != null;
// SharedPreferences // SharedPreferences
static Future<void> initialize() async { static Future<void> initialize() async {
if (_isInitialized) return; if (_isInitialized) return;
@ -48,9 +50,17 @@ class DataPersistence {
} }
} }
static Future<void> _ensureInitialized() async {
if (isInitialized) {
return;
}
await initialize();
_checkInitialized();
}
// //
static Future<bool> setString(String key, String value) async { static Future<bool> setString(String key, String value) async {
_checkInitialized(); await _ensureInitialized();
return await _prefs!.setString(key, value); return await _prefs!.setString(key, value);
} }
@ -62,7 +72,7 @@ class DataPersistence {
// //
static Future<bool> setInt(String key, int value) async { static Future<bool> setInt(String key, int value) async {
_checkInitialized(); await _ensureInitialized();
return await _prefs!.setInt(key, value); return await _prefs!.setInt(key, value);
} }
@ -74,7 +84,7 @@ class DataPersistence {
// //
static Future<bool> setBool(String key, bool value) async { static Future<bool> setBool(String key, bool value) async {
_checkInitialized(); await _ensureInitialized();
return await _prefs!.setBool(key, value); return await _prefs!.setBool(key, value);
} }
@ -86,7 +96,7 @@ class DataPersistence {
// //
static Future<bool> setDouble(String key, double value) async { static Future<bool> setDouble(String key, double value) async {
_checkInitialized(); await _ensureInitialized();
return await _prefs!.setDouble(key, value); return await _prefs!.setDouble(key, value);
} }

View File

@ -38,7 +38,10 @@ class AccountStorage {
} }
var userJson = DataPersistence.getCurrentUser(); var userJson = DataPersistence.getCurrentUser();
if (userJson.isNotEmpty) { if (userJson.isNotEmpty) {
_currentUser = SocialChatLoginRes.fromJson(jsonDecode(userJson)); _currentUser = _decodeCurrentUser(userJson);
if (_currentUser == null) {
unawaited(_clearPersistedSession(clearDurable: false));
}
} }
return _currentUser; return _currentUser;
} }
@ -46,16 +49,10 @@ class AccountStorage {
/// ///
void setCurrentUser(SocialChatLoginRes user) { void setCurrentUser(SocialChatLoginRes user) {
_currentUser = user; _currentUser = user;
setToken(_currentUser!.token ?? ""); token = _currentUser!.token ?? "";
String userJson = jsonEncode(_currentUser?.toJson()); String userJson = jsonEncode(_currentUser?.toJson());
if (userJson.isNotEmpty) { if (userJson.isNotEmpty) {
DataPersistence.setCurrentUser(userJson); unawaited(_persistSession(token: token, currentUserJson: userJson));
unawaited(
DurableAuthStorage.saveSession(
token: _currentUser!.token ?? "",
currentUserJson: userJson,
),
);
} }
} }
@ -130,54 +127,110 @@ class AccountStorage {
Future<void> restoreDurableSessionIfNeeded() async { Future<void> restoreDurableSessionIfNeeded() async {
final localToken = DataPersistence.getToken().trim(); final localToken = DataPersistence.getToken().trim();
final localUserJson = DataPersistence.getCurrentUser().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; return;
} }
final session = await DurableAuthStorage.readSession(); final session = await DurableAuthStorage.readSession();
if (session == null) { if (session == null) {
if (localToken.isNotEmpty || localUserJson.isNotEmpty) {
unawaited(_clearPersistedSession(clearDurable: false));
}
return; return;
} }
token = session.token; token = session.token;
await DataPersistence.setToken(session.token); _currentUser = _decodeCurrentUser(session.currentUserJson);
await DataPersistence.setCurrentUser(session.currentUserJson); if (_currentUser == null) {
try { token = "";
_currentUser = SocialChatLoginRes.fromJson( await _clearPersistedSession();
jsonDecode(session.currentUserJson), return;
);
} catch (_) {
_currentUser = null;
} }
await _persistSession(
token: session.token,
currentUserJson: session.currentUserJson,
);
} }
void setToken(String tk) { void setToken(String tk) {
token = tk; token = tk;
DataPersistence.setToken(token); unawaited(_persistToken(token));
} }
void _cleanUser() { void _cleanUser() {
token = ""; token = "";
_currentUser = null; _currentUser = null;
DataPersistence.setToken(""); unawaited(_clearPersistedSession());
DataPersistence.setCurrentUser("");
unawaited(DataPersistence.clearPendingChannelAuth()); unawaited(DataPersistence.clearPendingChannelAuth());
unawaited(DurableAuthStorage.clearSession());
} }
///退 ///退
void logout(BuildContext context) { void logout(BuildContext context) {
_cleanUser(); _cleanUser();
SCHeartbeatUtils.cancelTimer(); SCHeartbeatUtils.cancelTimer();
Provider.of<RtcProvider>( try {
context, Provider.of<RtcProvider>(
listen: false, context,
).exitCurrentVoiceRoomSession(true); listen: false,
Provider.of<RtmProvider>(context, listen: false).logout(); ).exitCurrentVoiceRoomSession(true);
} catch (_) {}
try {
Provider.of<RtmProvider>(context, listen: false).logout();
} catch (_) {}
SCNavigatorUtils.pushLoginIfNeeded(context, clearStack: true); SCNavigatorUtils.pushLoginIfNeeded(context, clearStack: true);
SCRoomUtils.closeAllDialogs(); try {
SCRoomUtils.closeAllDialogs();
} catch (_) {}
SCMessageUtils.redPacketFutureCache.clear(); SCMessageUtils.redPacketFutureCache.clear();
SCGlobalConfig.resetVisualEffectSwitchesToRecommendedDefaults(); 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<String, dynamic>.from(decoded));
}
} catch (_) {}
return null;
}
Future<void> _persistToken(String value) async {
try {
await DataPersistence.setToken(value);
} catch (_) {}
}
Future<void> _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<void> _clearPersistedSession({bool clearDurable = true}) async {
try {
await DataPersistence.setToken("");
await DataPersistence.setCurrentUser("");
} catch (_) {}
if (clearDurable) {
await DurableAuthStorage.clearSession();
}
} }
} }

View File

@ -170,8 +170,9 @@ class SCGiftVapSvgaManager {
'start preload path=$path active=$_activePreloadCount ' 'start preload path=$path active=$_activePreloadCount '
'queueRemaining=${_preloadQueue.length}', 'queueRemaining=${_preloadQueue.length}',
); );
_warmupPath(path).whenComplete(() { _warmupPath(path).catchError((_) {}).whenComplete(() {
_activePreloadCount--; _activePreloadCount =
_activePreloadCount > 0 ? _activePreloadCount - 1 : 0;
_log( _log(
'finish preload path=$path active=$_activePreloadCount ' 'finish preload path=$path active=$_activePreloadCount '
'queueRemaining=${_preloadQueue.length}', 'queueRemaining=${_preloadQueue.length}',
@ -263,9 +264,12 @@ class SCGiftVapSvgaManager {
} }
void _cacheSvgaEntity(String path, MovieEntity entity) { 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; videoItemCache[path] = entity;
_trimResolvedCache(videoItemCache, _maxSvgaCacheEntries); _trimSvgaCache();
} }
String? _touchCachedPlayablePath(String path) { String? _touchCachedPlayablePath(String path) {
@ -282,6 +286,22 @@ class SCGiftVapSvgaManager {
_trimResolvedCache(_playablePathCache, _maxPlayablePathCacheEntries); _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<T>(Map<String, T> cache, int maxEntries) { void _trimResolvedCache<T>(Map<String, T> cache, int maxEntries) {
while (cache.length > maxEntries) { while (cache.length > maxEntries) {
final oldestKey = cache.keys.first; final oldestKey = cache.keys.first;
@ -554,7 +574,7 @@ class SCGiftVapSvgaManager {
} else { } else {
_finishCurrentTask(); _finishCurrentTask();
} }
} catch (e, s) { } catch (_) {
_finishCurrentTask(); _finishCurrentTask();
} }
} }
@ -728,6 +748,39 @@ class SCGiftVapSvgaManager {
SCRoomEffectScheduler().clearHighCostTasks(reason: 'stop_playback'); 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() { void dispose() {
_log('dispose queue=$_queueSummary currentPath=${_currentTask?.path}'); _log('dispose queue=$_queueSummary currentPath=${_currentTask?.path}');
@ -735,7 +788,7 @@ class SCGiftVapSvgaManager {
stopPlayback(); stopPlayback();
_svgaLoadTasks.clear(); _svgaLoadTasks.clear();
_playablePathTasks.clear(); _playablePathTasks.clear();
videoItemCache.clear(); clearMemoryCache(includeCurrent: true);
_playablePathCache.clear(); _playablePathCache.clear();
_rgc?.dispose(); _rgc?.dispose();
_rgc = null; _rgc = null;

View File

@ -55,14 +55,17 @@ class SCHeartbeatUtils {
} }
/// ///
static void scheduleAnchorHeartbeat(String roomId) async { static void scheduleAnchorHeartbeat(String roomId) {
cancelAnchorTimer(); cancelAnchorTimer();
scheduleHeartbeat(_c, true, roomId: roomId); unawaited(scheduleHeartbeat(_c, true, roomId: roomId));
SCAccountRepository().anchorHeartbeat(roomId).whenComplete(() { SCAccountRepository()
_a ??= Timer.periodic(Duration(seconds: 60), (timer) { .anchorHeartbeat(roomId)
SCAccountRepository().anchorHeartbeat(roomId).catchError((_) => {}); .then((_) {
}); _a ??= Timer.periodic(Duration(seconds: 60), (timer) {
}); SCAccountRepository().anchorHeartbeat(roomId).catchError((_) => {});
});
})
.catchError((_) {});
} }
static void cancelAnchorTimer() { static void cancelAnchorTimer() {

View File

@ -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<void>.delayed(Duration.zero);
expect(DataPersistence.getToken(), isEmpty);
expect(DataPersistence.getCurrentUser(), isEmpty);
});
}