【问题标题】:Resume NSUrlSession on iOS10在 iOS10 上恢复 NSUrlSession
【发布时间】:2017-01-13 17:56:26
【问题描述】:

iOS 10 即将发布,因此值得测试应用程序的兼容性。在这样的测试中,我们发现我们的应用程序无法在 iOS10 上恢复后台下载。在旧版本上运行良好的代码不适用于新版本,无论是在模拟器上还是在设备上。

我没有将我们的代码简化为最小的工作测试用例,而是在互联网上搜索了 NSUrlSession 教程并对其进行了测试。行为是相同的:在 iOS 的早期版本上恢复工作,但在 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 下构建并启动。搜索某些内容,例如 “迅速”。开始下载,然后点击“暂停”,然后点击“继续”

预期结果:

继续下载。您可以查看它如何与 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 是否中断?可以固定在应用端吗?

【问题讨论】:

    标签: ios nsurlsession ios10 nsurlsessiondownloadtask


    【解决方案1】:

    这个问题是由 currentRequest 和 originalRequest NSKeyArchived 编码的一个不寻常的根“NSKeyedArchiveRootObjectKey”而不是 NSKeyedArchiveRootObjectKey 常量(字面意思是“根”)和 NSURL(Mutable)Request 的编码过程中的一些其他不当行为引起的。

    我在 beta 1 中检测到了这一点并提交了一个错误(编号 27144153,以防您想要复制)。甚至我也向 NSURLSession 团队的支持人员“Quinn the Eskimo”(apple dot com 的爱斯基摩人)发送了一封电子邮件,以确认他们收到了它,他说他们收到了并且知道问题。

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

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

    更新 3: Quinn 说此问题已在 iOS 10.2 中解决,您可以继续使用此代码与 iOS 10.0 和 10.1 兼容,并且它可以在新版本中正常工作。 p>

    (对于 Swift 3 代码,请向下滚动,对于 Objective C,请参阅 leavesstar post,但我没有对其进行测试)

    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
        }
    }
    

    斯威夫特 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
        }
    }
    

    【讨论】:

    • 啊,测试版操作系统的乐趣。
    • @Mousavian,再次感谢您为逆向工程带来的美好和平。
    • @Mousavian 嗯,这仍然对我不起作用。当我应用此修复程序时,下载不会失败,但也不会恢复,就像卡住了一样。此外,我仍然看到来自 [NSKeyedUnarchiver initForReadingData:] 的那两个日志语句,表明传递给初始化程序的数据为 NULL...您能否确认此修复仍然适用于使用 HalfTunes 教程的您?
    • 嘿@George,HalfTunes 的问题在于它依赖 originalRequest 属性来更新 UI。虽然此属性将为 nil,但任务将正确恢复,HalfTunes 无法正确更新 UI 进度。您可以将项目更改为使用 taskIdentifier 而不是 originalRequest 来识别 url 会话委托方法中的任务,您会看到一切都很好。
    • 这意味着没有人可以在 iOS 10 上进行后台上传?这很疯狂不是吗?
    【解决方案2】:

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

    首先,您看到的行为绝对是一个错误 NSURL会话。我们希望在未来的软件中解决这个问题 更新。这项工作正在被 .一世 没有任何信息可以分享修复何时可以正常发布 iOS 用户。

    至于解决方法,我昨天详细研究了这个问题,现在我 完全理解失败。 IMO有一种合理的工作方式 围绕这个,但我需要通过 NSURLSession 工程来运行我的想法 在我可以分享它们之前。我希望在下一次收到他们的回音 一两天。请稍候。

    我会在更新发生时发布更新,但我相信这会给人们一些希望,至少苹果正在研究这个问题。

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

    更新:

    来自奎因,

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

    因此,很遗憾,unsupported URL 问题无法解决,我们必须等待错误修复。

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

    另一个更新:

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

    这是否在 10.2 上有所体现?

    是的。此问题的修复程序包含在第一个 10.2 测试版中。一种 与我合作过的许多开发人员报告说这个补丁 卡住了,但我仍然建议您在 最新的测试版(目前是 iOS 10.2 beta 2, 14C5069c)。让我知道你是否 击中任何障碍。

    【讨论】:

    • 急切等待更新,请随时关注我们。
    • 有趣的部分是我直接通过电子邮件告诉他这个错误以及如何从 beta 1 开始修复。
    • 最近怎么样? @user1012802
    • 如果unsupported URL 有解决方法,我会在奎因之前发现它!我已经检查过了,发现它删除了临时文件,否则我会实现一个方法来手动创建一个新的 resumeData。
    【解决方案3】:

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

    它在 iOS 9.3.5(设备)和 iOS 10.1(模拟器)中运行良好。

    首先以穆萨维安的方式更正简历数据

     - (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"];
            }
        }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-06-10
      • 1970-01-01
      • 2018-03-02
      • 2013-10-11
      • 2016-07-15
      相关资源
      最近更新 更多