加载新Fragment或屏幕旋转时ExoPlayer多实例重复播放问题解决方案咨询
解决ExoPlayer切换Fragment/旋转屏幕时多实例播放混乱的问题
你的问题核心在于每次Fragment重建(切换页面、屏幕旋转)都会创建新的ExoPlayer实例,而旧实例没有被正确释放,导致多个音频同时播放。这是因为你把Player的创建逻辑绑定在了Fragment的生命周期里,而Fragment的生命周期会随页面切换、配置变更重启,下面是具体的解决方案:
一、核心问题分析
现有代码存在几个关键错误:
onStop()方法里错误调用了initializeMediaPlayer(),这会在Fragment停止时新建Player,旧实例没释放直接导致多实例。- 每次Fragment重建(比如旋转屏幕)都会触发
onCreateView(),进而重新初始化Player,旧实例仍在后台运行。 - 没有一个全局的、不随Fragment生命周期销毁的Player托管容器。
二、最优解决方案:用ViewModel托管ExoPlayer
ViewModel的生命周期和宿主Activity绑定,不会随Fragment重建、页面切换而销毁,完美解决多实例问题。
1. 创建ExoPlayer的ViewModel类
import androidx.lifecycle.ViewModel; import com.google.android.exoplayer2.SimpleExoPlayer; import android.content.Context; import com.google.android.exoplayer2.MediaItem; public class PlayerViewModel extends ViewModel { private SimpleExoPlayer player; private boolean playWhenReady = true; private int currentWindow = 0; private long playbackPosition = 0; // 获取唯一的Player实例 public SimpleExoPlayer getPlayer(Context context) { if (player == null) { player = new SimpleExoPlayer.Builder(context).build(); // 恢复之前的播放状态 player.setPlayWhenReady(playWhenReady); player.seekTo(currentWindow, playbackPosition); } return player; } // 保存播放状态 public void savePlayerState() { if (player != null) { playWhenReady = player.getPlayWhenReady(); playbackPosition = player.getCurrentPosition(); currentWindow = player.getCurrentWindowIndex(); } } // 释放Player(在Activity销毁时调用) public void releasePlayer() { savePlayerState(); if (player != null) { player.release(); player = null; } } @Override protected void onCleared() { super.onCleared(); releasePlayer(); } }
2. 修改Fragment代码,从ViewModel获取Player
import androidx.lifecycle.ViewModelProvider; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.ui.PlayerView; import android.widget.SeekBar; public class HomeFragment extends Fragment { private PlayerViewModel playerViewModel; private SimpleExoPlayer player; private PlayerView playerView; private SeekBar playSeekBar; private String revurl = "你的流媒体地址"; // 替换为实际地址 @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_home, container, false); initializeUIElements(root); // 初始化ViewModel(绑定Activity生命周期) playerViewModel = new ViewModelProvider(requireActivity()).get(PlayerViewModel.class); return root; } private void initializeUIElements(View root) { playSeekBar = root.findViewById(R.id.progressBar1); playSeekBar.setMax(100); playSeekBar.setVisibility(View.INVISIBLE); playerView = root.findViewById(R.id.video_view); } @Override public void onStart() { super.onStart(); if (Util.SDK_INT >= 24) { initPlayer(); } } @Override public void onResume() { super.onResume(); if ((Util.SDK_INT < 24 || player == null)) { initPlayer(); } } private void initPlayer() { player = playerViewModel.getPlayer(getContext()); playerView.setPlayer(player); // 仅第一次初始化时设置媒体地址,避免重复加载 if (player.getMediaItemCount() == 0) { MediaItem media = MediaItem.fromUri(revurl); player.setMediaItem(media); player.prepare(); } } @Override public void onPause() { super.onPause(); if (Util.SDK_INT < 24) { playerViewModel.savePlayerState(); playerView.setPlayer(null); // 断开与View的绑定,避免内存泄漏 } } @Override public void onStop() { super.onStop(); if (Util.SDK_INT >= 24) { playerViewModel.savePlayerState(); playerView.setPlayer(null); } } @SuppressLint("InlinedApi") private void hideUi() { playerView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); } }
3. 在主Activity中处理全局释放
当Activity真正销毁时,释放ViewModel中的Player实例:
@Override protected void onDestroy() { super.onDestroy(); PlayerViewModel playerViewModel = new ViewModelProvider(this).get(PlayerViewModel.class); playerViewModel.releasePlayer(); }
三、其他注意事项
- 切换Fragment时:Player实例会保留在ViewModel中,若需要暂停播放,可在Fragment的
onPause中调用player.setPlayWhenReady(false)。 - 屏幕旋转:ViewModel会自动保留Player实例和播放状态,旋转后直接恢复,不会新建实例。
- 避免重复加载媒体:通过
player.getMediaItemCount() == 0判断,确保仅第一次初始化时设置媒体地址。
这样修改后,整个App只会存在一个ExoPlayer实例,彻底解决多实例播放混乱的问题。
内容的提问来源于stack exchange,提问作者markharrop
相关产品推荐
相关产品推荐

