如何在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
相关产品推荐
相关产品推荐

