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、Playlist | 1~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(),这才稳定。注意适配权限变化。
整体缓存策略流程图
下面这张图展示了三层缓存之间的关系。网络请求到达后,先查磁盘缓存,命中直接返回,不命中才走上游网络。写回时同步写入缓存,下次启动就能用了。
几个实战建议
- 预热缓存: 在 App 启动后、用户真正点开视频前,可以利用空闲时间下载热门视频的 Manifest 和前两个分片。启动时直接命中缓存,体验像本地播放一样。
- 缓存版本对齐: 更新 App 后,旧的缓存可能无法使用(比如加密密钥变了)。我习惯在缓存键里加上 App 版本号,强制刷新一次。
- 监控命中率: 通过
CacheStats定期上报命中率,低于 70% 时要检查淘汰策略或缓存大小是否合理。
缓存优化做得好,启动速度提升 50%~80% 是常有的事。我曾经把一个直播 App 的冷启时间从 3.5 秒降到 0.9 秒,靠的就是 Manifest 缓存 + 预加载第一个分片。