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

如何修复GlobalScope警告?Compose+Media3音乐App优化

解决GlobalScope警告并遵循Android最佳实践

问题根源

GlobalScope属于敏感API,它的协程生命周期不受任何组件管控,极易引发内存泄漏(比如MusicServiceHandler销毁后协程仍在后台运行)。正确做法是使用与组件生命周期绑定的CoroutineScope,并通过Job管理协程的启动与取消。

修改方案

我们给MusicServiceHandler添加专属的协程作用域,替换GlobalScope,同时修正进度更新的逻辑实现:

1. 引入并绑定CoroutineScope

给MusicServiceHandler实现CoroutineScope接口,委托MainScope()作为协程作用域(更新StateFlow需要在主线程执行):

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope

class MusicServiceHandler(
    private val exoPlayer: Player,
) : Player.Listener, CoroutineScope by MainScope() {
    // 原有代码保留,移除@OptIn(DelicateCoroutinesApi::class)注解和GlobalScope相关代码
}

2. 修正onIsPlayingChanged方法

直接使用当前类的协程作用域启动进度更新,不再依赖GlobalScope:

override fun onIsPlayingChanged(isPlaying: Boolean) {
    _musicStates.value = MusicStates.MediaPlaying(isPlaying = isPlaying)
    _musicStates.value = MusicStates.CurrentMediaPlaying(exoPlayer.currentMediaItemIndex)
    if (isPlaying) {
        startProgressUpdate()
    } else {
        stopProgressUpdate()
    }
}

3. 修复startProgressUpdate逻辑

原来的job.run写法错误,改为通过协程作用域启动循环协程,并将返回的Job赋值给成员变量:

private fun startProgressUpdate() {
    // 先取消旧协程,避免重复启动
    job?.cancel()
    job = launch {
        while (isActive) { // 用isActive判断协程状态,取消后立即停止循环
            delay(500)
            _musicStates.value = MusicStates.MediaProgress(exoPlayer.currentPosition)
        }
    }
}

4. 添加协程清理逻辑

新增destroy方法,在组件销毁时取消所有协程并移除ExoPlayer监听器:

fun destroy() {
    cancel() // 取消当前作用域下的所有协程
    exoPlayer.removeListener(this)
}

5. 在ViewModel中调用destroy

在MusicViewModel的onCleared方法中调用destroy,确保协程随ViewModel一起销毁:

override fun onCleared() {
    viewModelScope.launch {
        musicServiceHandler.onMediaStateEvents(MediaStateEvents.Stop)
        musicServiceHandler.destroy() // 清理MusicServiceHandler的资源
    }
    super.onCleared()
}

完整修改后的MusicServiceHandler代码

import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import com.techullurgy.media3musicplayer.utils.MediaStateEvents
import com.techullurgy.media3musicplayer.utils.MusicStates
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class MusicServiceHandler(
    private val exoPlayer: Player,
) : Player.Listener, CoroutineScope by MainScope() {

    private val _musicStates: MutableStateFlow<MusicStates> = MutableStateFlow(MusicStates.Initial)
    val musicStates: StateFlow<MusicStates> = _musicStates.asStateFlow()

    private var job: Job? = null

    init {
        exoPlayer.addListener(this)
    }

    fun setMediaItem(mediaItem: MediaItem) {
        exoPlayer.setMediaItem(mediaItem)
        exoPlayer.prepare()
    }

    fun setMediaItemList(mediaItems: List<MediaItem>) {
        exoPlayer.setMediaItems(mediaItems)
        exoPlayer.prepare()
    }

    suspend fun onMediaStateEvents(
        mediaStateEvents: MediaStateEvents,
        selectedMusicIndex: Int = -1,
        seekPosition: Long = 0,
    ) {
        when (mediaStateEvents) {
            MediaStateEvents.Backward -> exoPlayer.seekBack()
            MediaStateEvents.Forward -> exoPlayer.seekForward()
            MediaStateEvents.PlayPause -> playPauseMusic()
            MediaStateEvents.SeekTo -> exoPlayer.seekTo(seekPosition)
            MediaStateEvents.SeekToNext -> exoPlayer.seekToNext()
            MediaStateEvents.SeekToPrevious -> exoPlayer.seekToPrevious()
            MediaStateEvents.Stop -> stopProgressUpdate()
            MediaStateEvents.SelectedMusicChange -> {
                when (selectedMusicIndex) {
                    exoPlayer.currentMediaItemIndex -> {
                        playPauseMusic()
                    }

                    else -> {
                        exoPlayer.seekToDefaultPosition(selectedMusicIndex)
                        _musicStates.value = MusicStates.MediaPlaying(
                            isPlaying = true
                        )
                        exoPlayer.playWhenReady = true
                        startProgressUpdate()
                    }
                }
            }

            is MediaStateEvents.MediaProgress -> {
                exoPlayer.seekTo(
                    (exoPlayer.duration * mediaStateEvents.progress).toLong()
                )
            }
        }
    }

    override fun onPlaybackStateChanged(playbackState: Int) {
        when (playbackState) {
            ExoPlayer.STATE_BUFFERING -> _musicStates.value =
                MusicStates.MediaBuffering(exoPlayer.currentPosition)

            ExoPlayer.STATE_READY -> _musicStates.value = MusicStates.MediaReady(exoPlayer.duration)

            Player.STATE_ENDED -> {
                // no-op
            }

            Player.STATE_IDLE -> {
                // no-op
            }
        }
    }

    override fun onIsPlayingChanged(isPlaying: Boolean) {
        _musicStates.value = MusicStates.MediaPlaying(isPlaying = isPlaying)
        _musicStates.value = MusicStates.CurrentMediaPlaying(exoPlayer.currentMediaItemIndex)
        if (isPlaying) {
            startProgressUpdate()
        } else {
            stopProgressUpdate()
        }
    }

    private suspend fun playPauseMusic() {
        if (exoPlayer.isPlaying) {
            exoPlayer.pause()
            stopProgressUpdate()
        } else {
            exoPlayer.play()
            _musicStates.value = MusicStates.MediaPlaying(
                isPlaying = true
            )
            startProgressUpdate()
        }
    }

    private fun startProgressUpdate() {
        job?.cancel()
        job = launch {
            while (isActive) {
                delay(500)
                _musicStates.value = MusicStates.MediaProgress(exoPlayer.currentPosition)
            }
        }
    }

    private fun stopProgressUpdate() {
        job?.cancel()
        _musicStates.value = MusicStates.MediaPlaying(isPlaying = false)
    }

    fun destroy() {
        cancel()
        exoPlayer.removeListener(this)
    }
}

修改说明

  • 用CoroutineScope by MainScope()让MusicServiceHandler拥有独立协程作用域,生命周期与组件绑定,避免内存泄漏。
  • 用isActive替代while(true),协程被取消后立即终止循环,减少资源浪费。
  • 新增destroy方法,在组件销毁时彻底清理协程和ExoPlayer监听器。
  • 移除了@OptIn(DelicateCoroutinesApi::class)注解,不再使用风险较高的GlobalScope。

内容的提问来源于stack exchange,提问作者Fatima Jamal

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 16:45:02