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

如何在RecyclerView中使用ExoPlayer Media3实现视频缓存

RecyclerView中ExoPlayer Media3的缓存与优化问题解答

1. 如何通过CacheManager高效缓存视频并确保RecyclerView中复用缓存内容

你的CacheManager单例设计方向是对的,以下是优化点和确保缓存复用的关键:

  • 提前初始化缓存:在Application的onCreate()中调用CacheManager.initialize(this),避免在RecyclerView绑定视图时才初始化,减少卡顿。
  • 确保缓存目录有效性:添加目录创建逻辑,防止缓存目录不存在导致缓存失效:
    object CacheManager {
        private lateinit var cache: SimpleCache
    
        fun initialize(context: Context) {
            if (!::cache.isInitialized) {
                val cacheDirectory = File(context.cacheDir, "ExoplayerCache")
                // 确保缓存目录存在
                if (!cacheDirectory.exists()) {
                    cacheDirectory.mkdirs()
                }
                val maxCacheSize = 100 * 1024 * 1024 // 100 MB
                val evictor = LeastRecentlyUsedCacheEvictor(maxCacheSize.toLong())
                val databaseProvider = StandaloneDatabaseProvider(context)
                cache = SimpleCache(cacheDirectory, evictor, databaseProvider)
            }
        }
    
        fun getCache(): SimpleCache = cache
    }
    
  • 保证缓存命中一致性:确保请求的视频URL完全一致(包括参数顺序、无动态变化的参数),ExoPlayer的缓存是基于URL作为key的,URL不同会导致缓存不命中。
  • 缓存DataSource配置强化:在CacheDataSource.Factory中添加缓存读写策略,比如允许离线播放缓存内容:
    val cacheDataSourceFactory = CacheDataSource.Factory()
        .setCache(CacheManager.getCache())
        .setUpstreamDataSourceFactory(DefaultHttpDataSource.Factory().setUserAgent("ExoPlayer"))
        .setCacheWriteDataSinkFactory(DefaultDataSink.Factory()) // 确保写入缓存
        .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR) // 缓存出错时走网络
    

2. ExoPlayerManager初始化与setMediaItem方法的问题修正

你的实现存在几处关键问题,以下是修正方案:

初始化player的问题

  • player池的状态判断不准确:STATE_IDLE状态的player可能还持有资源,复用前需要重置状态;同时限制player池的最大数量(建议2-3个,避免内存占用过高):
    object ExoPlayerManager {
        private val players = mutableListOf<ExoPlayer>()
        private const val MAX_PLAYER_COUNT = 2 // 限制最大player数量
    
        fun initializePlayer(context: Context): ExoPlayer {
            CacheManager.initialize(context)
            // 优先取闲置的player
            val idlePlayer = players.firstOrNull { it.playbackState == Player.STATE_IDLE }
            return if (idlePlayer != null) {
                players.remove(idlePlayer)
                idlePlayer
            } else {
                // 超过最大数量则释放最早的player
                if (players.size >= MAX_PLAYER_COUNT) {
                    val oldestPlayer = players.removeFirst()
                    oldestPlayer.release()
                }
                createNewPlayer(context)
            }
        }
    
        private fun createNewPlayer(context: Context): ExoPlayer {
            return ExoPlayer.Builder(context)
                .setHandleAudioBecomingNoisy(true)
                .build()
        }
    }
    

setMediaItem方法的问题

  • 播放状态设置顺序错误:应该先设置播放位置,再prepare;且playWhenReady = true会自动触发播放,无需额外调用play();同时避免重复添加Listener:
    fun setMediaItem(
        exoPlayer: ExoPlayer,
        videoUri: String,
        playbackPosition: Long = 0L,
        playbackStateListener: Player.Listener
    ) {
        // 先移除旧的Listener(如果有的话)
        exoPlayer.removeListener(playbackStateListener)
    
        val cacheDataSourceFactory = CacheDataSource.Factory()
            .setCache(CacheManager.getCache())
            .setUpstreamDataSourceFactory(DefaultHttpDataSource.Factory().setUserAgent("ExoPlayer"))
    
        val mediaSource = ProgressiveMediaSource.Factory(cacheDataSourceFactory)
            .createMediaSource(MediaItem.fromUri(videoUri))
    
        exoPlayer.setMediaSource(mediaSource)
        exoPlayer.repeatMode = Player.REPEAT_MODE_ONE
        exoPlayer.playWhenReady = true
        // 恢复之前的播放位置
        if (playbackPosition > 0) {
            exoPlayer.seekTo(playbackPosition)
        }
        exoPlayer.addListener(playbackStateListener)
        exoPlayer.prepare()
    }
    
  • releasePlayer的Listener移除错误:你当前调用holder.playbackStateListener()会创建新的Listener实例,无法移除之前添加的那个。需要在ViewHolder中保存Listener的引用:
    // MyViewHolder中修改
    var playbackListener: Player.Listener? = null
    
    override fun onBind(item: BaseModel, position: Int) {
        // ... 其他代码
        playbackListener = playbackStateListener(ivLoader)
        ExoPlayerManager.setMediaItem(
            exoPlayer!!,
            "Any Url",
            item.playbackPosition,
            playbackListener!!
        )
    }
    
    // 在releasePlayer时移除正确的Listener
    fun releasePlayer(holder: MyViewHolder, listItems: BaseModel) {
        val player = holder.exoPlayer
        listItems.playbackPosition = player?.currentPosition ?: 0L
        // 移除保存的Listener
        holder.playbackListener?.let { player?.removeListener(it) }
        player?.pause()
        player?.clearMediaItems()
        player?.seekTo(0) // 重置位置,方便下次复用
        players.add(player)
    }
    

3. RecyclerView中使用ExoPlayer的潜在问题与优化点

核心问题规避

  • 防止多视频同时播放:滑动RecyclerView时,暂停不可见item的player,只保留当前可见item的player播放。可以通过RecyclerView的addOnScrollListener监听滑动状态,或者在onViewRecycled方法中暂停并回收player:
    // RecyclerView配置
    recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
        override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
            super.onScrollStateChanged(recyclerView, newState)
            if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
                // 滑动时暂停所有播放
                val layoutManager = recyclerView.layoutManager as LinearLayoutManager
                val firstVisible = layoutManager.findFirstVisibleItemPosition()
                val lastVisible = layoutManager.findLastVisibleItemPosition()
                for (i in firstVisible..lastVisible) {
                    val holder = recyclerView.findViewHolderForAdapterPosition(i) as? MyViewHolder
                    holder?.exoPlayer?.pause()
                }
            }
        }
    })
    
  • 内存泄漏风险:确保在ViewHolder被回收时,完全解除player与View的绑定,移除所有Listener,避免持有Context或View的引用。
  • PlayerView复用问题:在onViewRecycled中设置videoView.player = null,避免旧player继续渲染到已复用的View上:
    override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
        super.onViewRecycled(holder)
        if (holder is MyViewHolder) {
            ExoPlayerManager.releasePlayer(holder, getItem(holder.adapterPosition))
            holder.binding.videoView.player = null
            holder.exoPlayer = null
            holder.playbackListener = null
        }
    }
    

性能优化

  • 限制player池大小:如之前所述,最多保留2-3个player,避免创建过多player导致内存占用过高。
  • 快速滑动时暂停加载:在RecyclerView快速滑动时,暂停所有player的prepare和播放,减少网络请求和CPU占用。
  • 音频焦点管理:添加音频焦点监听,当其他应用占用音频焦点时,暂停当前视频播放:
    // 在createNewPlayer时添加音频焦点监听
    private fun createNewPlayer(context: Context): ExoPlayer {
        val player = ExoPlayer.Builder(context)
            .setHandleAudioBecomingNoisy(true)
            .build()
        val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
        val audioFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
            .setOnAudioFocusChangeListener { focusChange ->
                when (focusChange) {
                    AudioManager.AUDIOFOCUS_LOSS -> player.pause()
                    AudioManager.AUDIOFOCUS_GAIN -> player.play()
                }
            }
            .build()
        audioManager.requestAudioFocus(audioFocusRequest)
        return player
    }
    
  • 错误处理:在Player.Listener中添加onPlayerError回调,处理播放失败的情况(比如网络错误、缓存错误):
    fun playbackStateListener(ivLoader: LottieAnimationView? = null) = object : Player.Listener{
        override fun onPlaybackStateChanged(playbackState: Int) {
            // ... 原有逻辑
        }
    
        override fun onPlayerError(error: PlaybackException) {
            ivLoader?.visibility = View.GONE
            // 显示错误提示或重试逻辑
        }
    }
    

内容的提问来源于stack exchange,提问作者Walid Ahmed

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 11:30:54