Jetpack Compose中Media3 MediaSession Service初始化崩溃问题
解决Jetpack Compose中Media3 MediaController未初始化崩溃问题
崩溃原因分析
- Compose初始渲染提前访问未初始化变量:
onCreate中Compose直接调用mediaController.toString(),但mediaController是在onStart的异步回调中才完成初始化,此时变量未赋值,直接触发lateinit property has not been initialized异常。 - 异步初始化竞态问题:即使UI不直接访问,点击播放按钮时
play函数也可能在mediaController初始化完成前被调用,同样会抛出异常。 - MediaSessionService未正确启动:
MediaController需要连接到运行中的服务,如果服务未启动,controllerFuture的初始化会延迟甚至失败,导致mediaController一直无法赋值。 - Service逻辑错误:
onGetSession中提前调用player.prepare()不符合设计逻辑,播放控制应由MediaController发起,而非服务主动执行。
修复方案
1. 用Compose状态管理MediaController
将mediaController改为可空状态变量,让Compose根据初始化状态渲染UI,避免直接访问未初始化的变量。
2. 提前启动MediaSessionService
在初始化SessionToken前启动服务,保证MediaController能顺利连接到Session。
3. 调整异步初始化逻辑
初始化完成后更新状态变量,让Compose感知变化并重新渲染。
4. 修正Service逻辑
移除onGetSession中的player.prepare(),同时添加前台服务逻辑(Android 12+要求媒体播放服务必须为前台服务)。
修正后的代码
MainActivity.kt
class MainActivity : ComponentActivity() { private val _mediaController = mutableStateOf<MediaController?>(null) private val mediaController: MediaController? get() = _mediaController.value private lateinit var controllerFuture: ListenableFuture<MediaController> private fun play() { mediaController?.let { controller -> val mediaItem = MediaItem.Builder() .setMediaId("https://sd2.djjohal.com/128/519928/Sip%20Sip%20-%20Geeta%20Zaildar%20(DJJOhAL.Com).mp3") .build() controller.setMediaItem(mediaItem) controller.prepare() controller.play() } } override fun onStart() { super.onStart() // 提前启动MediaSessionService startService(Intent(this, AudioPlayBackService::class.java)) val sessionToken = SessionToken(this, ComponentName(this, AudioPlayBackService::class.java)) controllerFuture = MediaController.Builder(this, sessionToken).buildAsync() controllerFuture.addListener({ try { _mediaController.value = controllerFuture.get() } catch (e: Exception) { e.printStackTrace() } }, MoreExecutors.directExecutor()) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { Media3testTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> // 根据MediaController状态渲染UI mediaController?.let { controller -> Text(text = "Controller已初始化: ${controller.sessionToken}") } ?: run { Text(text = "Controller加载中...") } PlayPause( modifier = Modifier.padding(innerPadding), isPlaying = mediaController?.isPlaying ?: false, onPlayPause = ::play, ) } } } } override fun onStop() { MediaController.releaseFuture(controllerFuture) _mediaController.value = null super.onStop() } } @Composable fun PlayPause( modifier: Modifier = Modifier, isPlaying: Boolean, onPlayPause: () -> Unit, ) { IconButton(modifier = modifier, onClick = onPlayPause) { if (isPlaying) { Icon(Icons.Default.Pause, contentDescription = "暂停") } else { Icon(Icons.Default.PlayArrow, "播放") } } }
AudioPlayBackService.kt
class AudioPlayBackService: MediaSessionService(), MediaSession.Callback { private var mediaSession: MediaSession? = null override fun onCreate() { super.onCreate() val player: Player = ExoPlayer.Builder(this) .build() mediaSession = MediaSession .Builder(this, player) .setCallback(this) .setId("BeatBloomPlayBackService") .build() // Android 12+要求媒体播放服务必须为前台服务,需提前创建通知渠道 val notification = Notification.Builder(this, "media_playback_channel") .setContentTitle("BeatBloom") .setContentText("正在播放媒体") .setSmallIcon(R.drawable.ic_launcher) .setStyle(Notification.MediaStyle()) .build() startForeground(1, notification) } override fun onAddMediaItems( mediaSession: MediaSession, controller: MediaSession.ControllerInfo, mediaItems: MutableList<MediaItem>, ): ListenableFuture<MutableList<MediaItem>> { val updatedMediaItems = mediaItems.map { it.buildUpon().setUri(it.mediaId).build() }.toMutableList() return Futures.immediateFuture(updatedMediaItems) } override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? { // 移除提前prepare逻辑,播放流程由Controller控制 return mediaSession } override fun onDestroy() { mediaSession?.run { player.release() release() mediaSession = null } stopForeground(STOP_FOREGROUND_REMOVE) super.onDestroy() } }
额外说明
- 需提前创建通知渠道:在应用启动时添加渠道创建代码,否则Android 8.0+无法显示前台通知。
- 播放按钮图标逻辑修正:原逻辑图标显示错误,已调整为播放状态显示暂停图标,未播放状态显示播放图标。
内容的提问来源于stack exchange,提问作者Prabh
相关产品推荐
相关产品推荐

