高效的流式视频预取框架

这套设计的核心不再是底层怎么写缓存,而是业务层如何高效调度资源。
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 及时断开不再需要的网络连接,省流量且保带宽。
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容