限时福利领取


背景痛点

在移动端音视频开发中,ExoPlayer 的默认 SimpleCache 存在几个明显短板:

  • 固定缓存大小:默认分配固定存储空间,可能导致低端设备内存不足或高端设备资源浪费
  • 无智能预加载:每次播放重新下载内容,相同视频反复消耗流量(实测重复播放消耗 100% 原始流量)
  • 冷启动延迟:尤其在 HLS/DASH 流媒体场景下,分片加载等待明显

缓存策略对比

技术方案

1. 缓存数据源改造

内置 CacheDataSource 的局限性:

  • 仅支持单一缓存目录
  • 无法自定义缓存淘汰策略
  • 预加载逻辑简单粗暴

推荐组合方案:

val cache = SimpleCache(
    File(context.cacheDir, "media_cache"),
    LeastRecentlyUsedCacheEvictor(MAX_CACHE_SIZE),
    null // 可注入自定义数据库实现
)

2. LRU 缓存淘汰策略

LeastRecentlyUsedCacheEvictor 工作原理:

  1. 维护访问时间戳链表
  2. 当缓存空间不足时
  3. 优先删除最久未使用的缓存块

LRU工作流程

3. 分片预加载机制

针对 HLS/DASH 的优化技巧:

val dataSourceFactory = CacheDataSource.Factory().apply {
    setCache(cache)
    setUpstreamDataSourceFactory(HttpDataSource.Factory())
    setFlags(CacheDataSource.FLAG_BLOCK_ON_CACHE) // 强制优先读缓存
}

// 预加载后续3个分片
player.setForegroundMode(true)
player.prepare(mediaSource, false, true) 

代码实现

完整配置示例:

// 初始化缓存(200MB上限)
val cacheSize = 200L * 1024 * 1024
val cacheEvictor = LeastRecentlyUsedCacheEvictor(cacheSize)
val cache = SimpleCache(cacheDir, cacheEvictor)

// 构建数据源链
val upstreamFactory = OkHttpDataSource.Factory()
    .setUserAgent("MyMediaApp")
    .setConnectTimeoutMs(10_000)

val cacheFactory = CacheDataSource.Factory()
    .setCache(cache)
    .setUpstreamDataSourceFactory(upstreamFactory)
    .setCacheWriteDataSinkFactory(null) // 边播边存
    .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)

// 统计命中率
val hitCounter = AtomicInteger()
val missCounter = AtomicInteger()

cache.addListener(object : Cache.Listener {
    override fun onSpanAdded(cache: Cache, span: CacheSpan) {
        missCounter.incrementAndGet()
    }
    override fun onSpanTouched(cache: Cache, oldSpan: CacheSpan, newSpan: CacheSpan) {
        hitCounter.incrementAndGet()
    }
})

性能考量

缓存大小建议

| 设备类型 | 推荐缓存大小 | 内存占用 | |------------|--------------|----------| | 低端手机 | 50-100MB | <30MB | | 中端设备 | 100-200MB | 50MB左右 | | 旗舰机型 | 200-500MB | 可控 |

存储介质选择

  • 内存缓存:适合频繁访问的短视频(实现方式:CachedContentIndex + MemoryCache
  • SSD缓存:长视频首选,注意并发写入控制

避坑指南

  1. 权限问题

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    Android 11+ 需改用 Context#getExternalFilesDir()
  2. 多实例竞争

  3. 全局单例 Cache 实例
  4. 使用 Cache#startReadWriteNonBlocking()

  5. 加密流处理

    setCacheKeyFactory { dataSpec ->
        if (dataSpec.uri.path?.contains("/encrypted/") == true) {
            "${dataSpec.uri}_${DRM_SESSION_ID}"
        } else dataSpec.key
    }

开放性思考

结合 CDN 的分级缓存策略可考虑:

  1. 本地缓存热门内容(LRU策略)
  2. 边缘节点缓存近期内容(TTL策略)
  3. 源站处理长尾请求

如何设计智能调度算法?欢迎在评论区分享你的方案!

Logo

音视频技术社区,一个全球开发者共同探讨、分享、学习音视频技术的平台,加入我们,与全球开发者一起创造更加优秀的音视频产品!

更多推荐