这套设计的核心不再是底层怎么写缓存,而是业务层如何高效调度资源。
1. 核心架构图
PreloadManager (单例): 暴露给 UI 层的唯一接口,负责接收当前的播放索引。
PreloadQueue (基于 NSOperationQueue): 负责优先级排序和并发控制。
PreloadOperation (封装任务): 每一个视频缓存请求封装为一个 Operation,支持取消、状态追踪。
Strategy Engine (策略引擎): 计算哪些 URL 该加,哪些该删。
2. 预缓存操作类 (VideoPreloadOperation)
继承自 NSOperation,方便利用 queuePriority 动态调整优先级。
@interface VideoPreloadOperation : NSOperation
@property (nonatomic, strong, readonly) NSURL *url;
@property (nonatomic, assign) NSInteger index; // 用于标识在列表中的位置
- (instancetype)initWithURL:(NSURL *)url index:(NSInteger)index;
@end
@implementation VideoPreloadOperation
// 重点:重写 start 或 main,在其中调用下载器
- (void)main {
if (self.isCancelled) return;
// 模拟调用底层的下载/缓存工具(如 KTVHTTPCache)
// 建议只缓存前 1MB-2MB
[[VideoDownloader shared] downloadDataWithURL:self.url
length:1024*1024
progress:nil
completion:^{
[self finish];
}];
}
@end
3. 调度管理器 (VideoPreloadManager)
重点:何时开始、何时取消、优先级怎么排。
@interface VideoPreloadManager : NSObject
+ (instancetype)shared;
/**
* 核心接口:当列表滚动停止或当前播放索引变化时调用
* @param currentIndex 当前正在显示的视频 Index
* @param allUrls 整个视频 URL 列表
*/
- (void)updatePreloadWindowWithIndex:(NSInteger)currentIndex
urlList:(NSArray<NSURL *> *)allUrls;
@end
@implementation VideoPreloadManager {
NSOperationQueue *_preloadQueue;
NSMutableDictionary<NSURL *, VideoPreloadOperation *> *_taskMap;
NSInteger _preloadCount; // 预加载向后的个数,如 3
}
- (instancetype)init {
if (self = [super init]) {
_preloadQueue = [[NSOperationQueue alloc] init];
_preloadQueue.maxConcurrentOperationCount = 2; // 控制并发,避免抢夺当前播放器的带宽
_taskMap = [NSMutableDictionary dictionary];
_preloadCount = 3;
}
return self;
}
- (void)updatePreloadWindowWithIndex:(NSInteger)currentIndex
urlList:(NSArray<NSURL *> *)allUrls {
// 1. 计算【有效窗口】区间
// 假设窗口为:当前 Index 的前 1 个(防止回滑)和 后 3 个
NSInteger start = MAX(0, currentIndex - 1);
NSInteger end = MIN(allUrls.count - 1, currentIndex + _preloadCount);
NSMutableSet *activeURLSet = [NSMutableSet set];
// 2. 优先级处理与添加任务
for (NSInteger i = start; i <= end; i++) {
NSURL *url = allUrls[i];
[activeURLSet addObject:url];
if (i == currentIndex) continue; // 当前正在播放的由播放器自己处理,不走预加载队列
if (!_taskMap[url]) {
VideoPreloadOperation *op = [[VideoPreloadOperation alloc] initWithURL:url index:i];
// --- 优先级插入策略 ---
if (i == currentIndex + 1) {
op.queuePriority = NSOperationQueuePriorityVeryHigh; // 紧接着的下一个,最高优先级
} else if (i > currentIndex + 1) {
op.queuePriority = NSOperationQueuePriorityNormal; // 更远的,普通优先级
} else {
op.queuePriority = NSOperationQueuePriorityLow; // 已经滑过去的,低优先级
}
_taskMap[url] = op;
[_preloadQueue addOperation:op];
} else {
// 如果任务已存在,根据当前位置动态调整优先级
VideoPreloadOperation *existingOp = _taskMap[url];
if (i == currentIndex + 1) {
existingOp.queuePriority = NSOperationQueuePriorityVeryHigh;
}
}
}
// 3. 取消缓存机制 (Cleanup)
// 凡是不在当前【有效窗口】内的任务,全部取消并移除
NSArray *runningURLs = [_taskMap allKeys];
for (NSURL *url in runningURLs) {
if (![activeURLSet containsObject:url]) {
VideoPreloadOperation *op = _taskMap[url];
[op cancel]; // 停止网络请求
[_taskMap removeObjectForKey:url];
NSLog(@"取消缓存任务: index %ld", (long)op.index);
}
}
}
@end
4. 关键机制详解
A. 何时触发 (Trigger Time)
不要在 scrollViewDidScroll: 这种高频回调里直接处理。
初次进入页面时: 手动调用一次 updatePreloadWindowWithIndex:0。
滚动停止时: 在 scrollViewDidEndScrollingAnimation: 和 scrollViewDidEndDecelerating: 中调用。
索引切换时: 如果你使用的是 UICollectionView 配合 PagingEnabled,在 willDisplayCell 中判断如果 index 变了,立即触发更新。
B. 取消机制 (Cancellation)
原理: NSOperation 的 cancel 会标记状态。在 VideoPreloadOperation 的下载回调中,必须检查 if (self.isCancelled) return;。
内存/连接释放: 一旦执行 cancel,底层对应的 NSURLSessionDataTask 必须执行 cancel,以立即释放带宽供当前播放视频使用。
C. 优先级插入 (Priority Injection)
通过 currentIndex + 1 识别“最紧迫任务”。
NSOperationQueue 会自动根据 queuePriority 在队列中重排任务。
即使队列满了,设置 VeryHigh 的任务也会比 Normal 的任务优先获得执行线程。
D. 并发控制 (Concurrency)
建议 maxConcurrentOperationCount 设为 2。
理由: 移动端带宽有限。1个线程留给当前播放的视频,1-2个线程留给预加载。线程太多会抢夺带宽,导致当前播放卡顿。
5. 进一步优化:针对滑动速度的策略
如果用户快速滑动(Fling),瞬间跳过了 10 个视频,传统的预加载会把这 10 个都请求一遍,造成浪费。
优化方案:
在 updatePreloadWindowWithIndex 中增加一个判断:
- (void)updatePreloadWindowWithIndex:(NSInteger)currentIndex urlList:(NSArray<NSURL *> *)allUrls {
// 记录上次触发的时间
NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
if (now - _lastTriggerTime < 0.2) {
// 如果两次调用的时间间隔极短,说明正在极速滑动
// 此时清空队列,只处理当前和下一个
[_preloadQueue cancelAllOperations];
[_taskMap removeAllObjects];
}
_lastTriggerTime = now;
// ... 执行后续窗口计算逻辑
}
总结该框架优势:
- 确定性:通过滑动窗口算法,明确了任何时刻只有 N个任务在运行。
- 灵活性:通过 queuePriority 确保“下一个要看的”永远最先下载。
- 资源保护:通过 cancel 及时断开不再需要的网络连接,省流量且保带宽。