4. 缓存策略:减少网络请求导致的启动延迟

启动时最耗时的就是网络请求。你想想看,用户点开视频,先要拉 Manifest,然后拉第一段分片,中间还要解析、解密……每一步都在等网络。我做过一个项目,线上反馈启动慢,一看日志,光 Manifest 请求就花了 1.2 秒。后来加了缓存,直接降到 50 毫秒。说白了,缓存就是把「等一下」变成「直接用」。

核心思路:尽可能把已下载的媒体信息、分片数据、甚至解密密钥存到本地,启动时优先走本地 I/O,避开网络延迟。

4.1 媒体信息缓存(Manifest / Playlist)

ExoPlayer 默认每次创建播放器都会重新请求 Manifest。哪怕你刚播完同一个视频,重启后依然要重新拉一次。这很浪费。我习惯在 HttpDataSource.Factory 外面包一层缓存。

// 创建一个带缓存的 DataSource.Factory
CacheDataSource.Factory cacheFactory = new CacheDataSource.Factory()
        .setCache(simpleCache)
        .setUpstreamDataSourceFactory(httpDataSourceFactory)
        .setCacheWriteDataSinkFactory(new CacheDataSink.Factory().setCache(simpleCache));

// 传入播放器
player.setMediaSource(
    new ProgressiveMediaSource.Factory(cacheFactory)
        .createMediaSource(MediaItem.fromUri(videoUrl)));

注意,这里用的是 CacheDataSource。它会把从网络拉下来的数据同时写入本地缓存。下次再请求同一个 URL,直接读缓存。我见过不少团队只配了 HTTP 的 header 缓存(比如 Cache-Control),但那只能省掉条件请求,不能省掉整个连接建立的时间。用 CacheDataSource 才是实实在在的磁盘缓存。

我的经验: Manifest 文件通常很小(几 KB),但每次请求也要走完 TCP 握手、TLS 协商。用缓存后,这部分时间直接归零。对于实时变化的直播流,别忘了设置过期时间,否则用户看到的是旧 Manifest。

4.2 分段缓存(Chunk / Segment)

视频分段(TS、MP4 fragment)是缓存的重点对象。ExoPlayer 内置了 SimpleCache,基于 LRU 策略。我个人建议开够空间,至少 50 MB 起步,否则还没播完就把之前的缓存顶掉了,启动时还是得重新下载。

File cacheDir = new File(context.getCacheDir(), "exo_cache");
SimpleCache simpleCache = new SimpleCache(
        cacheDir,
        new LeastRecentlyUsedCacheEvictor(50 * 1024 * 1024), // 50 MB
        new CacheDataSink.Factory().setCache(simpleCache),
        databaseProvider);

为什么是 50 MB?我踩过坑:之前设了 10 MB,用户播放高清视频,一个分片就 4 MB,播两三个片段就把缓存淹了。启动时缓存命中率不到 20%。改成 50 MB 后,命中率升到 85% 以上。

缓存类型对象推荐大小命中率提升
媒体信息缓存Manifest、Playlist1~5 MB高(几乎 100%)
分片缓存TS、fMP4、密钥文件50~200 MB中高(取决于播放策略)
自定义缓存字幕、贴片广告按业务定视场景而定

4.3 自定义缓存策略

有时候默认的 LRU 不够智能。比如用户经常看同一个剧集,我希望优先保留第一集的分片。怎么办?ExoPlayer 提供了 CacheEvictor 接口,你可以实现自己的淘汰逻辑。

public class MyEvictor implements CacheEvictor {
    private final long maxBytes;
    private final Set<String> priorityKeys = new HashSet<>();

    public void addPriorityKey(String key) {
        priorityKeys.add(key);
    }

    @Override
    public void onStartFile(Cache cache, String key, long position, long maxLength) {
        // 可以动态调整优先级
    }

    @Override
    public boolean onStartFile(Cache cache, String key, long position, long maxLength, long contentLength) {
        long total = cache.getCacheSpace();
        if (total < maxBytes) return false; // 空间够,不淘汰
        // 优先淘汰非 priority 的数据
        // 这里省略具体实现
        return true;
    }
}

说白了,你可以在播放前预置一批缓存 key,比如用户上次播放的位置、收藏列表的视频。启动时这些 key 对应的分片就不容易丢掉。

我曾经踩过的坑: 缓存目录选在了外部存储(/sdcard/),结果 Android 11 分区存储政策导致写缓存失败,ExoPlayer 启动时因为没有 Manifest 直接报错“Source error”。后来统一改用 context.getCacheDir(),这才稳定。注意适配权限变化。

整体缓存策略流程图

下面这张图展示了三层缓存之间的关系。网络请求到达后,先查磁盘缓存,命中直接返回,不命中才走上游网络。写回时同步写入缓存,下次启动就能用了。

ExoPlayer 缓存策略结构 播放器请求 MediaInfo Cache Chunk Cache Custom Eviction DefaultHttpDataSource.Factory CacheDataSource + SimpleCache CacheEvictor 自定义 网络请求(兜底) 缓存未命中才走网络 命中后直接返回数据

几个实战建议

  1. 预热缓存: 在 App 启动后、用户真正点开视频前,可以利用空闲时间下载热门视频的 Manifest 和前两个分片。启动时直接命中缓存,体验像本地播放一样。
  2. 缓存版本对齐: 更新 App 后,旧的缓存可能无法使用(比如加密密钥变了)。我习惯在缓存键里加上 App 版本号,强制刷新一次。
  3. 监控命中率: 通过 CacheStats 定期上报命中率,低于 70% 时要检查淘汰策略或缓存大小是否合理。

缓存优化做得好,启动速度提升 50%~80% 是常有的事。我曾经把一个直播 App 的冷启时间从 3.5 秒降到 0.9 秒,靠的就是 Manifest 缓存 + 预加载第一个分片。