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

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 '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<YumiApplication>
@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<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 {
//
await _deepLinkHandler.initDeepLinks(

View File

@ -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<SCLoginWithAccountPage>
}
///
_login() async {
Future<void> _login() async {
String account = accountController.text;
String pass = passController.text;
if (account.isEmpty || pass.isEmpty) {
@ -394,15 +395,47 @@ class SCLoginWithAccountPageState extends State<SCLoginWithAccountPage>
}
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.";
}
}

View File

@ -21,6 +21,7 @@ class _AllChatPageState extends State<AllChatPage> {
bool _isDisposed = false;
final List<Msg> _msgList = [];
late RtmProvider provider;
late final RoomNewMsgListener _newMsgListener = _onNewMsg;
@override
void initState() {
@ -28,7 +29,7 @@ class _AllChatPageState extends State<AllChatPage> {
_controller.addListener(_scrollListener);
provider = Provider.of<RtmProvider>(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<AllChatPage> {
@override
void dispose() {
_isDisposed = true;
provider.msgAllListener = null;
if (identical(provider.msgAllListener, _newMsgListener)) {
provider.msgAllListener = null;
}
_controller.removeListener(_scrollListener);
_controller.dispose();
super.dispose();

View File

@ -21,6 +21,7 @@ class _ChatPageState extends State<ChatPage> {
bool _isDisposed = false;
List<Msg> _msgList = [];
late RtmProvider provider;
late final RoomNewMsgListener _newMsgListener = _onNewMsg;
@override
void initState() {
@ -28,7 +29,7 @@ class _ChatPageState extends State<ChatPage> {
_controller.addListener(_scrollListener);
provider = Provider.of<RtmProvider>(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<ChatPage> {
@override
void dispose() {
_isDisposed = true;
provider.msgChatListener = null;
if (identical(provider.msgChatListener, _newMsgListener)) {
provider.msgChatListener = null;
}
_controller.removeListener(_scrollListener);
_controller.dispose();
super.dispose();

View File

@ -21,6 +21,7 @@ class _GiftChatPageState extends State<GiftChatPage> {
bool _isDisposed = false;
final List<Msg> _msgList = [];
late RtmProvider provider;
late final RoomNewMsgListener _newMsgListener = _onNewMsg;
@override
void initState() {
@ -28,7 +29,7 @@ class _GiftChatPageState extends State<GiftChatPage> {
_controller.addListener(_scrollListener);
provider = Provider.of<RtmProvider>(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<GiftChatPage> {
@override
void dispose() {
_isDisposed = true;
provider.msgGiftListener = null;
if (identical(provider.msgGiftListener, _newMsgListener)) {
provider.msgGiftListener = null;
}
_controller.removeListener(_scrollListener);
_controller.dispose();
super.dispose();

View File

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

View File

@ -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<void> 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<RtcProvider>(
context,
listen: false,
).exitCurrentVoiceRoomSession(true);
Provider.of<RtmProvider>(context, listen: false).logout();
try {
Provider.of<RtcProvider>(
context,
listen: false,
).exitCurrentVoiceRoomSession(true);
} catch (_) {}
try {
Provider.of<RtmProvider>(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<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 '
'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<T>(Map<String, T> 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;

View File

@ -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() {

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);
});
}