每块进度
您可以使用 NSOperation 实例包装您的操作并设置它们之间的依赖关系。这对您的场景来说非常方便,因为 NSOperationQueue 支持开箱即用的 NSProgress 报告。我仍然会将解决方案包装在以下界面中(一个简约的示例,但您可以根据需要扩展它):
@interface TDWSerialDownloader : NSObject
@property(copy, readonly, nonatomic) NSArray<NSURL *> *urls;
@property(strong, readonly, nonatomic) NSProgress *progress;
- (instancetype)initWithURLArray:(NSArray<NSURL *> *)urls;
- (void)resume;
@end
在类的匿名类别(实现文件)中,确保您还有一个单独的属性来存储 NSOperationQueue(稍后需要检索 NSProgress 实例):
@interface TDWSerialDownloader()
@property(strong, readonly, nonatomic) NSOperationQueue *tasksQueue;
@property(copy, readwrite, nonatomic) NSArray<NSURL *> *urls;
@end
在构造函数中创建队列并制作提供的 url 的浅表副本(NSURL 没有可变副本,与 NSArray 不同):
- (instancetype)initWithURLArray:(NSArray<NSURL *> *)urls {
if (self = [super init]) {
_urls = [[NSArray alloc] initWithArray:urls copyItems:NO];
NSOperationQueue *queue = [NSOperationQueue new];
queue.name = @"the.dreams.wind.SerialDownloaderQueue";
queue.maxConcurrentOperationCount = 1;
_tasksQueue = queue;
}
return self;
}
不要忘记公开队列的 progress 属性,以便视图稍后可以使用它:
- (NSProgress *)progress {
return _tasksQueue.progress;
}
现在是核心部分。您实际上无法控制 NSURLSession 在哪个线程中执行请求,它总是异步发生,因此您必须在 delegateQueue 的 delegateQueue 之间手动同步(队列回调在其中执行)和 @ 987654340@ 内部运营。我通常为此使用信号量,但对于这种情况当然有不止一种方法。此外,如果您向 NSOperationQueue 添加操作,它会尝试立即运行它们,但您不希望这样做,因为首先您需要在它们之间设置依赖关系。出于这个原因,您应该将 suspended 属性设置为 YES 直到添加所有操作并设置依赖项。这些想法的完整实现在 resume 方法中:
- (void)resume {
NSURLSession *session = NSURLSession.sharedSession;
// Prevents queue from starting the download straight away
_tasksQueue.suspended = YES;
NSOperation *lastOperation;
for (NSURL *url in _urls.reverseObjectEnumerator) {
NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@ started", url);
__block dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"%@ was downloaded", url);
// read data here if needed
dispatch_semaphore_signal(semaphore);
}];
[task resume];
// 4 minutes timeout
dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 60 * 4));
NSLog(@"%@ finished", url);
}];
if (lastOperation) {
[lastOperation addDependency:operation];
}
lastOperation = operation;
[_tasksQueue addOperation:operation];
}
_tasksQueue.progress.totalUnitCount = _tasksQueue.operationCount;
_tasksQueue.suspended = NO;
}
请注意,TDWSerialDownloader 的方法/属性都不是线程安全的,因此请确保您从单线程使用它。
此处在客户端代码中如何使用此类:
TDWSerialDownloader *downloader = [[TDWSerialDownloader alloc] initWithURLArray:@[
[[NSURL alloc] initWithString:@"https://google.com"],
[[NSURL alloc] initWithString:@"https://stackoverflow.com/"],
[[NSURL alloc] initWithString:@"https://developer.apple.com/"]
]];
_mProgressView.observedProgress = downloader.progress;
[downloader resume];
_mProgressView 是 UIProgressView 类的实例。您还希望在所有操作完成之前保持对 downloader 的强引用(否则它可能会过早释放任务队列)。
进度百分比
对于您在 cmets 中提供的要求,即仅使用 NSURLSessionDataTask 时的百分比进度跟踪,您不能单独依赖 NSOperationQueue(类的 progress 属性仅跟踪已完成任务的数量) .这是一个复杂得多的问题,可以分为三个高级步骤:
- 向服务器请求整个数据的长度;
- 设置
NSURLSessionDataDelegate代表;
- 按顺序执行数据任务,并向UI报告获取的数据进度;
步骤1
如果您无法控制服务器实现,或者它不支持任何方式通知客户端整个数据长度,则无法执行此步骤。具体如何完成取决于协议实现,但通常您使用部分 Range 或 HEAD 请求。在我的示例中,我将使用 HEAD 请求:
NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
if (!weakSelf) {
return;
}
typeof(weakSelf) __strong strongSelf = weakSelf;
[strongSelf p_changeProgressSynchronised:^(NSProgress *progress) {
progress.totalUnitCount = 0;
}];
__block dispatch_group_t lengthRequestsGroup = dispatch_group_create();
for (NSURL *url in strongSelf.urls) {
dispatch_group_enter(lengthRequestsGroup);
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"HEAD";
typeof(self) __weak weakSelf = strongSelf;
NSURLSessionDataTask *task = [strongSelf->_urlSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse
*_Nullable response, NSError *_Nullable error) {
if (!weakSelf) {
return;
}
typeof(weakSelf) __strong strongSelf = weakSelf;
[strongSelf p_changeProgressSynchronised:^(NSProgress *progress) {
progress.totalUnitCount += response.expectedContentLength;
dispatch_group_leave(lengthRequestsGroup);
}];
}];
[task resume];
}
dispatch_group_wait(lengthRequestsGroup, DISPATCH_TIME_FOREVER);
}];
如您所见,所有零件长度都需要作为单个NSOperation 请求。这里的http请求不需要按特定顺序执行,甚至不需要顺序执行,但是操作仍然需要等到所有请求都完成后,所以dispatch_group就派上用场了。
还值得一提的是,NSProgress 是一个相当复杂的对象,它需要一些小的同步来避免竞争条件。此外,由于此实现不再依赖 NSOperationQueue 的内置进度属性,我们将不得不维护我们自己的此对象实例。考虑到这一点,这里是属性及其访问方法实现:
@property(strong, readonly, nonatomic) NSProgress *progress;
...
- (NSProgress *)progress {
__block NSProgress *localProgress;
dispatch_sync(_progressAcessQueue, ^{
localProgress = _progress;
});
return localProgress;
}
- (void)p_changeProgressSynchronised:(void (^)(NSProgress *))progressChangeBlock {
typeof(self) __weak weakSelf = self;
dispatch_barrier_async(_progressAcessQueue, ^{
if (!weakSelf) {
return;
}
typeof(weakSelf) __strong strongSelf = weakSelf;
progressChangeBlock(strongSelf->_progress);
});
}
_progressAccessQueue 是一个并发调度队列:
_progressAcessQueue = dispatch_queue_create("the.dreams.wind.queue.ProgressAcess", DISPATCH_QUEUE_CONCURRENT);
第2步
NSURLSession的block-oriented API虽然方便但不够灵活。它只能在请求完全完成时报告响应。为了获得更细粒度的响应,我们可以使用 NSURLSessionDataDelegate 协议方法并将我们自己的类设置为会话实例的委托:
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
_urlSession = [NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:self
delegateQueue:nil];
为了监听委托方法内部的 http 请求进度,我们必须用没有它们的对应方法替换基于块的方法。我还将超时设置为 4 分钟,这对于大块数据更合理。最后但同样重要的是,信号量现在需要在多个方法中使用,所以它必须变成一个属性:
@property(strong, nonatomic) dispatch_semaphore_t taskSemaphore;
...
strongSelf.taskSemaphore = dispatch_semaphore_create(0);
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:kRequestTimeout];
[[session dataTaskWithRequest:request] resume];
最后我们可以像这样实现委托方法:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (error) {
[self cancel];
// 3.2 Failed completion
_callback([_data copy], error);
}
dispatch_semaphore_signal(_taskSemaphore);
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
[_data appendData:data];
[self p_changeProgressSynchronised:^(NSProgress *progress) {
progress.completedUnitCount += data.length;
}];
}
URLSession:task:didCompleteWithError: 方法还会检查错误情况,但它主要应该只是通过信号量表示当前请求已完成。另一种方法是累积接收到的数据并报告当前进度。
步骤 3
最后一步与我们实现的并没有什么不同每块进度实现,但为了示例数据,我这次决定在谷歌上搜索一些大视频文件:
typeof(self) __weak weakSelf = self;
TDWSerialDataTaskSequence *dataTaskSequence = [[TDWSerialDataTaskSequence alloc] initWithURLArray:@[
[[NSURL alloc] initWithString:@"https://download.samplelib.com/mp4/sample-5s.mp4"],
// [[NSURL alloc] initWithString:@"https://error.url/sample-20s.mp4"], // uncomment to check error scenario
[[NSURL alloc] initWithString:@"https://download.samplelib.com/mp4/sample-30s.mp4"],
[[NSURL alloc] initWithString:@"https://download.samplelib.com/mp4/sample-20s.mp4"]
] callback:^(NSData * _Nonnull data, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (!weakSelf) {
return;
}
typeof(weakSelf) __strong strongSelf = weakSelf;
if (error) {
strongSelf->_dataLabel.text = error.localizedDescription;
} else {
strongSelf->_dataLabel.text = [NSString stringWithFormat:@"Data length loaded: %lu", data.length];
}
});
}];
_progressView.observedProgress = dataTaskSequence.progress;
由于实现了所有花哨的东西,这个示例有点太大而无法作为 SO 答案涵盖所有特性,因此请随时参考 this repo 作为参考。