【问题标题】:Running multiple NSURLSessionDataTask sequently and tracking their progress依次运行多个 NSURLSessionDataTask 并跟踪它们的进度
【发布时间】:2022-11-24 05:10:48
【问题描述】:

大家好我只是想知道如何按顺序使用NSURLSessionTask进行串行下载? 我正在寻找的是第一次下载完成后转到下一个,但无论我如何尝试,它仍然是并行的,而不是按顺序进行的。 我试过DISPATCH_QUEUE_SERIALdispatch_group_t

唯一有效的方法是this,但问题是它不调用委托方法,因为它调用完成处理程序,所以我无法向用户更新进度。还有一件事是我不能使用NSURLSessionDownloadTask我必须使用“DataTask”。

这是我尝试但没有结果的最新代码

-(void)download1{

self.task1 = [ self.session dataTaskWithURL:[NSURL URLWithString:@"https://example.com/file.zip"]];
[self.task1 resume];
}
-(void)download2 {

self.task2 = [self.session dataTaskWithURL:[NSURL URLWithString:@"https://example.com/file.z01"]];

}

-(void)download3 {

self.task3 = [self.session dataTaskWithURL:[NSURL URLWithString:@"https://example.com/file.z02"]];

}

-(void)download:(id)sender {

[self testInternetConnection];

dispatch_queue_t serialQueue = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue, ^{
    [self download1];
});

dispatch_sync(serialQueue, ^{
    [self download2];
    [self.task2 resume];
    
});

dispatch_sync(serialQueue, ^{
    [self download3];
    [self.task3 resume];
});



}

我只有一个 UIProgressView 和一个 UILabel 在每个文件的下载过程中更新。 提前致谢。

【问题讨论】:

  • 您似乎观察到 task.progress,因此如果需要,您仍然应该能够使用补全。

标签: objective-c download nsurlsession


【解决方案1】:

每块进度

您可以使用 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 在哪个线程中执行请求,它总是异步发生,因此您必须在 delegateQueuedelegateQueue 之间手动同步(队列回调在其中执行)和 @ 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];

_mProgressViewUIProgressView 类的实例。您还希望在所有操作完成之前保持对 downloader 的强引用(否则它可能会过早释放任务队列)。


进度百分比

对于您在 cmets 中提供的要求,即仅使用 NSURLSessionDataTask 时的百分比进度跟踪,您不能单独依赖 NSOperationQueue(类的 progress 属性仅跟踪已完成任务的数量) .这是一个复杂得多的问题,可以分为三个高级步骤:

  1. 向服务器请求整个数据的长度;
  2. 设置NSURLSessionDataDelegate代表;
  3. 按顺序执行数据任务,并向UI报告获取的数据进度;

    步骤1

    如果您无法控制服务器实现,或者它不支持任何方式通知客户端整个数据长度,则无法执行此步骤。具体如何完成取决于协议实现,但通常您使用部分 RangeHEAD 请求。在我的示例中,我将使用 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 作为参考。

【讨论】:

  • 您好,感谢您的指导,很抱歉回复晚了。好吧,我肯定会尝试使用您的指南,看看我是否能得到结果。
  • 哇,感谢它的工作,但这里的事情是等待任务完成然后更新进度。所以它是按任务工作的,我无法得到像 1% 2% 3% 这样的百分比,而不是每个任务。就像处理 NSURLSession 委托方法的方法一样。我必须按顺序下载 3 个的原因是我的文件很大,iOS 设备会崩溃,这就是为什么我必须将文件分成 3 个,以便我下载并将它们保存到文档文件夹并在那里解压缩。所以我的第一个文件是 20MB,与其他 2 个文件各 400MB 相比,这将花费更少的时间。
  • @Edi 这只有在您从中获取数据的服务器支持Content-Range http 标头时才有可能,否则如果不先下载每个块就无法请求数据大小。
  • 我相信它是支持的,因为我能够得到它。因为当我检查它时它给出了内容长度。
  • @Edi 稍后我会补充我的回答,这是一个很大的变化
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-12-23
  • 2020-12-17
  • 2014-09-18
  • 1970-01-01
相关资源
最近更新 更多