如何修复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
相关产品推荐
相关产品推荐

