在iOS10上继续NSUrlSession

iOS 10即将发布,因此值得testing与其兼容的应用程序。 在这样的testing中,我们发现我们的应用程序无法恢复iOS10上的后台下载。 在以前版本上运行良好的代码在仿真器和设备上都无法运行。

我没有将我们的代码减less到最小工作testing用例,而是在网上search了NSUrlSession教程并进行了testing。 行为是一样的:在iOS的previos版本上重新开始工作,但在10日中断。

重现步骤:

  1. 下载一个项目表单NSUrlSession教程https://www.raywenderlich.com/110458/nsurlsession-tutorial-getting-started
  2. 直接链接: http : //www.raywenderlich.com/wp-content/uploads/2016/01/HalfTunes-Final.zip
  3. build立它并在iOS 10下启动。search一些东西,例如“swift”。 开始下载,然后点击“暂停”,然后“恢复”

预期成绩:

下载被恢复。 你可以检查它是如何与iOS10以前的版本一起工作的。

实际结果:

下载失败。 在xcode控制台中,您可以看到:

2016-09-02 16:11:24.913 HalfTunes[35205:2279228] *** -[NSKeyedUnarchiver initForReadingWithData:]: data is NULL 2016-09-02 16:11:24.913 HalfTunes[35205:2279228] *** -[NSKeyedUnarchiver initForReadingWithData:]: data is NULL 2016-09-02 16:11:24.913 HalfTunes[35205:2279228] Invalid resume data for background download. Background downloads must use http or https and must download to an accessible file. 

更多场景:

如果您在下载文件时激活了离线模式,则可以获得

 Url session completed with error: Error Domain=NSURLErrorDomain Code=-1002 "unsupported URL" UserInfo={NSLocalizedDescription=unsupported URL} { NSLocalizedDescription = "unsupported URL"; } 

当networkingclosures时,networking重新启动时下载不会恢复。 其他暂停的使用情况,例如重启,也不起作用。

追加调查:

我试图检查是否返回resumeData是有效的使用代码build议

我怎样才能检查一个NSData的Blob是有效的NSURLSessionDownloadTask的resumeData?

但是目标文件已经到位。 虽然resumeData格式已经改变,现在文件名存储在NSURLSessionResumeInfoTempFileName中,你必须在它附加NSTemporaryDirectory()。

除此之外,我已经向苹果提交了一个错误报告,但是他们还没有回复。

(生命,宇宙和一切)的问题:

所有其他应用程序中的NSUrlSession是否恢复正常? 它可以固定在应用程序端?

这个问题来自currentRequest和originalRequest NSKeyArchived编码与“NSKeyedArchiveRootObjectKey”而不是NSKeyedArchiveRootObjectKey常量,这是字面上的“根”和NSURL(可变)请求的编码过程中的一些其他不正常的根。

我在beta 1中发现了这个错误,并提交了一个错误(如果你想重复的话,编号为27144153)。 即使我给NSURLSession团队的支持人员发送了一封电子邮件给“Quinn the Eskimo”(eskimo1在apple dot com),以确认他们收到了这个消息,他说他们知道了,并且知道问题。

更新:我终于想出了如何解决这个问题。 将数据提供给correctResumeData()函数,它将返回可用的恢复数据

更新2:您可以使用NSURLSession.correctedDownloadTaskWithResumeData()/ URLSession.correctedDownloadTask(withResumeData :)函数来获取具有正确的originalRequest和currentRequestvariables的任务

更新3: Quinn说这个问题已经在iOS 10.2中解决了,你可以继续使用这个代码来兼容iOS 10.0和10.1,它可以在没有任何问题的情况下使用新版本。

(对于Swift 3代码,请在下面滚动,对于Objective C请参阅leavesstarpost,但是我没有testing它)

Swift 2.3:

 func correctRequestData(data: NSData?) -> NSData? { guard let data = data else { return nil } // return the same data if it's correct if NSKeyedUnarchiver.unarchiveObjectWithData(data) != nil { return data } guard let archive = (try? NSPropertyListSerialization.propertyListWithData(data, options: [.MutableContainersAndLeaves], format: nil)) as? NSMutableDictionary else { return nil } // Rectify weird __nsurlrequest_proto_props objects to $number pattern var k = 0 while archive["$objects"]?[1].objectForKey("$\(k)") != nil { k += 1 } var i = 0 while archive["$objects"]?[1].objectForKey("__nsurlrequest_proto_prop_obj_\(i)") != nil { let arr = archive["$objects"] as? NSMutableArray if let dic = arr?[1] as? NSMutableDictionary, let obj = dic["__nsurlrequest_proto_prop_obj_\(i)"] { dic.setObject(obj, forKey: "$\(i + k)") dic.removeObjectForKey("__nsurlrequest_proto_prop_obj_\(i)") arr?[1] = dic archive["$objects"] = arr } i += 1 } if archive["$objects"]?[1].objectForKey("__nsurlrequest_proto_props") != nil { let arr = archive["$objects"] as? NSMutableArray if let dic = arr?[1] as? NSMutableDictionary, let obj = dic["__nsurlrequest_proto_props"] { dic.setObject(obj, forKey: "$\(i + k)") dic.removeObjectForKey("__nsurlrequest_proto_props") arr?[1] = dic archive["$objects"] = arr } } // Rectify weird "NSKeyedArchiveRootObjectKey" top key to NSKeyedArchiveRootObjectKey = "root" if archive["$top"]?.objectForKey("NSKeyedArchiveRootObjectKey") != nil { archive["$top"]?.setObject(archive["$top"]?["NSKeyedArchiveRootObjectKey"], forKey: NSKeyedArchiveRootObjectKey) archive["$top"]?.removeObjectForKey("NSKeyedArchiveRootObjectKey") } // Reencode archived object let result = try? NSPropertyListSerialization.dataWithPropertyList(archive, format: NSPropertyListFormat.BinaryFormat_v1_0, options: NSPropertyListWriteOptions()) return result } func getResumeDictionary(data: NSData) -> NSMutableDictionary? { var iresumeDictionary: NSMutableDictionary? = nil // In beta versions, resumeData is NSKeyedArchive encoded instead of plist if #available(iOS 10.0, OSX 10.12, *) { var root : AnyObject? = nil let keyedUnarchiver = NSKeyedUnarchiver(forReadingWithData: data) do { root = try keyedUnarchiver.decodeTopLevelObjectForKey("NSKeyedArchiveRootObjectKey") ?? nil if root == nil { root = try keyedUnarchiver.decodeTopLevelObjectForKey(NSKeyedArchiveRootObjectKey) } } catch {} keyedUnarchiver.finishDecoding() iresumeDictionary = root as? NSMutableDictionary } if iresumeDictionary == nil { do { iresumeDictionary = try NSPropertyListSerialization.propertyListWithData(data, options: [.MutableContainersAndLeaves], format: nil) as? NSMutableDictionary; } catch {} } return iresumeDictionary } func correctResumeData(data: NSData?) -> NSData? { let kResumeCurrentRequest = "NSURLSessionResumeCurrentRequest" let kResumeOriginalRequest = "NSURLSessionResumeOriginalRequest" guard let data = data, let resumeDictionary = getResumeDictionary(data) else { return nil } resumeDictionary[kResumeCurrentRequest] = correctRequestData(resumeDictionary[kResumeCurrentRequest] as? NSData) resumeDictionary[kResumeOriginalRequest] = correctRequestData(resumeDictionary[kResumeOriginalRequest] as? NSData) let result = try? NSPropertyListSerialization.dataWithPropertyList(resumeDictionary, format: NSPropertyListFormat.XMLFormat_v1_0, options: NSPropertyListWriteOptions()) return result } extension NSURLSession { func correctedDownloadTaskWithResumeData(resumeData: NSData) -> NSURLSessionDownloadTask { let kResumeCurrentRequest = "NSURLSessionResumeCurrentRequest" let kResumeOriginalRequest = "NSURLSessionResumeOriginalRequest" let cData = correctResumeData(resumeData) ?? resumeData let task = self.downloadTaskWithResumeData(cData) // a compensation for inability to set task requests in CFNetwork. // While you still get -[NSKeyedUnarchiver initForReadingWithData:]: data is NULL error, // this section will set them to real objects if let resumeDic = getResumeDictionary(cData) { if task.originalRequest == nil, let originalReqData = resumeDic[kResumeOriginalRequest] as? NSData, let originalRequest = NSKeyedUnarchiver.unarchiveObjectWithData(originalReqData) as? NSURLRequest { task.setValue(originalRequest, forKey: "originalRequest") } if task.currentRequest == nil, let currentReqData = resumeDic[kResumeCurrentRequest] as? NSData, let currentRequest = NSKeyedUnarchiver.unarchiveObjectWithData(currentReqData) as? NSURLRequest { task.setValue(currentRequest, forKey: "currentRequest") } } return task } } 

Swift 3:

 func correct(requestData data: Data?) -> Data? { guard let data = data else { return nil } if NSKeyedUnarchiver.unarchiveObject(with: data) != nil { return data } guard let archive = (try? PropertyListSerialization.propertyList(from: data, options: [.mutableContainersAndLeaves], format: nil)) as? NSMutableDictionary else { return nil } // Rectify weird __nsurlrequest_proto_props objects to $number pattern var k = 0 while ((archive["$objects"] as? NSArray)?[1] as? NSDictionary)?.object(forKey: "$\(k)") != nil { k += 1 } var i = 0 while ((archive["$objects"] as? NSArray)?[1] as? NSDictionary)?.object(forKey: "__nsurlrequest_proto_prop_obj_\(i)") != nil { let arr = archive["$objects"] as? NSMutableArray if let dic = arr?[1] as? NSMutableDictionary, let obj = dic["__nsurlrequest_proto_prop_obj_\(i)"] { dic.setObject(obj, forKey: "$\(i + k)" as NSString) dic.removeObject(forKey: "__nsurlrequest_proto_prop_obj_\(i)") arr?[1] = dic archive["$objects"] = arr } i += 1 } if ((archive["$objects"] as? NSArray)?[1] as? NSDictionary)?.object(forKey: "__nsurlrequest_proto_props") != nil { let arr = archive["$objects"] as? NSMutableArray if let dic = arr?[1] as? NSMutableDictionary, let obj = dic["__nsurlrequest_proto_props"] { dic.setObject(obj, forKey: "$\(i + k)" as NSString) dic.removeObject(forKey: "__nsurlrequest_proto_props") arr?[1] = dic archive["$objects"] = arr } } /* I think we have no reason to keep this section in effect for item in (archive["$objects"] as? NSMutableArray) ?? [] { if let cls = item as? NSMutableDictionary, cls["$classname"] as? NSString == "NSURLRequest" { cls["$classname"] = NSString(string: "NSMutableURLRequest") (cls["$classes"] as? NSMutableArray)?.insert(NSString(string: "NSMutableURLRequest"), at: 0) } }*/ // Rectify weird "NSKeyedArchiveRootObjectKey" top key to NSKeyedArchiveRootObjectKey = "root" if let obj = (archive["$top"] as? NSMutableDictionary)?.object(forKey: "NSKeyedArchiveRootObjectKey") as AnyObject? { (archive["$top"] as? NSMutableDictionary)?.setObject(obj, forKey: NSKeyedArchiveRootObjectKey as NSString) (archive["$top"] as? NSMutableDictionary)?.removeObject(forKey: "NSKeyedArchiveRootObjectKey") } // Reencode archived object let result = try? PropertyListSerialization.data(fromPropertyList: archive, format: PropertyListSerialization.PropertyListFormat.binary, options: PropertyListSerialization.WriteOptions()) return result } func getResumeDictionary(_ data: Data) -> NSMutableDictionary? { // In beta versions, resumeData is NSKeyedArchive encoded instead of plist var iresumeDictionary: NSMutableDictionary? = nil if #available(iOS 10.0, OSX 10.12, *) { var root : AnyObject? = nil let keyedUnarchiver = NSKeyedUnarchiver(forReadingWith: data) do { root = try keyedUnarchiver.decodeTopLevelObject(forKey: "NSKeyedArchiveRootObjectKey") ?? nil if root == nil { root = try keyedUnarchiver.decodeTopLevelObject(forKey: NSKeyedArchiveRootObjectKey) } } catch {} keyedUnarchiver.finishDecoding() iresumeDictionary = root as? NSMutableDictionary } if iresumeDictionary == nil { do { iresumeDictionary = try PropertyListSerialization.propertyList(from: data, options: PropertyListSerialization.ReadOptions(), format: nil) as? NSMutableDictionary; } catch {} } return iresumeDictionary } func correctResumeData(_ data: Data?) -> Data? { let kResumeCurrentRequest = "NSURLSessionResumeCurrentRequest" let kResumeOriginalRequest = "NSURLSessionResumeOriginalRequest" guard let data = data, let resumeDictionary = getResumeDictionary(data) else { return nil } resumeDictionary[kResumeCurrentRequest] = correct(requestData: resumeDictionary[kResumeCurrentRequest] as? Data) resumeDictionary[kResumeOriginalRequest] = correct(requestData: resumeDictionary[kResumeOriginalRequest] as? Data) let result = try? PropertyListSerialization.data(fromPropertyList: resumeDictionary, format: PropertyListSerialization.PropertyListFormat.xml, options: PropertyListSerialization.WriteOptions()) return result } extension URLSession { func correctedDownloadTask(withResumeData resumeData: Data) -> URLSessionDownloadTask { let kResumeCurrentRequest = "NSURLSessionResumeCurrentRequest" let kResumeOriginalRequest = "NSURLSessionResumeOriginalRequest" let cData = correctResumeData(resumeData) ?? resumeData let task = self.downloadTask(withResumeData: cData) // a compensation for inability to set task requests in CFNetwork. // While you still get -[NSKeyedUnarchiver initForReadingWithData:]: data is NULL error, // this section will set them to real objects if let resumeDic = getResumeDictionary(cData) { if task.originalRequest == nil, let originalReqData = resumeDic[kResumeOriginalRequest] as? Data, let originalRequest = NSKeyedUnarchiver.unarchiveObject(with: originalReqData) as? NSURLRequest { task.setValue(originalRequest, forKey: "originalRequest") } if task.currentRequest == nil, let currentReqData = resumeDic[kResumeCurrentRequest] as? Data, let currentRequest = NSKeyedUnarchiver.unarchiveObject(with: currentReqData) as? NSURLRequest { task.setValue(currentRequest, forKey: "currentRequest") } } return task } } 

关于unsupported URL错误和networking上的resumeData丢失问题的部分失败,或者其他失败,我已经logging了苹果的TSI,以及Quinn的最新回应:

首先,你所看到的行为绝对是NSURLSession中的一个错误。 我们希望在未来的软件更新中解决这个问题。 那项工作正在被追踪。 我没有任何信息可以分享到什么时候修复将发布给普通的iOS用户。

关于解决方法,我昨天详细地探讨了这个问题,现在我完全明白了这个失败。 国际海事组织有一个合理的方法来解决这个问题,但我需要通过NSURLSession工程运行我的想法,才能分享他们。 我希望在接下来的一两天内能够收到他们的回信。 请支持。

我会发布更新,但是我确定这给了人们一些希望,至less这个问题正在被苹果看着。

(巨大的道具Mousavian解决暂停/恢复行为的解决方法)

更新:

从奎恩,

确实。 自从我们上次谈到以来(我很抱歉,我花了很长时间才回到你身边,最近我被埋在事件中),我代表其他开发人员进一步挖掘了这一点,并发现:这个问题performance在两个上下文中,以NSURLErrorCannotWriteToFile和NSURLErrorUnsupportedURL错误为特征。 B.我们可以围绕第一而不是第二。 我附加了一个更新我的文档,填写细节。 不幸的是,我们无法想出第二个症状的解决办法。 唯一的出路是iOS工程师修复这个错误。 我们希望这将发生在iOS 10软件更新中,但我没有任何具体的细节可以共享(除了这个修复看起来像是错过了10.1总线):

所以,不幸的是, unsupported URL问题没有解决办法,我们必须等待bug修复。

NSURLErrorCannotWriteToFile问题由上面的Mousavian的代码处理。

其他更新:

奎因证实了最新的10.2 beta试图解决这些问题。

这看起来在10.2吗?

是。 这个问题的解决scheme包含在第一个10.2testing版中。 我曾与许多开发人员报告说,这个补丁已经停滞不前,但我仍然build议你自己尝试一下最新的testing版本(目前iOS 10.2 beta 2,14C5069c)。 让我知道如果你碰到任何障碍。

这是Mousavian的答案的Objective – C代码。

它在iOS 9.3.5(设备)和iOS 10.1(模拟器)工作正常。

首先以Mousavian的方式修正Resume数据

  - (NSData *)correctRequestData:(NSData *)data { if (!data) { return nil; } if ([NSKeyedUnarchiver unarchiveObjectWithData:data]) { return data; } NSMutableDictionary *archive = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListMutableContainersAndLeaves format:nil error:nil]; if (!archive) { return nil; } int k = 0; while ([[archive[@"$objects"] objectAtIndex:1] objectForKey:[NSString stringWithFormat:@"$%d", k]]) { k += 1; } int i = 0; while ([[archive[@"$objects"] objectAtIndex:1] objectForKey:[NSString stringWithFormat:@"__nsurlrequest_proto_prop_obj_%d", i]]) { NSMutableArray *arr = archive[@"$objects"]; NSMutableDictionary *dic = [arr objectAtIndex:1]; id obj; if (dic) { obj = [dic objectForKey:[NSString stringWithFormat:@"__nsurlrequest_proto_prop_obj_%d", i]]; if (obj) { [dic setObject:obj forKey:[NSString stringWithFormat:@"$%d",i + k]]; [dic removeObjectForKey:[NSString stringWithFormat:@"__nsurlrequest_proto_prop_obj_%d", i]]; arr[1] = dic; archive[@"$objects"] = arr; } } i += 1; } if ([[archive[@"$objects"] objectAtIndex:1] objectForKey:@"__nsurlrequest_proto_props"]) { NSMutableArray *arr = archive[@"$objects"]; NSMutableDictionary *dic = [arr objectAtIndex:1]; if (dic) { id obj; obj = [dic objectForKey:@"__nsurlrequest_proto_props"]; if (obj) { [dic setObject:obj forKey:[NSString stringWithFormat:@"$%d",i + k]]; [dic removeObjectForKey:@"__nsurlrequest_proto_props"]; arr[1] = dic; archive[@"$objects"] = arr; } } } id obj = [archive[@"$top"] objectForKey:@"NSKeyedArchiveRootObjectKey"]; if (obj) { [archive[@"$top"] setObject:obj forKey:NSKeyedArchiveRootObjectKey]; [archive[@"$top"] removeObjectForKey:@"NSKeyedArchiveRootObjectKey"]; } NSData *result = [NSPropertyListSerialization dataWithPropertyList:archive format:NSPropertyListBinaryFormat_v1_0 options:0 error:nil]; return result; } - (NSMutableDictionary *)getResumDictionary:(NSData *)data { NSMutableDictionary *iresumeDictionary; if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion >= 10) { NSMutableDictionary *root; NSKeyedUnarchiver *keyedUnarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data]; NSError *error = nil; root = [keyedUnarchiver decodeTopLevelObjectForKey:@"NSKeyedArchiveRootObjectKey" error:&error]; if (!root) { root = [keyedUnarchiver decodeTopLevelObjectForKey:NSKeyedArchiveRootObjectKey error:&error]; } [keyedUnarchiver finishDecoding]; iresumeDictionary = root; } if (!iresumeDictionary) { iresumeDictionary = [NSPropertyListSerialization propertyListWithData:data options:0 format:nil error:nil]; } return iresumeDictionary; } static NSString * kResumeCurrentRequest = @"NSURLSessionResumeCurrentRequest"; static NSString * kResumeOriginalRequest = @"NSURLSessionResumeOriginalRequest"; - (NSData *)correctResumData:(NSData *)data { NSMutableDictionary *resumeDictionary = [self getResumDictionary:data]; if (!data || !resumeDictionary) { return nil; } resumeDictionary[kResumeCurrentRequest] = [self correctRequestData:[resumeDictionary objectForKey:kResumeCurrentRequest]]; resumeDictionary[kResumeOriginalRequest] = [self correctRequestData:[resumeDictionary objectForKey:kResumeOriginalRequest]]; NSData *result = [NSPropertyListSerialization dataWithPropertyList:resumeDictionary format:NSPropertyListXMLFormat_v1_0 options:0 error:nil]; return result; } 

我没有创buildNSURLSession类,我只是在我的单例创build。 这里是创buildNSURLSessionDownloadTask的代码:

  NSData *cData = [self correctResumData:self.resumeData]; if (!cData) { cData = self.resumeData; } self.downloadTask = [self.session downloadTaskWithResumeData:cData]; if ([self getResumDictionary:cData]) { NSDictionary *dict = [self getResumDictionary:cData]; if (!self.downloadTask.originalRequest) { NSData *originalData = dict[kResumeOriginalRequest]; [self.downloadTask setValue:[NSKeyedUnarchiver unarchiveObjectWithData:originalData] forKey:@"originalRequest"]; } if (!self.downloadTask.currentRequest) { NSData *currentData = dict[kResumeCurrentRequest]; [self.downloadTask setValue:[NSKeyedUnarchiver unarchiveObjectWithData:currentData] forKey:@"currentRequest"]; } }