При преобразовании NSURLSessionDataTask в NSURLSessionDownloadTask мы сталкиваемся с потерей данных. В частности, в файлах размером более 16 КБ мы теряем первые 16 КБ (ровно 16384 байта). Файл, который записывается на диск, короток на длину исходного ответа....
Длинный пост, спасибо за чтение и любые предложения.
ОБНОВЛЕНИЕ 2014-09-30 — Окончательное исправление
Поэтому недавно я снова столкнулся с таким же поведением и решил копнуть глубже. Как оказалось, Matt T (автор AFNetworking) опубликовал коммит, который изменяет метод AFURLSessionManager -respondsToSelector
, и он вернет NO, если какой-либо из ДОПОЛНИТЕЛЬНЫХ вызовов делегата не установлен как Blocks. Коммит находится здесь (проблема № 1779): https://github.com/AFNetworking/AFNetworking/commit/6951a26ada965edc6e43cf83a4985b88b0f514d2.
Итак, способ, которым вы ДОЛЖНЫ использовать необязательные делегаты, - это вызвать метод -setTaskDidReceiveAuthenticationChallengeBlock:
(вызовите один для yнеобязательный делегат, который вы хотите использовать) с вашим блоком ВМЕСТО переопределения метода -URLSession:dataTask:didReceiveResponse:completionHandler:
в вашем подклассе. Выполнение этого дает ожидаемый результат.
Настраивать:
Мы пишем приложение для iOS, которое загружает файлы с веб-сервера. Файлы защищены php-скриптом, который аутентифицирует запрос от клиента iOS.
Мы используем AFNetworking 2.0+ и выполняем начальную операцию POST (NSURLSessionDataTask) в API, отправляя учетные данные пользователя и тому подобное. вот первоначальный запрос:
NSURLSessionDataTask *task = [self POST:API_FULL_SYNC_GETFILE_PATH parameters:body success:^(NSURLSessionDataTask *task, id responseObject){ .. }];
У нас есть собственный класс, который наследуется от класса AFHTTPSessionManager
, в котором содержится весь код iOS в этой задаче.
Сервер получает этот запрос и аутентифицирует пользователя. Одним из параметров POST является файл, который пытается загрузить клиент. Сервер находит файл и выдает его. Чтобы упростить начало, я удалил аутентификацию и некоторые заголовки управления кешем, но вот серверный php-скрипт, который работает:
$file_name = $callparams['FILENAME'];
$requested_file = "$sync_data_dir/$file_name";
@apache_setenv('no-gzip', 1);
@ini_set('zlib.output_compression', 'Off');
set_time_limit(0);`
$file_size = filesize($requested_file);
header("Content-Type: application/gzip");
header("Content-Transfer-Encoding: Binary");
header("Content-Length: {$file_size}");
header("Content-Disposition: attachment; filename=\"{$file_name}\"");
$read_bytes = readfile($requested_file);
Файлы всегда имеют расширение .gz.
Вернувшись на клиент, получен ответ и вызывается метод -URLSession:dataTask:didReceiveResponse:completionHandler:
из NSURLSessionDataDelegate
. Определяем тип MIME и переключаем задачу на задачу загрузки:
-(void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
[super URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
/*
This transforms a data task into a download taks for certain API calls. Check the headers to determine what to do
*/
if ([response.MIMEType isEqualToString:@"application/gzip"]) {
// Convert to download task
completionHandler(NSURLSessionResponseBecomeDownload);
return;
}
// continue as-is
completionHandler(NSURLSessionResponseAllow);
}
Вызывается метод -URLSession:dataTask:didBecomeDownloadTask:
. Мы используем этот метод, чтобы связать задачу данных и задачу загрузки с использованием идентификатора. Это делается для отслеживания результатов задачи загрузки в обработчике завершения задачи данных. Не очень важно для проблемы, но вот код:
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask
{
[super URLSession:session dataTask:dataTask didBecomeDownloadTask:downloadTask];
// Relate the data task with the download task.
if (!_downloadTaskIdToDownloadIdTaskMap) {
_downloadTaskIdToDownloadIdTaskMap = [NSMutableDictionary dictionary];
}
[_downloadTaskIdToDownloadIdTaskMap setObject:@(dataTask.taskIdentifier) forKey:@(downloadTask.taskIdentifier)];
}
Где проявляется проблема:
В методе -URLSession:downloadTask:didFinishDownloadingToURL:
размер записываемого временного файла меньше длины содержимого.
Что мы обнаружили:
A) Если мы реализуем метод URLSession:dataTask:didReceiveData:
класса NSURLSessionTaskDelegate
, мы наблюдаем ровно 1 вызов для каждого файла, который мы пытаемся загрузить. Если файл больше 16384 байт, то результирующий временный файл будет короче на эту величину. Поместив запись журнала в этот метод, мы видим, что длина параметра данных составляет 16384 байта для файлов большего размера.
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
[super URLSession:session dataTask:dataTask didReceiveData:data];
NSMutableDictionary *dataTaskDetails = [_dataTaskDetails objectForKey:@(dataTask.taskIdentifier)];
NSString *fileName = dataTaskDetails[@"FILENAME"];
DDLogDebug(@"Data recieved for file '%@'. Data length %d",fileName,data.length);
}
Б) Размещая запись журнала в методе URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:
класса NSURLSessionDownloadDelegate
, мы наблюдаем 1 или более вызовов этого метода для каждого файла, который мы пытаемся загрузить. Если размер файла ‹16K, то отображается только один вызов. Если файл больше 16 КБ, мы получаем больше звонков. Вот этот метод:
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
[super URLSession:session downloadTask:downloadTask didWriteData:bytesWritten totalBytesWritten:totalBytesWritten totalBytesExpectedToWrite:totalBytesExpectedToWrite];
id dataTaskId = [_downloadTaskIdToDownloadIdTaskMap objectForKey:@(downloadTask.taskIdentifier)];
NSMutableDictionary *dataTaskDetails = [_dataTaskDetails objectForKey:dataTaskId];
NSString *fileName = dataTaskDetails[@"FILENAME"];
DDLogDebug(@"File '%@': Wrote %lld bytes. Total %lld of %lld bytes written.",fileName,bytesWritten,totalBytesWritten,totalBytesExpectedToWrite);
}
В качестве примера ниже приведен вывод консоли для одного файла «members.json.gz». Я добавил комментарии, чтобы выделить важную строку.
[2014-02-24 00:54:16:290][main][I][APIClient.m:syncFullGetFile:withSyncToken:andUserName:andPassword:andCompletedBlock:][Line: 184] API Client requesting file 'members.json.gz' for session with token 'MToxMzkzMjIxMjM4'. <-- This is the initial request for the file.
[2014-02-24 00:54:17:448][NSOperationQueue 0x14eb6380][?][APIClient.m:URLSession:dataTask:didReceiveData:][Line: 542] Data recieved for file 'members.json.gz'. Data length 16384 <-- Initial response, seems to fire BEFORE the conversion to a download task.
[2014-02-24 00:54:17:487][NSOperationQueue 0x14eb6380][?][APIClient.m:URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:][Line: 521] File 'members.json.gz': Wrote 16384 bytes. Total 16384 of 92447 bytes written. <-- Now the data task is a download task.
[2014-02-24 00:54:17:517][NSOperationQueue 0x14eb6380][?][APIClient.m:URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:][Line: 521] File 'members.json.gz': Wrote 16384 bytes. Total 32768 of 92447 bytes written.
[2014-02-24 00:54:17:533][NSOperationQueue 0x14eb6380][?][APIClient.m:URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:][Line: 521] File 'members.json.gz': Wrote 16384 bytes. Total 49152 of 92447 bytes written.
[2014-02-24 00:54:17:550][NSOperationQueue 0x14eb6380][?][APIClient.m:URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:][Line: 521] File 'members.json.gz': Wrote 16384 bytes. Total 65536 of 92447 bytes written.
[2014-02-24 00:54:17:568][NSOperationQueue 0x14eb6380][?][APIClient.m:URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:][Line: 521] File 'members.json.gz': Wrote 10527 bytes. Total 76063 of 92447 bytes written. <-- Total is short by same 16384 - same number as the initial response.
[2014-02-24 00:54:17:573][NSOperationQueue 0x14eb6380][?][APIClient.m:URLSession:downloadTask:didFinishDownloadingToURL:][Line: 472] Temp file size for 'members.json.gz' is 76063
[2014-02-24 00:54:17:573][NSOperationQueue 0x14eb6380][?][APIClient.m:URLSession:downloadTask:didFinishDownloadingToURL:][Line: 485] File 'members.json.gz' downloaded. Reported 92447 of 92447 bytes received.
[2014-02-24 00:54:17:574][NSOperationQueue 0x14eb6380][?][APIClient.m:URLSession:downloadTask:didFinishDownloadingToURL:][Line: 490] File size after move for 'members.json.gz' is 76063
[2014-02-24 00:54:17:574][NSOperationQueue 0x14eb6380][E][APIClient.m:URLSession:downloadTask:didFinishDownloadingToURL:][Line: 497] Expected size of file 'members.json.gz' is 92447 but size on disk is 76063. Temp file size is 0.
Помощь:
Мы считаем, что делаем что-то не так. Возможно, заголовки, которые мы отправляем с сервера, плохо сочетаются с переключателем data-to-download-task. Возможно, мы неправильно используем AFNetworking.
Кто-нибудь знает об этом поведении? Должны ли мы захватить исходное тело ответа в URLSession:dataTask:didReceiveData:
до того, как задача переключится на задачу загрузки?
Что действительно странно, так это то, что если размер файла меньше 16 КБ, проблем не возникает. Весь файл пишется.
ВСЕ запросы на файлы начинаются как задачи данных и преобразуются в задачи загрузки.