AFNetworking 2.0 将 NSURLSessionDataTask 转换为 NSURLSessionDownloadTask 不会将所有文件数据写入磁盘
Posted
技术标签:
【中文标题】AFNetworking 2.0 将 NSURLSessionDataTask 转换为 NSURLSessionDownloadTask 不会将所有文件数据写入磁盘【英文标题】:AFNetworking 2.0 Converting NSURLSessionDataTask to NSURLSessionDownloadTask doesn't write all file data to disk 【发布时间】:2014-02-24 06:55:20 【问题描述】:在将 NSURLSessionDataTask 转换为 NSURLSessionDownloadTask 时,我们遇到了数据丢失的问题。具体来说,在大于 16K 的文件上,我们会丢失前 16K 字节(正好是 16384 字节)。写入磁盘的文件比初始响应的长度短....
长帖,感谢阅读和任何建议。
更新 2014-09-30 - 最终修复
所以我最近再次遇到了同样的行为,并决定深入挖掘。事实证明,Matt T(AFNetworking 的作者)发布了一个修改 AFURLSessionManager -respondsToSelector
方法的提交,如果任何 OPTIONAL 委托调用未设置为块,它将返回 NO。提交在这里(问题 #1779):https://github.com/AFNetworking/AFNetworking/commit/6951a26ada965edc6e43cf83a4985b88b0f514d2。
因此,您应该使用可选委托的方式是调用-setTaskDidReceiveAuthenticationChallengeBlock:
方法(调用您想要使用的可选委托的方法),而不是在您的子类中覆盖-URLSession:dataTask:didReceiveResponse:completionHandler:
方法。这样做会产生预期的结果。
设置:
我们正在编写一个从 Web 服务器下载文件的 ios 应用程序。这些文件由一个 php 脚本保护,该脚本对来自 iOS 客户端的请求进行身份验证。
我们正在使用 AFNetworking 2.0+ 并且正在对发送用户凭据等的 API 执行初始 POST (NSURLSessionDataTask) 操作。这是最初的请求:
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 文件。
返回客户端,收到响应并调用NSURLSessionDataDelegate
的-URLSession:dataTask:didReceiveResponse: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:didFinishDownloadingToURL:
方法中,写入的临时文件的大小小于内容长度。
我们发现了什么:
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:totalBytesWritten:totalBytesExpectedToWrite:
方法中,我们观察到对于我们尝试下载的每个文件都对该方法进行了1 次或多次调用。如果文件
- (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”的控制台输出。我添加了 cmets 以突出显示重要的行。
[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:
中的初始响应正文?
真正奇怪的是,如果文件小于 16K,则没有问题。整个文件被写入。
所有文件请求都作为数据任务开始,然后转换为下载任务。
【问题讨论】:
【参考方案1】:我可以将 NSURLSessionDataTask
转换为 NSURLSessionBackgroundTask
,并且 (a) 文件大小合适,并且 (b) 我没有看到任何对 didReceiveData
的调用。
我注意到您正在调用这些各种委托方法的 super
实例。这有点好奇。我想知道您的didReceiveResponse
的super
实现本身是否正在调用完成处理程序,从而导致您两次调用此完成处理程序。值得注意的是,如果我故意调用处理程序两次,我可以重现您的问题,一次使用NSURLSessionResponseAllow
,然后再次使用NSURLSessionResponseBecomeDownload
调用它。
确保只调用一次完成处理程序,并非常小心这些 super
方法中的内容(或完全删除对它们的引用)。
【讨论】:
太棒了。不错的作品。作为对这里所有内容的说明,AFNetworking 文档here 指出,如果您将AFURLSessionManager
类子类化并覆盖任何特别列出的委托方法,您必须首先调用超级实现。 2.1.0 版中的超级实现确实调用了默认传递NSURLSessionResponseAllow
的完成处理程序。我删除了对 super 的调用,代码运行良好。
@EricRisler 是的,或者查看 AFNetworking 源代码,看起来您可以调用 setDataTaskDidReceiveResponseBlock
并定义您希望如何响应那里的完成处理程序。
正确。考虑到开发人员可以使用块或委托或两者兼而有之,这是一种不太明显的实现行为。如果你在这种情况下不使用块,你会得到这种“奇怪”的行为。现在我只是不调用 super 并向我的类添加注释,说明原因而不是添加更多代码。当然,可能需要在未来的版本中添加块。 :)以上是关于AFNetworking 2.0 将 NSURLSessionDataTask 转换为 NSURLSessionDownloadTask 不会将所有文件数据写入磁盘的主要内容,如果未能解决你的问题,请参考以下文章
AFNetworking 2.0 将 NSURLSessionDataTask 转换为 NSURLSessionDownloadTask 不会将所有文件数据写入磁盘
从 AFNetworking 1.3 迁移到 AFNetworking 2.0 的问题