ios - AFNetworking 2.0 将 NSURLSessionDataTask 转换为 NSURLSessionDownloadTask 不会将所有文件数据写入磁盘

标签 ios objective-c network-programming afnetworking-2

在将 NSURLSessionDataTask 转换为 NSURLSessionDownloadTask 时,我们遇到了数据丢失。具体来说,对于大于 16K 的文件,我们将丢失前 16K 字节(正好是 16384 字节)。写入磁盘的文件比初始响应的长度短....

很长的帖子,感谢您的阅读和任何建议。

更新 2014-09-30 - 最终修复

所以我最近再次遇到了同样的行为,并决定深入挖掘。事实证明,Matt T(AFNetworking 的作者)发布了一个修改 AFURLSessionManager -respondsToSelector 的提交。方法,如果任何可选委托(delegate)调用未设置为 block ,它将返回 NO。提交在这里(问题 #1779):https://github.com/AFNetworking/AFNetworking/commit/6951a26ada965edc6e43cf83a4985b88b0f514d2 .

因此,您应该使用可选委托(delegate)的方式是调用 -setTaskDidReceiveAuthenticationChallengeBlock:方法(调用您要使用的可选委托(delegate)的方法),而不是覆盖 -URLSession:dataTask:didReceiveResponse:completionHandler:子类中的方法。这样做会产生预期的结果。

设置:

我们正在编写一个从 Web 服务器下载文件的 iOS 应用程序。这些文件由验证来自 iOS 客户端的请求的 php 脚本保护。

我们正在使用 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 参数之一是客户端尝试下载的文件。服务器定位文件并将其吐出。为了让事情简单开始,我删除了身份验证和一些缓存控制 header ,但这里是运行的服务器 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:方法被调用。我们使用此方法将数据任务和下载任务使用 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) 如果我们实现 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.

帮助:

我们认为我们做错了什么。也许我们从服务器发送的 header 与数据到下载任务切换不兼容。也许我们没有正确使用 AFNetworking。

有没有人知道这种行为?我们是否应该在 URLSession:dataTask:didReceiveData: 中捕获初始响应主体?在任务切换到下载任务之前?

真正奇怪的是,如果文件小于 16K,则没有问题。整个文件被写入。

对文件的所有请求都从数据任务开始,然后转换为下载任务。

最佳答案

我可以转换 NSURLSessionDataTaskNSURLSessionBackgroundTask ,并且 (a) 文件大小合适,并且 (b) 我没有看到任何对 didReceiveData 的调用.

我注意到您正在调用 super这些各种委托(delegate)方法的实例。这就有点好奇了。我想知道你的 super didReceiveResponse 的实现正在调用完成处理程序本身,导致您两次调用此完成处理程序。值得注意的是,如果我故意调用处理程序两次,一次是使用 NSURLSessionResponseAllow,我可以重现您的问题。在我再次调用 NSURLSessionResponseBecomeDownload 之前.

确保你只调用你的完成处理程序一次,并且非常小心你在这些 super 中的内容。方法(或只是完全删除对它们的引用)。

关于ios - AFNetworking 2.0 将 NSURLSessionDataTask 转换为 NSURLSessionDownloadTask 不会将所有文件数据写入磁盘,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21980848/

相关文章:

在 C 中解析网络数据包的正确方法

ios - 将 IB 创建的约束复制到另一个 UIView

ios - 如何更新 PFUser.currentUser() 对象中的字段?

objective-c - 缩略图存储策略

objective-c - 多 View 上的 UIPanGestureRecognizer - Objective-c

security - 带有metasploit框架的python脚本

ios - AVPlayer seekTo 不准确

ios - UITabBarController 在 iPhone 5 模拟器上没有响应 : 4-inch retina display

ios - 无法访问资源包

networking - 使用 gopacket 发送 UDP 数据包