You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

能否安全将AVContentKeySession的makeStreamingContentKeyRequestDataForApp改为同步?

FairPlay DRM: 复用持久密钥并优化异步密钥请求处理

首先直接给结论:你可以用信号量强制makeStreamingContentKeyRequestDataForApp的调用同步,但这不是最优解,甚至可能引发死锁或性能问题。更合理的方案是通过缓存请求队列的方式,让同一密钥的多个请求复用同一次服务器调用,全程异步处理,既节省服务器成本又避免线程阻塞。

一、信号量同步的可行性与潜在风险

你的代码思路是可行的,但存在几个需要修正的问题和风险:

  1. 代码错误:你代码里的变量名不一致(sema和semaphore),而且在ARC环境下不需要dispatch_release——系统会自动管理信号量的内存。
  2. 死锁风险:如果makeStreamingContentKeyRequestDataForApp的completion block和dispatch_semaphore_wait在同一个线程执行,会直接导致死锁。因为等待操作阻塞了线程,completion block永远无法执行来发送信号。
  3. 性能损耗:阻塞线程会占用系统线程资源,尤其是你同时发起多个下载任务时,可能导致线程池耗尽,拖慢整个应用的响应速度。

如果一定要用信号量,修正后的代码需要确保异步调用在独立线程执行:

-(void)handleOnlineRequest:(AVContentKeyRequest *)req {
    NSData *appCert = [self _getAppCertData];
    NSData *assetId = [self _kidFromRequest:req];
    __block NSData *contentKeyRequestData = nil;
    __block NSError *requestError = nil;

    dispatch_semaphore_t sema = dispatch_semaphore_create(0);
    // 切换到全局并发队列执行异步调用,避免completion block与等待操作同线程
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [req makeStreamingContentKeyRequestDataForApp:appCert contentIdentifier:assetId options:nil completion:^(NSData *data, NSError *error) {
            contentKeyRequestData = data;
            requestError = error;
            dispatch_semaphore_signal(sema);
        }];
    });

    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);

    if (requestError) {
        [req respondWithError:requestError];
        return;
    }

    // 继续向服务器请求CKC、生成并保存持久密钥,最后响应请求
    [self _fetchCKCFromServerAndRespond:req withSPC:contentKeyRequestData];
}

二、更优的异步复用方案:缓存请求队列

针对你的场景(多音轨/多下载任务复用同一密钥),更合理的做法是维护一个正在处理的密钥请求队列,同一Asset ID的请求会等待首个请求完成后复用结果,全程异步无阻塞。

实现步骤:

  1. 定义缓存容器和串行队列(保证线程安全)
  2. 处理请求时,先检查是否已有同Asset ID的请求在处理
  3. 若有,将当前请求加入等待队列;若无,发起新的密钥请求
  4. 首个请求完成后,将结果同步给所有等待的请求,并清理缓存

示例代码:

// 类属性定义
@property (nonatomic, strong) NSCache *pendingKeyRequestsCache;
@property (nonatomic, strong) dispatch_queue_t keyRequestSerialQueue;

// 初始化方法
- (instancetype)init {
    self = [super init];
    if (self) {
        _pendingKeyRequestsCache = [[NSCache alloc] init];
        // 串行队列保证缓存操作的线程安全
        _keyRequestSerialQueue = dispatch_queue_create("com.yourdomain.fairplay.keyqueue", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

// 处理AVContentKeyRequest的核心方法
- (void)processContentKeyRequest:(AVContentKeyRequest *)request {
    NSData *assetId = [self _kidFromRequest:request];
    
    dispatch_sync(self.keyRequestSerialQueue, ^{
        // 检查是否已有同Asset ID的请求在处理
        NSMutableArray *pendingRequests = [self.pendingKeyRequestsCache objectForKey:assetId];
        if (pendingRequests) {
            // 加入等待队列,等待首个请求完成
            [pendingRequests addObject:request];
            return;
        }
        
        // 无正在处理的请求,创建新的等待队列
        pendingRequests = [NSMutableArray arrayWithObject:request];
        [self.pendingKeyRequestsCache setObject:pendingRequests forKey:assetId];
        
        NSData *appCert = [self _getAppCertData];
        
        // 发起SPC请求
        [request makeStreamingContentKeyRequestDataForApp:appCert contentIdentifier:assetId options:nil completion:^(NSData *spcData, NSError *spcError) {
            if (spcError) {
                // 错误处理:给所有等待的请求返回错误
                dispatch_sync(self.keyRequestSerialQueue, ^{
                    for (AVContentKeyRequest *pendingReq in pendingRequests) {
                        [pendingReq respondByRequestingRetry]; // 或根据需求返回具体错误
                    }
                    [self.pendingKeyRequestsCache removeObjectForKey:assetId];
                });
                return;
            }
            
            // 向密钥服务器请求CKC并生成持久密钥
            [self _fetchCKCFromServerWithSPC:spcData assetId:assetId completion:^(NSData *persistentKey, NSError *ckcError) {
                dispatch_sync(self.keyRequestSerialQueue, ^{
                    if (ckcError) {
                        for (AVContentKeyRequest *pendingReq in pendingRequests) {
                            [pendingReq respondWithError:ckcError];
                        }
                    } else {
                        // 保存持久密钥
                        [self _savePersistentKey:persistentKey forAssetId:assetId];
                        // 给所有等待的请求返回密钥
                        AVContentKeyResponse *keyResponse = [AVContentKeyResponse contentKeyResponseWithSecureKey:persistentKey];
                        for (AVContentKeyRequest *pendingReq in pendingRequests) {
                            [pendingReq respondWithContentKeyResponse:keyResponse];
                        }
                    }
                    // 清理缓存
                    [self.pendingKeyRequestsCache removeObjectForKey:assetId];
                });
            }];
        }];
    });
}

这个方案的优势:

  • 无线程阻塞:全程异步处理,不会占用线程资源导致性能下降
  • 避免重复请求:同一Asset ID的请求只会发起一次服务器调用,大幅降低密钥服务器的成本
  • 线程安全:通过串行队列保证缓存操作的原子性,避免竞态条件
  • 容错性强:错误发生时,所有等待的请求都会得到统一处理,不会出现部分成功部分失败的情况

总结

优先选择异步缓存请求队列的方案,这是FairPlay密钥复用场景下的标准实践。如果因为特殊需求必须使用信号量,一定要注意线程切换,避免死锁风险。

内容的提问来源于stack exchange,提问作者Tom Hamming

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 05:33:27