第3章 基础播放器创建与生命周期绑定

3.1 原理概述

ExoPlayer 的播放器实例与 Android Activity/Fragment 生命周期深度耦合。错误的生命周期管理会导致内存泄漏、播放状态丢失、Surface 失效等严重问题。本章的核心原理围绕两条线展开:

  • Builder 模式:通过 ExoPlayer.Builder 链式调用创建高度可配置的播放器实例,避免构造参数膨胀。
  • 生命周期托管:播放器必须跟随宿主组件的 onStart → onResume → onPause → onStop 进行精确的播放控制与资源释放。
核心原则:播放器实例的创建应在 onCreate 中完成,释放应在 onDestroy 中完成;播放/暂停行为应绑定到 onStart/onStop,Surface 管理绑定到 onResume/onPause。

3.2 实现流程

基础播放器的完整生命周期流程包含 6 个阶段,下表呈现各阶段的关键操作:

阶段 宿主回调 播放器操作 View 状态
创建 onCreate new ExoPlayer.Builder(context).build() PlayerView 初始化,绑定 player
准备 onCreate 或 onStart setMediaItem() + prepare() Surface 未就绪,显示占位
可见 onStart play() Surface 可用,开播
前台 onResume (保持播放) Surface 重建后重新绑定
暂停 onPause pause() 保留最后帧
停止 onStop stop(),释放 Surface 清空显示
销毁 onDestroy release() 解除绑定
ExoPlayer 生命周期绑定流程 onCreate ExoPlayer.Builder 构建 PlayerView.setPlayer() setMediaItem + prepare onStart → play() onResume → Surface 就绪 onPause → pause() onStop → stop() onDestroy → release() ⚠ 注意事项 PlayerView 需在 onResume 后重新绑定 Surface

3.3 核心 API 与代码实现

以下是一个标准的 Activity 内实现播放器生命周期绑定的完整代码模板:

public class PlayerActivity extends AppCompatActivity {
    private ExoPlayer player;
    private PlayerView playerView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_player);

        // 1. 通过 Builder 创建播放器实例
        player = new ExoPlayer.Builder(this)
                .setSeekBackIncrementMs(5000)
                .setSeekForwardIncrementMs(15000)
                .setPauseAtEndOfMedia(false)
                .build();

        // 2. 绑定 PlayerView
        playerView = findViewById(R.id.playerView);
        playerView.setPlayer(player);
        playerView.setUseController(true);

        // 3. 准备媒体
        MediaItem mediaItem = MediaItem.fromUri("https://example.com/sample.mp4");
        player.setMediaItem(mediaItem);
        player.prepare();
    }

    @Override
    protected void onStart() {
        super.onStart();
        if (player != null) {
            player.play();
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (player != null) {
            // Surface 在 onResume 后可用,PlayerView 内部自动处理
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (player != null) {
            player.pause();
        }
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (player != null) {
            player.stop();
        }
    }

    @Override
    protected void onDestroy() {
        if (player != null) {
            player.release();
            player = null;
        }
        super.onDestroy();
    }
}
Builder 关键配置参数:
  • setSeekBackIncrementMs() / setSeekForwardIncrementMs() — 快进退时长
  • setPauseAtEndOfMedia() — 播完自动暂停
  • setRenderersFactory() — 自定义渲染器工厂
  • setTrackSelector() — 轨道选择策略
  • setLoadControl() — 缓冲控制

3.4 View 绑定的内部机制

PlayerView 方法 作用 调用时机
setPlayer(ExoPlayer) 绑定播放器,自动监听播放状态更新 UI 播放器创建后立即调用
getPlayer() 返回当前绑定的播放器引用 任何需要访问播放器的时候
setUseController(boolean) 启用或禁用控制器面板 初始化时配置
setControllerVisibilityListener(...) 监听控制器显隐 需要定制交互时

3.5 常见错误与避坑

错误 1:在 onPause 中调用 release()
导致播放器完全销毁,返回前台后不可恢复。正确做法:onPause 中仅调用 pause(),release() 仅在 onDestroy 中执行。
错误 2:多次 setPlayer 导致播放器关联混乱
每个 PlayerView 只能绑定一个播放器,重复绑定前需先 playerView.setPlayer(null) 解绑旧实例。
错误 3:Fragment 中忘记处理 onDestroyView
Fragment 的 View 生命周期与 Fragment 本身不同步。必须在 onDestroyView 中解绑 PlayerView,避免 View 泄漏。
// Fragment 中的正确释放方式
@Override
public void onDestroyView() {
    super.onDestroyView();
    playerView.setPlayer(null); // 先解绑 View
    // 播放器本身在 onDestroy 中 release
}

@Override
public void onDestroy() {
    super.onDestroy();
    if (player != null) {
        player.release();
        player = null;
    }
}

3.6 性能与稳定性优化

  • 懒初始化:PlayerView 的 setPlayer 在 onCreate 中执行即可,不要延迟到 onResume,避免首次播放白屏。
  • Surface 重建保护:切换后台再返回时,Surface 可能重建。PlayerView 内部会自动处理,但自定义 Surface 场景下需在 onResume 中重新绑定。
  • 内存泄漏预防:播放器持有 Context 引用,务必在 onDestroy 中调用 release() 并置空。
  • 播放状态保存:在 onSaveInstanceState 中保存播放位置,在 onRestoreInstanceState 中恢复,避免旋转屏幕后从头播放。
// 保存与恢复播放位置
@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    if (player != null) {
        outState.putLong("position", player.getCurrentPosition());
    }
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    long position = savedInstanceState.getLong("position", 0);
    if (player != null && position > 0) {
        player.seekTo(position);
    }
}

3.7 调试方法

生命周期绑定的调试推荐使用以下手段:

  1. 在 Activity/Fragment 的每个生命周期回调中添加日志输出,观察调用顺序是否正确。
  2. 使用 ExoPlayer 的 addListener() 监听 Player.StateListener,跟踪播放状态转换。
  3. 通过 player.getPlaybackState() 实时获取状态码:STATE_IDLE(1)、STATE_BUFFERING(2)、STATE_READY(3)、STATE_ENDED(4)。
// 添加播放状态监听
player.addListener(new Player.Listener() {
    @Override
    public void onPlaybackStateChanged(int playbackState) {
        Log.d("PlayerState", "状态变更: " + playbackState);
    }

    @Override
    public void onPlayerError(PlaybackException error) {
        Log.e("PlayerError", "播放错误: " + error.getMessage());
    }
});

3.8 部署检查清单

检查项 验证方法 预期结果
Builder 配置完整 查看构建日志 无异常抛出
View 绑定正确 运行后点击播放 视频画面正常显示
后台暂停 按 Home 键 播放暂停,返回后继续
旋转恢复 旋转设备 播放位置不变
无泄漏 Profile / LeakCanary 退出后无 Player 实例残留