Почему NSURLSession медленнее, чем cURL при загрузке многих файлов?

Я использую cURL для загрузки около 1700+ файлов, которые составляют около ~ 290 МБ, в моем приложении iOS. Для загрузки всех из них с помощью cURL требуется около 5-7 минут для подключения к Интернету. Но так как не у всех есть быстрое подключение к Интернету (особенно когда на ходу), я решил разрешить загрузку файлов в фоновом режиме, чтобы пользователь мог делать другие вещи в ожидании завершения загрузки. Здесь NSURLSession входит.

Используя NSURLSession, в моем интернет-подключении требуется около 20 минут, чтобы загрузить их все, пока приложение находится на переднем плане. Я не возражаю, что это медленное время, когда приложение находится в фоновом режиме, потому что я понимаю, что для загрузки загрузок зависит от ОС. Но это проблема, когда она замедляется, даже когда она на переднем плане. Это ожидаемое поведение? Это из-за количества файлов?

В случае, если я не правильно использую NSURLSession, вот фрагмент того, как я его использую:

// Initialization

NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"<my-identifier>"];
sessionConfiguration.HTTPMaximumConnectionsPerHost = 40;

backgroundSession = [NSURLSession sessionWithConfiguration:sessionConfiguration
                                                  delegate:self
                                             delegateQueue:nil];

// ...

// Creating the tasks and starting the download
for (int i = 0; i < 20 && queuedRequests.count > 0; i++) {
    NSDictionary *requestInfo = [queuedRequests lastObject];
    NSURLSessionDownloadTask *downloadTask = [backgroundSession downloadTaskWithURL:[NSURL URLWithString:requestInfo[@"url"]]];
    ongoingRequests[@(downloadTask.taskIdentifier)] = requestInfo;
    [downloadTask resume];
    [queuedRequests removeLastObject];
    NSLog(@"Begin download file %d/%d: %@", allRequests.count - queuedRequests.count, allRequests.count, requestInfo[@"url"]);
}

// ...

// Somewhere in (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location

// After each download task is completed, grab a file to download from
// queuedRequests, and create another task

if (queuedRequests.count > 0) {
    requestInfo = [queuedRequests lastObject];
    NSURLSessionDownloadTask *newDownloadTask = [backgroundSession downloadTaskWithURL:[NSURL URLWithString:requestInfo[@"url"]]];
    ongoingRequests[@(newDownloadTask.taskIdentifier)] = requestInfo;

    [newDownloadTask resume];
    [queuedRequests removeLastObject];
    NSLog(@"Begin download file %d/%d: %@", allRequests.count - queuedRequests.count, allRequests.count, requestInfo[@"url"]);
}

Я также пытался использовать несколько NSURLSession, но он все еще медленный. Причина, по которой я это пробовал, заключается в том, что при использовании cURL я создаю несколько потоков (около 20), и каждый поток загружает один файл за раз.

Мне также не удалось уменьшить количество файлов, запустив его, потому что мне нужно приложение, чтобы иметь возможность загружать отдельные файлы, так как я время от времени обновляю их. В основном, когда приложение запускается, оно проверяет, есть ли какие-либо файлы, которые были обновлены, и загружать только эти файлы. Поскольку файлы хранятся в S3, а S3 не имеет службы zipping, я не мог бы застегнуть их в один файл на лету.

Ответы

Ответ 1

Как упоминалось в комментариях Filip и Rob, медленность заключается в том, что когда NSURLSession инициализируется backgroundSessionConfigurationWithIdentifier:, задачи загрузки будут выполняться в фоновом режиме, независимо от того, находится ли приложение на переднем плане. Поэтому я решил эту проблему, имея 2 экземпляра NSURLSession: один для загрузки переднего плана и один для загрузки фона:

NSURLSessionConfiguration *foregroundSessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
foregroundSessionConfig.HTTPMaximumConnectionsPerHost = 40;

foregroundSession = [NSURLSession sessionWithConfiguration:foregroundSessionConfig
                                                  delegate:self
                                             delegateQueue:nil];
[foregroundSession retain];

NSURLSessionConfiguration *backgroundSessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"com.terato.darknessfallen.BackgroundDownload"];
backgroundSessionConfig.HTTPMaximumConnectionsPerHost = 40;

backgroundSession = [NSURLSession sessionWithConfiguration:backgroundSessionConfig
                                                  delegate:self
                                             delegateQueue:nil];
[backgroundSession retain];

Когда приложение переключается на фоновый режим, я просто вызываю cancelByProducingResumeData: для каждой из загружаемых задач, которые все еще выполняются, а затем передают его на downloadTaskWithResumeData::

- (void)switchToBackground
{
    if (state == kDownloadManagerStateForeground) {
        [foregroundSession getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
            for (NSURLSessionDownloadTask *downloadTask in downloadTasks) {
                [downloadTask cancelByProducingResumeData:^(NSData *resumeData) {
                    NSURLSessionDownloadTask *downloadTask = [backgroundSession downloadTaskWithResumeData:resumeData];
                    [downloadTask resume];
                }];
            }
        }];

        state = kDownloadManagerStateBackground;
    }
}

Аналогично, когда приложение переключается на передний план, я делаю то же самое, но переключился foregroundSession на backgroundSession:

- (void)switchToForeground
{
    if (state == kDownloadManagerStateBackground) {
        [backgroundSession getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
            for (NSURLSessionDownloadTask *downloadTask in downloadTasks) {
                [downloadTask cancelByProducingResumeData:^(NSData *resumeData) {
                    NSURLSessionDownloadTask *downloadTask = [foregroundSession downloadTaskWithResumeData:resumeData];
                    [downloadTask resume];
                }];
            }
        }];

        state = kDownloadManagerStateForeground;
    }
}

Кроме того, не забудьте вызвать beginBackgroundTaskWithExpirationHandler: перед вызовом switchToBackground, когда приложение переключится на фоновый. Это делается для того, чтобы метод мог быть завершен в фоновом режиме. В противном случае он будет вызываться только после того, как приложение снова войдет в план.