Ios AFNetworking 2.0将NSURLSessionDataTask转换为NSURLSessionDownloadTask不需要';t将所有文件数据写入磁盘
将NSURLSessionDataTask转换为NSURLSessionDownloadTask时,我们遇到数据丢失。具体来说,对于大于16K的文件,我们将丢失前16K字节(确切地说是16384字节)。写入磁盘的文件短于初始响应的长度 长文章,感谢阅读和任何建议 更新2014-09-30-最终修复 所以我最近又遇到了同样的行为,我决定更深入地研究。事实证明,Matt T(AFNetworking的作者)发布了一个commit,它修改了Ios AFNetworking 2.0将NSURLSessionDataTask转换为NSURLSessionDownloadTask不需要';t将所有文件数据写入磁盘,ios,objective-c,network-programming,afnetworking-2,Ios,Objective C,Network Programming,Afnetworking 2,将NSURLSessionDataTask转换为NSURLSessionDownloadTask时,我们遇到数据丢失。具体来说,对于大于16K的文件,我们将丢失前16K字节(确切地说是16384字节)。写入磁盘的文件短于初始响应的长度 长文章,感谢阅读和任何建议 更新2014-09-30-最终修复 所以我最近又遇到了同样的行为,我决定更深入地研究。事实证明,Matt T(AFNetworking的作者)发布了一个commit,它修改了AFURLSessionManager-respondsToS
AFURLSessionManager-respondsToSelector
方法,如果任何可选的委托调用未设置为块,它将返回NO。提交就在这里(第1779期):
因此,使用可选委托的方法应该是使用块调用-setTaskDidReceiveAuthenticationChallengeBlock:
方法(调用您要使用的可选委托的方法),而不是覆盖子类中的-URLSession:dataTask:didReceiveResponse:completionHandler:
方法。这样做会产生预期的结果
设置:
我们正在编写一个从web服务器下载文件的iOS应用程序。这些文件由一个php脚本保护,该脚本对来自iOS客户端的请求进行身份验证
我们正在使用AFNetworking 2.0+,并正在对发送用户凭据等的API执行初始POST(NSURLSessionDataTask)操作。以下是最初的请求:
NSURLSessionDataTask*task=[self POST:API_FULL_SYNC_GETFILE_路径参数: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文件
回到客户端,将收到响应,并调用NSURLSessionDataDelegate
的-URLSession:dataTask:didReceiverResponse:completionHandler:
方法。我们检测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:
方法。我们使用此方法使用id关联数据任务和下载任务。这样做是为了在数据任务完成处理程序中跟踪下载任务的结果。对于这个问题来说不是非常重要,但下面是代码:
- (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:didfishdownloadingtourl:
方法中,写入的临时文件的大小小于内容长度
我们发现:
A) 如果我们实现了NSURLSessionTaskDelegate
类的URLSession:dataTask:didReceiveData:
方法,那么对于我们尝试下载的每个文件,我们只观察到一个调用。如果该文件大于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) 将日志条目放入NSURLSessionDownloadDelegate
类的URLSession:downloadTask:didWriteData:TotalBytesWrite:totalBytesExpectedToWrite:
方法中,我们观察到每个尝试下载的文件都有一个或多个对此方法的调用。如果文件是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.
[2014-02-24 00:54:16:290][main][I][APIClient.m:syncFullGetFile:withSyncToken:andUserName:andPassword:andCompletedBlock:[Line:184]API客户端请求文件'members.json.gz'用于带有标记'MToxMzkzMjIxMjM4'的会话 我可以将NSURLSessionDataTask
转换为NSURLSessionBackgroundTask
,并且(a)文件大小正确,并且(b)我没有看到对didReceiveData
的任何调用
我注意到您正在调用这些不同委托方法的super
实例。这有点奇怪。我想知道您的super
实现的didReceiveResponse
是否正在调用完成处理程序本身,导致您调用此完成处理程序两次。值得注意的是,如果我故意调用处理程序两次,一次是使用NSURLSessionResponseAllow
调用,然后再次使用NSURLSessionResponseBecomeDownload
调用,我可以重现您的问题
确保只调用一次完成处理程序,并非常小心这些super
方法中的内容(或者干脆删除对它们的引用)。。干得好。请注意,AFNetworking文档指出,如果您将AFURLSessionManager
类划分为子类并覆盖任何特定列出的委托方法,则必须首先调用超级实现。版本2.1.0中的超级实现在默认情况下会调用传递NSURLSessionResponseAllow
的完成处理程序。我删除了对super的调用,代码运行正常。@EricRisler是的,或者查看AFNetworking源代码,看起来您可以调用setDataTaskDidReceiveResponseBlock
,并定义如何响应那里的完成处理程序。正确。这是一种不太明显的实现行为,因为开发人员可以使用块或委托,也可以同时使用两者。如果在这种情况下不使用块,就会出现这种“奇怪”的行为。现在我只是不给super打电话,给他添加评论