在iOS10上恢复NSUrlSession

Ale*_*nov 35 ios nsurlsession nsurlsessiondownloadtask ios10

iOS 10将很快发布,因此值得测试应用程序与它的兼容性.在此类测试期间,我们发现我们的应用程序无法在iOS10上恢复后台下载.在以前的版本上运行良好的代码在模拟器和设备上都不适用于新版本.

我没有将我们的代码减少到最小的工作测试用例,而是在互联网上搜索NSUrlSession教程并测试它们.行为是相同的:恢复工作在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. 构建它并在iOS 10下启动.搜索一些内容,例如"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.
Run Code Online (Sandbox Code Playgroud)

更多场景:

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

Url session completed with error: Error Domain=NSURLErrorDomain Code=-1002 "unsupported URL" UserInfo={NSLocalizedDescription=unsupported URL} {
    NSLocalizedDescription = "unsupported URL";
}
Run Code Online (Sandbox Code Playgroud)

当网络关闭时,下载永远不会在网络再次启动时恢复.暂停的其他用例(例如重新启动)也不起作用.

补充调查:

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

如何检查NSData blob是否有效作为NSURLSessionDownloadTask的resumeData?

但目标文件已到位.虽然resumeData格式已更改,现在文件名存储在NSURLSessionResumeInfoTempFileName中,您必须将NSTemporaryDirectory()附加到它.

除此之外,我已经向苹果填写了一份错误报告,但他们还没有回复.

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

是否在所有其他应用程序中恢复了NSUrlSession?可以在应用程序端修复吗?

Mou*_*ian 39

这个问题起源于currentRequest和originalRequest NSKeyArchived编码的异常根"NSKeyedArchiveRootObjectKey"而不是NSKeyedArchiveRootObjectKey常量,它是字面上的"root"和NSURL(Mutable)请求的编码过程中的一些其他行为不端.

我在beta 1中检测到并提交了一个错误(第27144153号,如果你想复制的话).甚至我发送了一封电子邮件给"Quinn the Eskimo"(eskimo1 at apple dot com),他是NSURLSession团队的支持者,确认他们收到了这封邮件,他说他们得到了这个并且知道问题.

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

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

更新3: Quinn说这个问题已在iOS 10.2中得到解决,您可以继续使用此代码与iOS 10.0和10.1兼容,并且它可以在没有任何问题的情况下使用新版本.

(对于Swift 3代码,请在下面滚动,对于Objective C,请参阅leavesstar帖子,但我没有测试过)

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
    }
}
Run Code Online (Sandbox Code Playgroud)

斯威夫特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
    }
}
Run Code Online (Sandbox Code Playgroud)

  • @Mousavian,再次感谢您对逆向工程的辉煌和平. (3认同)
  • 嘿@George,HalfTunes的问题是它依赖originalRequest属性来更新UI.虽然此属性将为nil,但任务将正确恢复,HalfTunes无法正确更新UI进度.您可以更改项目以使用taskIdentifier而不是originalRequest来识别url会话委托方法中的任务,您将看到一切正常. (3认同)
  • 这意味着没有人可以在iOS 10上进行后台上传?那太疯狂了不是吗? (3认同)
  • 穆萨维安,非常感谢你. (2认同)

use*_*802 5

关于unsupported URL错误和丢失的resumeData在网络上失败或其他失败的部分问题,我已经用Apple记录了TSI,以及Quinn的最新回复:

首先,您所看到的行为肯定是NSURLSession中的一个错误.我们希望在将来的软件更新中解决此问题.正在跟踪这项工作.我没有任何信息可以分享修复程序何时发送给普通的iOS用户.

至于解决方法,昨天我详细研究了这个问题,现在我完全理解了失败.IMO有一种合理的方法可以解决这个问题,但我需要先将我的想法运行到NSURLSession工程之后再分享它们.我希望在接下来的一两天内收到他们的回复.请等待.

我会在发生更新时发布更新,但我确信这给了人们一些希望,至少苹果正在考虑这个问题.

(Mousavian解决方案的大量道具,用于暂停/恢复行为)

更新:

来自奎因,

确实.自从我们上次谈话以来(我很抱歉我花了这么长时间才回到你这里;我最近被埋没在事件中)我代表其他一些开发人员进一步挖掘了这一点并发现:A.此问题出现在两个上下文中,其特征是NSURLErrorCannotWriteToFile和NSURLErrorUnsupportedURL错误.B.我们可以解决第一个问题但不是第二个问题.我附上了我的文档的更新,填写了详细信息.不幸的是,我们无法找到第二种症状的解决方法.前进的唯一方法是让iOS Engineering修复该bug.我们希望这将在iOS 10软件更新中发生,但我没有任何具体的细节可以分享(除了这个修复看起来它错过了10.1总线) - :

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

NSURLErrorCannotWriteToFile问题由上述Mousavian的代码处理.

另一个更新:

Quinn确认了最新的10.2测试版试图解决这些问题.

这看过10.2了吗?

是.此问题的修复程序包含在第一个10.2测试版中.我曾与之合作的一些开发人员报告说这个补丁已经停滞不前,但我仍然建议您在最新的测试版(目前是iOS 10.2 beta 2,14C5069c)上自行试用.如果你遇到任何障碍,请告诉我.