You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

RecyclerView中SimpleExoPlayer调用setPlayWhenReady死线程异常求助

问题分析

这个IllegalStateException本质是ExoPlayer关联的后台线程已经销毁(播放器已释放),但仍有消息试图通过Handler发送到该线程。在RecyclerView场景下,核心问题出在View复用导致的播放器状态不同步——当View被快速滑动回收/复用时,播放器的释放、初始化操作可能出现时序问题,比如:

  • View被复用前,旧的播放器还没完全释放,新的播放器已经创建;
  • 播放器释放后,之前注册的Listener仍在回调,尝试操作已销毁的播放器;
  • 点击播放按钮时,播放器已经被释放,但代码没做状态检查就调用setPlayWhenReady。

解决方案

下面是针对性的修复方案,按优先级排序:


1. 给播放器状态加原子标记,避免无效操作

在自定义View中添加一个标记,跟踪播放器是否已释放,所有播放器操作前先检查标记,从根源避免调用已释放的播放器方法:

public class MyCustomView extends View {
    private SimpleExoPlayer player;
    private boolean isPlayerReleased = false; // 新增状态标记
    private Player.EventListener playerEventListener;
    private DataSource.Factory dataSourceFactory;
    private TrackSelector trackSelector;
    // ... 其他成员变量 ...

    // 初始化播放器的封装方法
    private void initPlayer(String videoUrl) {
        isPlayerReleased = false;
        DefaultLoadControl loadControl = new DefaultLoadControl.Builder()
                .setBufferDurationsMs(1100, 2000, 1000, 1000).createDefaultLoadControl();
        player = ExoPlayerFactory.newSimpleInstance(getContext(), trackSelector, loadControl);
        playerView.setUseController(false);
        playerView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH);
        playerView.setPlayer(player);
        
        // 初始化Listener
        playerEventListener = new PlayerEventListener(this);
        player.addListener(playerEventListener);
        
        Uri uri = Uri.parse(videoUrl);
        MediaSource audioSource = new ExtractorMediaSource.Factory(dataSourceFactory)
                .createMediaSource(uri);
        player.prepare(audioSource);
    }

    // 释放播放器的方法
    private void releasePlayer() {
        if (player != null) {
            isPlayerReleased = true;
            player.removeListener(playerEventListener); // 先移除Listener
            player.release();
            player = null;
            videoPlay.setVisibility(View.VISIBLE);
            postImage.setVisibility(View.VISIBLE);
        }
    }

    // 播放按钮点击事件修改
    videoPlay.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            if (isPlayerReleased || player == null) {
                // 播放器已释放,重新初始化
                initPlayer(videoUrl);
                player.setPlayWhenReady(true);
                updateViewVisibility(true); // 封装视图显示逻辑
                return;
            }
            
            updateViewVisibility(true);
            try {
                player.setPlayWhenReady(true);
            } catch (IllegalStateException e) {
                e.printStackTrace();
                // 捕获异常后重试初始化
                initPlayer(videoUrl);
                player.setPlayWhenReady(true);
            }
        }
    });

    // 封装视图显示/隐藏逻辑
    private void updateViewVisibility(boolean isPlaying) {
        videoPlay.setVisibility(isPlaying ? View.GONE : View.VISIBLE);
        postImage.setVisibility(isPlaying ? View.GONE : View.VISIBLE);
        playerView.setVisibility(isPlaying ? View.VISIBLE : View.GONE);
    }
}

2. 利用RecyclerView的回调确保播放器在复用前被释放

RecyclerView的onViewRecycled回调会在View被回收准备复用时触发,在这里主动调用自定义View的释放方法,比仅依赖onDetachedFromWindow更可靠:

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
    // ... 其他代码 ...

    @Override
    public void onViewRecycled(@NonNull ViewHolder holder) {
        super.onViewRecycled(holder);
        // 主动释放自定义View中的播放器
        holder.myCustomView.releasePlayer();
    }
}

同时保留onDetachedFromWindow的释放逻辑,形成双重保障:

public class MyCustomView extends View {
    // ... 其他代码 ...

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        releasePlayer();
    }
}

3. 优化Player.EventListener,避免内存泄漏和无效回调

避免使用匿名内部类作为Listener,改用静态内部类+弱引用,防止Listener持有View引用导致内存泄漏,同时在回调中检查播放器状态:

public class MyCustomView extends View {
    // ... 其他代码 ...

    // 静态内部类+弱引用的Listener
    private static class PlayerEventListener implements Player.EventListener {
        private WeakReference<MyCustomView> viewRef;

        public PlayerEventListener(MyCustomView view) {
            this.viewRef = new WeakReference<>(view);
        }

        @Override
        public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
            MyCustomView view = viewRef.get();
            if (view == null || view.isPlayerReleased || view.player == null) {
                return; // View已回收或播放器已释放,直接返回
            }
            if (playbackState == Player.STATE_ENDED) {
                view.updateViewVisibility(false);
                try {
                    view.player.setPlayWhenReady(false);
                    view.player.seekTo(0);
                } catch (IllegalStateException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

4. 添加重试逻辑(可选)

如果仍有偶发的异常,可以在捕获异常后添加延迟重试逻辑,确保播放器状态稳定后再操作:

// 在播放按钮的try-catch中添加重试
try {
    player.setPlayWhenReady(true);
} catch (IllegalStateException e) {
    e.printStackTrace();
    // 延迟500ms重试初始化
    postDelayed(() -> {
        if (!isPlayerReleased) {
            initPlayer(videoUrl);
            player.setPlayWhenReady(true);
        }
    }, 500);
}

总结

核心思路是确保播放器状态的一致性:所有操作前检查状态,在View回收/复用、脱离窗口时及时释放播放器,同时避免Listener的无效回调和内存泄漏。通过这些优化,基本可以解决这个偶发的Handler异常问题。

内容的提问来源于stack exchange,提问作者Andrei F

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 05:17:44