Compose应用通过Socket轮询加密数据引发OOM崩溃的问题排查与架构优化咨询
分析与解决你的Compose应用OOM问题
从你的代码和堆转储信息来看,内存持续增长最终OOM的核心原因是未正确管理协程生命周期导致的重复数据收集,加上Socket后台持续生成大量对象无法被GC回收。下面我会一步步拆解问题,并给出优化后的架构方案:
核心问题定位
1. 重复创建未取消的协程
你通过DisposableEffect监听ON_CREATE事件调用getCoinData,但每次页面配置变化(如旋转屏幕)都会触发ON_CREATE,导致创建多个独立的协程收集Socket数据。旧协程没有被取消,会一直持有每次接收的CryptoDataSetItemDto列表和转换后的CryptoDataItem对象,这些对象无法被GC回收,最终堆积导致OOM。
2. 错误的shareIn使用
shareIn需要传入CoroutineScope,但你传入的是CoroutineContext,这会创建一个无绑定生命周期的scope,无法自动取消,进一步加剧内存泄漏。
3. Socket持续后台运行
单例SocketManager的Socket始终保持连接,即使应用在后台也会持续接收数据生成大量对象,而未被取消的协程会一直持有这些对象。
解决方案与优化架构
1. 修复ViewModel的协程管理
ViewModel应使用自身的viewModelScope(会在ViewModel销毁时自动取消),同时避免重复启动收集任务:
@HiltViewModel class WithdrawDepositViewModel @Inject constructor( private val useCase: WalletUseCases, private val socketManager: SocketManager, savedStateHandle: SavedStateHandle ) : ViewModel() { private val _state = mutableStateOf(YourState()) // 替换为你的实际State类 val state = _state.asStateFlow() // 用原子布尔值防止重复启动收集任务 private val isCollecting = AtomicBoolean(false) fun startCollectingCryptoData() { if (!isCollecting.compareAndSet(false, true)) return viewModelScope.launch(Dispatchers.Default) { socketManager.cryptoFlow.asStateFlow() .collectLatest { cryptoListDto -> // 优化:缓存IRT对象,避免每次创建新实例 val cryptoList = cryptoListDto.map { it.toCryptoList() }.toMutableList() cryptoList.add(0, IRT_CRYPTO_ITEM) _state.value = state.value.copy( cryptoList = cryptoList, isLoading = false ) } } } override fun onCleared() { super.onCleared() isCollecting.set(false) } companion object { // 全局缓存IRT对象,复用实例减少内存开销 private val IRT_CRYPTO_ITEM = CryptoDataItem( enName = "Toman", faName = "تومان", symbol = "IRT" ) } }
2. 在Composable中使用repeatOnLifecycle管理收集时机
用Jetpack推荐的repeatOnLifecycle替代手动监听Lifecycle事件,确保页面在前台时才收集数据,后台时自动停止:
@Composable fun HomeScreen( onNavigate: (String) -> Unit, scaffoldState: ScaffoldState, viewModel: HomeScreenViewModel = hiltViewModel() ) { val lifecycle = LocalLifecycleOwner.current.lifecycle val state by viewModel.state.collectAsState() LaunchedEffect(lifecycle) { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.startCollectingCryptoData() } } // 你的页面UI代码 }
3. 让Socket随应用生命周期启停
修改SocketManager,监听应用进程生命周期,后台时断开Socket,减少不必要的数据接收和对象创建:
class SocketManager @Inject constructor( private val json: Json, private val appLifecycle: Lifecycle ) { var cryptoFlow = MutableStateFlow<List<CryptoDataSetItemDto>>(emptyList()) private lateinit var socket: Socket init { initSocket() observeAppLifecycle() } private fun initSocket() { val opts = IO.Options().apply { path = Constants.SOCKET_PATH secure = true } socket = IO.socket(Constants.SOCKET_BASE_URL, opts) socket.on("crypto_data") { dataArray -> dataArray.forEach { data -> try { val coinList = json.decodeFromString<List<CryptoDataSetItemDto>>(data.toString()) cryptoFlow.tryEmit(coinList) } catch (e: Exception) { Sentry.captureException(e) } } } } private fun observeAppLifecycle() { appLifecycle.addObserver(object : LifecycleEventObserver { override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { when (event) { Lifecycle.Event.ON_START -> if (!socket.connected()) socket.connect() Lifecycle.Event.ON_STOP -> if (socket.connected()) socket.disconnect() else -> {} } } }) } }
同时在Dagger模块中提供应用生命周期:
@Module @InstallIn(SingletonComponent::class) object SocketModule { // ... 其他Provider代码 @Provides @Singleton fun provideAppLifecycle(): Lifecycle { return ProcessLifecycleOwner.get().lifecycle } }
4. 额外优化建议
- 检查
CryptoDataSetItemDto和CryptoDataItem的实现,确保没有持有Context或其他大对象的强引用,避免内存泄漏。 - 可以在
cryptoFlow后添加distinctUntilChanged操作符,避免处理重复的数据(如果Socket可能发送相同的列表)。 - 堆转储分析时,可查看对象的引用链,确认是否还有其他未被发现的泄漏点。
内容的提问来源于stack exchange,提问作者Reza Faraji
相关产品推荐
相关产品推荐

