Android音频流App中ExoPlayer切换Fragment后音频控制异常问题
问题:Fragment切换后ExoPlayer音频无法正确停止/重复播放
当前场景是托管在MainActivity中的RadioPlayerFragment,使用ExoPlayer实现电台播放,需求为:
- 播放音频时切换到其他Fragment,音频需继续播放
- 返回该Fragment时,根据实际播放状态展示对应UI
但出现异常:切换到其他Fragment再返回后,UI状态显示正确,但点击停止按钮时,UI更新为停止状态但音频未停止;再次点击播放按钮会同时播放两个音频(未停止的旧音频和新启动的音频),仅在Fragment切换返回后出现此问题,正常操作时无异常。
原Fragment代码
class RadioPlayerFragment : Fragment() { private lateinit var player: SimpleExoPlayer private lateinit var mBinding: FragmentRadioPlayerBinding private var isPlaying: Boolean = false override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { mBinding = FragmentRadioPlayerBinding.inflate(inflater, container, false) return mBinding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) Log.d(TAG, "onViewCreated \n $isPlaying") isPlaying = SharedPrefs.getBoolean(requireContext(), "isPlaying") initializePlayer() updateUI() mBinding.btnPlayPause.setOnClickListener { if (isPlaying) { Log.d(TAG, "onViewCreated: $isPlaying pausing audio") pauseRadio() } else { Log.d(TAG, "onViewCreated: $isPlaying playing audio") playRadio() } } requireContext().registerReceiver( notificationActionReceiver, IntentFilter("ACTION_STOP") ) } override fun onStart() { super.onStart() Log.d(TAG, "onStart") if (isPlaying) { Log.d(TAG, "onStart: isPlaying") player.playWhenReady = true updateUI() updateNotification() } } private fun initializePlayer() { if (::player.isInitialized && isPlaying) { player.stop() player.release() } player = SimpleExoPlayer.Builder(requireContext()) .setTrackSelector(DefaultTrackSelector(requireContext())) .setLoadControl(DefaultLoadControl()) .build() val mediaItem = MediaItem.fromUri(Uri.parse(Constants.k_STREAM_URL)) player.setMediaItem(mediaItem) player.prepare() Log.d(TAG, "initializePlayer: $isPlaying") player.playWhenReady = false } private fun playRadio() { player.playWhenReady = true isPlaying = true SharedPrefs.setBoolean(requireContext(), "isPlaying", true) updateNotification() updateUI() } private fun pauseRadio() { player.playWhenReady = false isPlaying = false Log.d(TAG, "pauseRadio: $isPlaying") SharedPrefs.setBoolean(requireContext(), "isPlaying", false) updateNotification() updateUI() } private fun updateUI() { if (isPlaying) { mBinding.gif.setImageResource(R.drawable.vuplay) mBinding.btnPlayPause.setImageResource(R.drawable.stopbutton) } else { mBinding.gif.setImageResource(R.drawable.vustop) mBinding.btnPlayPause.setImageResource(R.drawable.playbutton) } } private fun updateNotification() { createNotificationChannel() val stopIntent = Intent(requireContext(), NotificationActionReceiver::class.java).apply { action = "ACTION_STOP" } val stopPendingIntent: PendingIntent = PendingIntent.getBroadcast(requireContext(), 0, stopIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) val notification = NotificationCompat.Builder(requireContext(), "radio_channel") .setContentTitle("GUGS FM is playing") .setContentText("App is running in the background") .setSmallIcon(R.drawable.logo_2) .addAction(R.drawable.stopbutton, "Stop", stopPendingIntent) .build() if (ActivityCompat.checkSelfPermission( requireContext(), Manifest.permission.POST_NOTIFICATIONS ) != PackageManager.PERMISSION_GRANTED ) { return } NotificationManagerCompat.from(requireContext()).notify(1, notification) } private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val name = "Radio Channel" val descriptionText = "Channel for radio notifications" val importance = NotificationManager.IMPORTANCE_LOW val channel = NotificationChannel("radio_channel", name, importance).apply { description = descriptionText } val notificationManager = requireContext().getSystemService(NotificationManager::class.java) notificationManager.createNotificationChannel(channel) } } private val notificationActionReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if (intent?.action == "ACTION_STOP") { pauseRadio() } } } companion object{ val TAG = "RadioPlayerFragment" } }
问题原因
- 重复创建Player实例:Fragment切换后重建时,
onViewCreated会再次调用initializePlayer,但原判断逻辑仅在player已初始化且正在播放时才释放旧实例,实际切换时旧player仍在后台运行,但Fragment重建后player变量已不是原引用,导致新实例被创建,旧实例继续播放。 - 广播接收器未注销:每次进入Fragment都注册接收器,但未在销毁时注销,可能导致多个接收器关联不同的player实例。
- Player生命周期管理错误:Fragment切换时(如replace操作)原Fragment会执行
onDestroyView,但未对player做保留处理,旧实例脱离控制继续运行。
修复方案
方案1:使用ViewModel托管Player(推荐)
ViewModel生命周期与Activity绑定,能确保切换Fragment时player实例不被销毁,彻底解决重复实例问题:
1. 创建RadioPlayerViewModel
class RadioPlayerViewModel : ViewModel() { var player: SimpleExoPlayer? = null var isPlaying: Boolean = false fun initializePlayer(context: Context) { if (player == null) { player = SimpleExoPlayer.Builder(context) .setTrackSelector(DefaultTrackSelector(context)) .setLoadControl(DefaultLoadControl()) .build().apply { val mediaItem = MediaItem.fromUri(Uri.parse(Constants.k_STREAM_URL)) setMediaItem(mediaItem) prepare() playWhenReady = isPlaying } } } fun playRadio() { player?.playWhenReady = true isPlaying = true SharedPrefs.setBoolean(context, "isPlaying", true) } fun pauseRadio() { player?.playWhenReady = false isPlaying = false SharedPrefs.setBoolean(context, "isPlaying", false) } override fun onCleared() { super.onCleared() player?.release() player = null } }
2. 修改RadioPlayerFragment
class RadioPlayerFragment : Fragment() { private lateinit var mBinding: FragmentRadioPlayerBinding private val viewModel: RadioPlayerViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { mBinding = FragmentRadioPlayerBinding.inflate(inflater, container, false) return mBinding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // 从SharedPref恢复播放状态 viewModel.isPlaying = SharedPrefs.getBoolean(requireContext(), "isPlaying") viewModel.initializePlayer(requireContext()) updateUI() mBinding.btnPlayPause.setOnClickListener { if (viewModel.isPlaying) { viewModel.pauseRadio() } else { viewModel.playRadio() } updateUI() updateNotification() } requireContext().registerReceiver( notificationActionReceiver, IntentFilter("ACTION_STOP") ) } override fun onStart() { super.onStart() if (viewModel.isPlaying) { viewModel.player?.playWhenReady = true updateNotification() } } private fun updateUI() { if (viewModel.isPlaying) { mBinding.gif.setImageResource(R.drawable.vuplay) mBinding.btnPlayPause.setImageResource(R.drawable.stopbutton) } else { mBinding.gif.setImageResource(R.drawable.vustop) mBinding.btnPlayPause.setImageResource(R.drawable.playbutton) } } private val notificationActionReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if (intent?.action == "ACTION_STOP") { viewModel.pauseRadio() updateUI() updateNotification() } } } override fun onDestroyView() { super.onDestroyView() requireContext().unregisterReceiver(notificationActionReceiver) } companion object{ val TAG = "RadioPlayerFragment" } }
方案2:不使用ViewModel的临时修复
如果不想引入ViewModel,可通过调整player生命周期逻辑避免重复实例:
class RadioPlayerFragment : Fragment() { private var player: SimpleExoPlayer? = null // 改为可空变量 private lateinit var mBinding: FragmentRadioPlayerBinding private var isPlaying: Boolean = false override fun onCreateView(...) { // 原代码不变 } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) isPlaying = SharedPrefs.getBoolean(requireContext(), "isPlaying") // 仅当player为null时初始化 if (player == null) { initializePlayer() } else { player?.playWhenReady = isPlaying } updateUI() // 原点击事件、广播注册逻辑不变 } private fun initializePlayer() { player = SimpleExoPlayer.Builder(requireContext()) .setTrackSelector(DefaultTrackSelector(requireContext())) .setLoadControl(DefaultLoadControl()) .build().apply { val mediaItem = MediaItem.fromUri(Uri.parse(Constants.k_STREAM_URL)) setMediaItem(mediaItem) prepare() playWhenReady = isPlaying } } // 修改播放/暂停方法,使用可空player private fun playRadio() { player?.playWhenReady = true isPlaying = true SharedPrefs.setBoolean(requireContext(), "isPlaying", true) updateNotification() updateUI() } private fun pauseRadio() { player?.playWhenReady = false isPlaying = false SharedPrefs.setBoolean(requireContext(), "isPlaying", false) updateNotification() updateUI() } // 仅在Activity销毁时释放player override fun onDestroy() { super.onDestroy() player?.release() player = null } override fun onDestroyView() { super.onDestroyView() requireContext().unregisterReceiver(notificationActionReceiver) } // 其他方法不变 }
关键修复点总结
- 单例Player实例:确保整个播放流程中只有一个Player实例,ViewModel是最可靠的实现方式。
- 注销广播接收器:在
onDestroyView中注销接收器,防止内存泄漏和多实例触发问题。 - 统一状态管理:使用ViewModel或共享变量同步UI与Player的播放状态,避免状态不一致。
内容的提问来源于stack exchange,提问作者Sahar Batool
相关产品推荐
相关产品推荐

