web-dev-qa-db-ja.com

iOS10でNSUrlSessionを再開する

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で起動します。たとえば、「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";
}

ネットワークがシャットダウンされ、ネットワークが再びアップしたときにダウンロードが回復しない場合。再起動など、一時停止を伴うその他のユースケースも機能しません。

追加調査:

で提案されたコードを使用して、返されたresumeDataが有効かどうかを確認しようとしました

NSURLSessionDownloadTaskのresumeDataとしてNSData blobが有効であることを確認するにはどうすればよいですか?

ただし、ターゲットファイルは配置されています。 resumeDataの形式が変更され、ファイル名がNSURLSessionResumeInfoTempFileNameに保存され、NSTemporaryDirectory()を追加する必要がありますが。

それに加えて、Appleにバグレポートを記入しましたが、まだ返信していません。

(生命、宇宙、そしてすべての)質問:

NSUrlSessionの再開は他のすべてのアプリで壊れていますか?アプリケーション側で修正できますか?

35
Alexey Guseynov

この問題は、文字通り「ルート」であるNSKeyedArchiveRootObjectKey定数ではなく、「NSKeyedArchiveRootObjectKey」の異常なルートでエンコードされたcurrentRequestおよびoriginalRequest NSKeyArchivedから発生しました。

ベータ1でそれを検出し、バグを報告しました(重複したい場合は、番号27144153)。 NSURLSessionチームのサポート担当者である「Quinn the Eskimo」(Apple dot comのeskimo1)にメールを送信して、受け取ったことを確認し、彼はそれを受け取って問題を認識していると言いました。

UPDATE:最後に、この問題を解決する方法を見つけました。 correctResumeData()関数にデータを渡すと、使用可能な再開データが返されます

UPDATE 2:NSURLSession.correctedDownloadTaskWithResumeData()/ URLSession.correctedDownloadTask(withResumeData :)関数を使用して、正しいoriginalRequest変数とcurrentRequest変数を持つタスクを取得できます。

更新3:クインは、この問題はiOS 10.2で解決されたと言います。問題のない新しいバージョン。

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

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

unsupported URLエラーに関する質問の一部とネットワーク障害またはその他の障害でresumeDataを失ったことに関して、AppleにTSIを記録し、Quinnからの最新の応答を記録しました。

まず、表示されている動作は間違いなくNSURLSessionのバグです。この問題は、将来のソフトウェアアップデートで修正する予定です。その作業はによって追跡されています。修正プログラムが通常のiOSユーザーにいつ出荷されるかについて共有する情報がありません。

回避策については、昨日この問題を詳細に掘り下げましたが、今では失敗を完全に理解しています。 IMOにはこれを回避する合理的な方法がありますが、共有する前にNSURLSessionエンジニアリングを過ぎてアイデアを実行する必要があります。翌日か二日のうちに彼らから返事をもらいたい。お待ちください。

アップデートが発生したらすぐに投稿しますが、少なくともこの問題がAppleによって検討されているという希望を人々に与えてくれると思います。

(一時停止/再開動作のムーサビアの回避策に対する大規模な小道具)

更新:

クインから、

確かに。私たちが最後に話して以来(そして、ここに戻るのに非常に時間がかかったことをおizeびします;最近、事件に埋もれました)他の開発者に代わってこれをさらに掘り下げて、それを発見しました:A.この問題は、NSURLErrorCannotWriteToFileおよびNSURLErrorUnsupportedURLエラーによって特徴付けられる2つのコンテキストで現れます。 B.最初の問題は回避できますが、2番目の問題は回避できません。詳細を記入する更新をドキュメントに添付しました。残念ながら、2番目の症状の回避策を思い付くことができませんでした。今後の唯一の方法は、iOSエンジニアリングがそのバグを修正することです。 iOS 10ソフトウェアのアップデートでそれが実現することを願っていますが、具体的な詳細を共有する必要はありません(この修正が10.1バスを逃したように見えること以外は):

したがって、残念ながら、unsupported URLの問題は回避策がなく、バグが修正されるのを待つ必要があります。

NSURLErrorCannotWriteToFileの問題は、上記のMousavianのコードによって処理されます。

別の更新:

Quinnは、これらの問題を解決するための最新の10.2ベータ版の試みを確認しました。

これは10.2をご覧になりましたか?

はい。この問題の修正は、最初の10.2ベータに含まれていました。私が協力した多くの開発者がこのパッチがスタックしていると報告していますが、最新のベータ版(現在はiOS 10.2ベータ2、14C5069c)で試してみることをお勧めします。ひっかかった場合はお知らせください。

5
user1012802

ここに目的があります-Mousavianの答えの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のカテゴリは作成しませんでした。MySingletonで作成します。 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"];
        }
    }
1
leavesster