在 iOS10 恢复 NSUrlSession
Resume NSUrlSession on iOS10
iOS 10 即将发布,因此值得测试应用程序与它的兼容性。在此类测试中,我们发现我们的应用无法在 iOS10 上恢复后台下载。在旧版本上运行良好的代码不适用于新版本,无论是在模拟器上还是在设备上。
我没有将我们的代码减少到最小的工作测试用例,而是在互联网上搜索 NSUrlSession 教程并对其进行了测试。行为是相同的:恢复在 iOS 的先前版本上工作,但在 10th.
上中断。
重现步骤:
- 下载一个项目表单NSUrlSession教程
https://www.raywenderlich.com/110458/nsurlsession-tutorial-getting-started
- 直接link:
http://www.raywenderlich.com/wp-content/uploads/2016/01/HalfTunes-Final.zip
- 构建它并在 iOS 下启动 10. 搜索一些东西,例如
"swift"。开始下载然后点击 "Pause" 然后 "Resume"
预期结果:
下载已恢复。您可以检查它如何与 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";
}
当网络关闭时,当网络再次启动时下载永远不会恢复。其他暂停用例(例如重启)也不起作用。
补充调查:
我尝试使用
中建议的代码检查返回的 resumeData 是否有效
How can I check that an NSData blob is valid as resumeData for an NSURLSessionDownloadTask?
但目标文件已到位。尽管 resumeData 格式已更改,现在文件名存储在 NSURLSessionResumeInfoTempFileName 中,您必须向其附加 NSTemporaryDirectory()。
此外,我已经向苹果公司提交了错误报告,但他们还没有回复。
问题(关于生命、宇宙和一切):
NSUrlSession 的恢复是否在所有其他应用程序中损坏?可以在应用端修复吗?
这个问题是由于 currentRequest 和 originalRequest NSKeyArchived 使用不寻常的 "NSKeyedArchiveRootObjectKey" 根编码而不是 NSKeyedArchiveRootObjectKey 常量(字面意思是 "root")和 NSURL(Mutable)Request 编码过程中的其他一些不当行为引起的。
我在 beta 1 中检测到并提交了一个错误(编号 27144153,以防您想要复制)。我什至给 "Quinn the Eskimo" (eskimo1 at apple dot com) 发了一封电子邮件,他是 NSURLSession 团队的支持人员,确认他们收到了邮件,他说他们收到了,并且知道这个问题。
更新: 我终于想出了解决这个问题的方法。将数据提供给 correctResumeData() 函数,它将 return 可用的简历数据
更新 2: 您可以使用 NSURLSession.correctedDownloadTaskWithResumeData() / URLSession.correctedDownloadTask(withResumeData:) 函数获取具有正确 originalRequest 和 currentRequest 变量的任务
更新 3: Quinn 说此问题已在 iOS 10.2 中解决,您可以继续使用此代码以与 iOS 10.0 和 10.1 兼容它将毫无问题地与新版本一起使用。
(对于 Swift 3 代码,请在下方滚动,对于 Objective C 请参阅 但我没有测试它)
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
错误和在网络故障或其他故障时丢失 resumeData 的部分问题,我已经向 Apple 记录了 TSI,Quinn 的最新回复:
Firstly, the behaviour you’re seeing is definitely a bug in
NSURLSession. We hope to fix this problem in a future software
update. That work is being tracked by . I
don’t have any info to share as to when the fix will ship to normal
iOS users.
As to workarounds, I dug into this issue in detail yesterday and I now
fully understand the failure. IMO there is a reasonable way to work
around this but I need to run my ideas past NSURLSession engineering
before I can share them. I hope to hear back from them in the next
day or two. Please stand by.
我会 post 更新,但我相信这会给人们一些希望,至少苹果正在关注这个问题。
(suspend/resume 行为的 Mousavian 解决方法的大量支持)
更新:
来自奎因,
Indeed. Since we last talked (and I apologise that I’ve taken so long to get back to you here; I’ve been buried in incidents recently) I’ve dug into this further on behalf of some other developers and discovered that:
A. This issue manifests in two contexts, characterised by the NSURLErrorCannotWriteToFile and NSURLErrorUnsupportedURL errors.
B. We can work around the first but not the second.
I’ve attached an update to my doc that fills in the details.
Unfortunately we were unable to come up with a workaround for the second symptom. The only way forward is for iOS Engineering to fix that bug. We hope that will happen in an iOS 10 software update but I don’t have any concrete details to share (other than that this fix looks like it missed the 10.1 bus)-:
因此,不幸的是,unsupported URL
问题没有解决方法,我们必须等待错误被修复。
NSURLErrorCannotWriteToFile
问题由上面的 Mousavian 代码处理。
另一项更新:
Quinn 确认最新的 10.2 beta 尝试解决这些问题。
Did this get a look in on 10.2?
是的。这个问题的修复包含在第一个 10.2 beta 中。一种
与我合作过的许多开发人员都报告说这个补丁
已经卡住了,但我仍然建议您在
最新测试版(目前 iOS 10.2 beta 2, 14C5069c)。如果你让我知道
碰到任何障碍。
这里是 Objective - Mousavian 答案的 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;
}
我没有为 NSURLSession 创建类别,我只是在 My Singleton 中创建。
这是创建 NSURLSessionDownloadTask 的代码:
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"];
}
}
iOS 10 即将发布,因此值得测试应用程序与它的兼容性。在此类测试中,我们发现我们的应用无法在 iOS10 上恢复后台下载。在旧版本上运行良好的代码不适用于新版本,无论是在模拟器上还是在设备上。
我没有将我们的代码减少到最小的工作测试用例,而是在互联网上搜索 NSUrlSession 教程并对其进行了测试。行为是相同的:恢复在 iOS 的先前版本上工作,但在 10th.
上中断。重现步骤:
- 下载一个项目表单NSUrlSession教程 https://www.raywenderlich.com/110458/nsurlsession-tutorial-getting-started
- 直接link: http://www.raywenderlich.com/wp-content/uploads/2016/01/HalfTunes-Final.zip
- 构建它并在 iOS 下启动 10. 搜索一些东西,例如 "swift"。开始下载然后点击 "Pause" 然后 "Resume"
预期结果:
下载已恢复。您可以检查它如何与 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";
}
当网络关闭时,当网络再次启动时下载永远不会恢复。其他暂停用例(例如重启)也不起作用。
补充调查:
我尝试使用
中建议的代码检查返回的 resumeData 是否有效How can I check that an NSData blob is valid as resumeData for an NSURLSessionDownloadTask?
但目标文件已到位。尽管 resumeData 格式已更改,现在文件名存储在 NSURLSessionResumeInfoTempFileName 中,您必须向其附加 NSTemporaryDirectory()。
此外,我已经向苹果公司提交了错误报告,但他们还没有回复。
问题(关于生命、宇宙和一切):
NSUrlSession 的恢复是否在所有其他应用程序中损坏?可以在应用端修复吗?
这个问题是由于 currentRequest 和 originalRequest NSKeyArchived 使用不寻常的 "NSKeyedArchiveRootObjectKey" 根编码而不是 NSKeyedArchiveRootObjectKey 常量(字面意思是 "root")和 NSURL(Mutable)Request 编码过程中的其他一些不当行为引起的。
我在 beta 1 中检测到并提交了一个错误(编号 27144153,以防您想要复制)。我什至给 "Quinn the Eskimo" (eskimo1 at apple dot com) 发了一封电子邮件,他是 NSURLSession 团队的支持人员,确认他们收到了邮件,他说他们收到了,并且知道这个问题。
更新: 我终于想出了解决这个问题的方法。将数据提供给 correctResumeData() 函数,它将 return 可用的简历数据
更新 2: 您可以使用 NSURLSession.correctedDownloadTaskWithResumeData() / URLSession.correctedDownloadTask(withResumeData:) 函数获取具有正确 originalRequest 和 currentRequest 变量的任务
更新 3: Quinn 说此问题已在 iOS 10.2 中解决,您可以继续使用此代码以与 iOS 10.0 和 10.1 兼容它将毫无问题地与新版本一起使用。
(对于 Swift 3 代码,请在下方滚动,对于 Objective C 请参阅
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
错误和在网络故障或其他故障时丢失 resumeData 的部分问题,我已经向 Apple 记录了 TSI,Quinn 的最新回复:
Firstly, the behaviour you’re seeing is definitely a bug in NSURLSession. We hope to fix this problem in a future software update. That work is being tracked by . I don’t have any info to share as to when the fix will ship to normal iOS users.
As to workarounds, I dug into this issue in detail yesterday and I now fully understand the failure. IMO there is a reasonable way to work around this but I need to run my ideas past NSURLSession engineering before I can share them. I hope to hear back from them in the next day or two. Please stand by.
我会 post 更新,但我相信这会给人们一些希望,至少苹果正在关注这个问题。
(suspend/resume 行为的 Mousavian 解决方法的大量支持)
更新:
来自奎因,
Indeed. Since we last talked (and I apologise that I’ve taken so long to get back to you here; I’ve been buried in incidents recently) I’ve dug into this further on behalf of some other developers and discovered that: A. This issue manifests in two contexts, characterised by the NSURLErrorCannotWriteToFile and NSURLErrorUnsupportedURL errors. B. We can work around the first but not the second. I’ve attached an update to my doc that fills in the details. Unfortunately we were unable to come up with a workaround for the second symptom. The only way forward is for iOS Engineering to fix that bug. We hope that will happen in an iOS 10 software update but I don’t have any concrete details to share (other than that this fix looks like it missed the 10.1 bus)-:
因此,不幸的是,unsupported URL
问题没有解决方法,我们必须等待错误被修复。
NSURLErrorCannotWriteToFile
问题由上面的 Mousavian 代码处理。
另一项更新:
Quinn 确认最新的 10.2 beta 尝试解决这些问题。
Did this get a look in on 10.2?
是的。这个问题的修复包含在第一个 10.2 beta 中。一种 与我合作过的许多开发人员都报告说这个补丁 已经卡住了,但我仍然建议您在 最新测试版(目前 iOS 10.2 beta 2, 14C5069c)。如果你让我知道 碰到任何障碍。
这里是 Objective - Mousavian 答案的 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;
}
我没有为 NSURLSession 创建类别,我只是在 My Singleton 中创建。 这是创建 NSURLSessionDownloadTask 的代码:
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"];
}
}