Докато преобразуваме 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, тогава няма проблеми. Целият файл е записан.
ВСИЧКИ заявки за файлове започват като задача за данни и се преобразуват в задачи за изтегляне.