AFNetworking 2.0 Преобразуването на NSURLSessionDataTask в NSURLSessionDownloadTask не записва всички файлови данни на диск

Докато преобразуваме NSURLSessionDataTask в NSURLSessionDownloadTask, изпитваме загуба на данни. По-конкретно, при файлове, които са по-големи от 16K, ние губим първите 16K байта (точно 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);
}

B) Поставяйки запис в журнала в метода URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite: на класа NSURLSessionDownloadDelegate, наблюдаваме 1 или повече извиквания на този метод за всеки файл, който се опитваме да изтеглим. Ако файлът е ‹16K, тогава се появява само едно извикване. Ако файлът е >16K, получаваме повече обаждания. Ето този метод:

- (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.

Помогне:

Смятаме, че правим нещо нередно. Може би заглавките, които изпращаме от сървъра, не работят добре с превключвателя от данни към задача за изтегляне. Може би не използваме правилно AFNetworking.

Някой има ли представа за това поведение? Трябва ли да заснемем тялото на първоначалния отговор в URLSession:dataTask:didReceiveData:, преди задачата да бъде превключена на задача за изтегляне?

Наистина странната част е, че ако файлът е под 16K, тогава няма проблеми. Целият файл е записан.

ВСИЧКИ заявки за файлове започват като задача за данни и се преобразуват в задачи за изтегляне.


person Eric Risler    schedule 24.02.2014    source източник


Отговори (1)


Мога да конвертирам NSURLSessionDataTask в NSURLSessionBackgroundTask и (а) файлът е с правилния размер и (б) не виждам никакви извиквания към didReceiveData.

Забелязвам, че извиквате super екземпляри на тези различни делегирани методи. Това е малко любопитно. Чудя се дали вашата super реализация на didReceiveResponse извиква самия манипулатор за завършване, което води до извикване на този манипулатор за завършване два пъти. За отбелязване е, че мога да възпроизведа вашия проблем, ако съзнателно извикам манипулатора два пъти, веднъж с NSURLSessionResponseAllow, преди да го извикам отново с NSURLSessionResponseBecomeDownload.

Уверете се, че извиквате вашия манипулатор на завършване само веднъж и бъдете много внимателни относно това, което имате в тези super методи (или просто премахнете препратката към тях изцяло).

person Rob    schedule 24.02.2014
comment
невероятно Добра работа. Като бележка към всички, които идват тук, документацията за AFNetworking тук заявява, че ако подкласирате класа AFURLSessionManager и замените който и да е от конкретно изброените делегирани методи, първо трябва да извикате суперреализацията. Супер реализацията във версия 2.1.0 извиква манипулатора за завършване, предавайки NSURLSessionResponseAllow по подразбиране. Премахнах обаждането до super и кодът работи добре. - person Eric Risler; 25.02.2014
comment
@EricRisler Да, или гледайки източника на AFNetworking, изглежда, че можете да извикате setDataTaskDidReceiveResponseBlockи да дефинирате как искате да отговорите на манипулатора за завършване там. - person Rob; 25.02.2014
comment
Правилно. Донякъде не толкова очевидно поведение при внедряване, като се има предвид, че разработчикът може да използва или блокове, или делегати, или и двете. Ако не използвате блокове в този случай, получавате това странно поведение. Засега просто не се обаждам на super и не добавям коментар към моя клас, посочвайки защо, вместо да добавям още код. Може да се наложи да добавите блока в бъдещи версии, разбира се. :) - person Eric Risler; 25.02.2014