You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 19:04:06