2015-10-02 7 views
28

NSKeyedUnarchiver.decodeObject verursacht einen Absturz/SIGABRT, wenn die ursprüngliche Klasse unbekannt ist. Die einzige Lösung, die ich gesehen habe, um dieses Problem zu erfassen, stammt aus der frühen Geschichte von Swift und erforderte die Verwendung von Objective C (ebenfalls vorveraltete Swift 2-Implementierung von guard, throws, try & catch). Ich könnte die Objective-C-Route herausfinden - aber ich würde lieber eine Swift-only-Lösung verstehen, wenn möglich.Swift nur Weg, um NSKeyedUnarchiver.decodeObject Absturz zu verhindern?

Zum Beispiel - die Daten wurden mit NSPropertyListFormat.XMLFormat_v1_0 codiert. Der folgende Code schlägt bei unarchiver.decodeObject() fehl, wenn die Klasse der codierten Daten unbekannt ist.

//... 
let dat = NSData(contentsOfURL: url)! 
let unarchiver = NSKeyedUnarchiver(forReadingWithData: dat) 

//it will crash after this if the class in the xml file is not known 

if let newListCollection = (unarchiver.decodeObject()) as? List { 
    return newListCollection 
} else { 
    return nil 
} 
//... 

Ich suche einen Swift 2 einzige Möglichkeit, zu prüfen, ob die Daten, bevor .decodeObject gültig ist - da .decodeObject hat keine throws - was bedeutet, dass try-catch keine Option in Swift zu sein scheint (Methoden ohne throws kann AFAIK nicht gewickelt werden). Oder eine andere Möglichkeit, die Daten zu dekodieren, die einen Fehler erzeugen, den ich fangen kann, wenn die Decodierung fehlschlägt. Ich möchte, dass der Benutzer eine Datei vom iCloud-Laufwerk oder von Dropbox importieren kann. Daher muss er ordnungsgemäß validiert werden. Ich kann nicht davon ausgehen, dass die verschlüsselten Daten sicher sind.

Die NSKeyedUnarchiver Methoden .unarchiveTopLevelObjectWithData & .validateValue haben beide throws. Gibt es vielleicht einen Weg, wie diese genutzt werden könnten? Ich kann nicht herausfinden, wie man in diesem Zusammenhang überhaupt versucht, validateValue zu implementieren. Ist das überhaupt eine mögliche Route? Oder sollte ich eine der anderen Methoden für eine Lösung suchen?

Oder kennt jemand eine Alternative Swift 2 nur um dieses Problem anzugehen? Ich glaube, dass der Schlüssel, an dem ich interessiert bin, wahrscheinlich $classname heißt - aber TBH Ich bin in Bezug auf den Versuch, herauszufinden, wie man validateValue implementieren kann - oder ob das der richtige Weg zum Durchhalten wäre. Ich habe das Gefühl, dass mir etwas offensichtlich fehlt.


EDIT: Hier ist eine Lösung - dank des Rintaro große Antwort (en) unter

Die erste Antwort für mich das Problem gelöst - das heißt, einen Delegierten zu implementieren.

//... 
let dat = NSData(contentsOfURL: url)! 
let unarchiver = NSKeyedUnarchiver(forReadingWithData: dat) 

do { 
    let decodedDataObject = try unarchiver.decodeTopLevelObject() 
    if let newListCollection = decodedDataObject as? List { 
     return newListCollection 
    } else { 
     return nil 
    } 
} 
catch { 
    return nil 
} 
//... 

Antwort

22

Wenn NSKeyedUnarchiver Begegnungen unbekannte Klassen, unarchiver(_:cannotDecodeObjectOfClassName:originalClasses:) delegieren Methode aufgerufen wird:

Vorerst jedoch habe ich mit einer Lösung um Rintaro die zusätzliche bearbeitet Antwort wie folgt aufgebaut gegangen.

Der Delegierte kann zum Beispiel laden, einige Code die Klasse in der Laufzeit und gibt die Klasse oder Ersatz eine andere Klasse Objekt einzuführen. Wenn der Delegat nil zurückgibt, bricht die Archivierung ab und die Methode löst eine NSInvalidUnarchiveOperationException aus.

So können Sie den Delegaten wie folgt implementieren:

class MyUnArchiverDelegate: NSObject, NSKeyedUnarchiverDelegate { 

    // This class is placeholder for unknown classes. 
    // It will eventually be `nil` when decoded. 
    final class Unknown: NSObject, NSCoding { 
     init?(coder aDecoder: NSCoder) { super.init(); return nil } 
     func encodeWithCoder(aCoder: NSCoder) {} 
    } 

    func unarchiver(unarchiver: NSKeyedUnarchiver, cannotDecodeObjectOfClassName name: String, originalClasses classNames: [String]) -> AnyClass? { 
     return Unknown.self 
    } 
} 

Dann:

let unarchiver = NSKeyedUnarchiver(forReadingWithData: dat) 
let delegate = MyUnArchiverDelegate() 
unarchiver.delegate = delegate 

unarchiver.decodeObjectForKey("root") 
// -> `nil` if the root object is unknown class. 

ADDED:

ich nicht bemerkt, dass NSCoder Ha s extension mit mehr swifty Methoden:

extension NSCoder { 
    @warn_unused_result 
    public func decodeObjectOfClass<DecodedObjectType : NSCoding where DecodedObjectType : NSObject>(cls: DecodedObjectType.Type, forKey key: String) -> DecodedObjectType? 
    @warn_unused_result 
    @nonobjc public func decodeObjectOfClasses(classes: NSSet?, forKey key: String) -> AnyObject? 
    @warn_unused_result 
    public func decodeTopLevelObject() throws -> AnyObject? 
    @warn_unused_result 
    public func decodeTopLevelObjectForKey(key: String) throws -> AnyObject? 
    @warn_unused_result 
    public func decodeTopLevelObjectOfClass<DecodedObjectType : NSCoding where DecodedObjectType : NSObject>(cls: DecodedObjectType.Type, forKey key: String) throws -> DecodedObjectType? 
    @warn_unused_result 
    public func decodeTopLevelObjectOfClasses(classes: NSSet?, forKey key: String) throws -> AnyObject? 
} 

Sie können:

do { 
    try unarchiver.decodeTopLevelObjectForKey("root") 
    // OR `unarchiver.decodeTopLevelObject()` depends on how you archived. 
} 
catch let (err) { 
    print(err) 
} 
// -> emits something like: 
// Error Domain=NSCocoaErrorDomain Code=4864 "*** -[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (MyProject.MyClass) for key (root); the class may be defined in source code or a library that is not linked" UserInfo={NSDebugDescription=*** -[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (MyProject.MyClass) for key (root); the class may be defined in source code or a library that is not linked} 
+0

Vielen Dank. Das ist eine großartige Antwort. – simons

+1

Siehe meine aktualisierte Antwort. – rintaro

+0

Danke nochmal. 'Wenn try unarchiver.decodeTopLevelObject()! = nil' funktioniert für mich so wie ich es implementiert habe. Wenn es nicht "nil" ist, dann bin ich gut zu gehen. Das habe ich vermisst, als ich nach Methoden suchte, die werfen. Deine ursprüngliche Antwort funktioniert auch für mich. – simons

15

eine andere Art und Weise ist es, den Namen der Klasse für NSCoding verwendet zu beheben. Sie müssen einfach nur verwenden:

  • NSKeyedArchiver.setClassName("List", forClass: List.self vor
  • NSKeyedUnarchiver.setClass(List.self, forClassName: "List") Serialisierung vor Deserialisieren

wo immer nötig.

Sieht aus wie iOS-Erweiterungen den Klassennamen mit dem Namen der Erweiterung voranstellen.

+1

Dies funktioniert gut für eine neue App, aber seien Sie vorsichtig, wenn Ihre App bereits im Geschäft ist, da beim ersten Start nach dem Upgrade alle vorhandenen Daten nicht mit dem neuen Klassennamen archiviert wurden, was zu einem Absturz. Stellen Sie sicher, dass Sie aus Gründen der Abwärtskompatibilität über Code verfügen. Eine Möglichkeit, dies zu tun, besteht darin, den Delegaten weiterhin zu verwenden, um den korrekten Klassennamen zurückzugeben. –

+0

@JamesKuang Ich denke, dass er über den Fall spricht, wo es ohne das nie funktionieren wird, dh. Sie codieren in der Klasse und dekodieren in der Erweiterung, sodass die Klassennamen nicht übereinstimmen. – xaphod

+0

Seltsamerweise musste ich NSKeyedUnarchiver.setClass verwenden, damit dies funktionierte (in einer verschachtelten Swift-Klasse). Aber nur wenn man es von TestFlight bekommt. Das Ausführen von Xcode (8.3.1) auf meinem Test-Telefon hat gut funktioniert. Und die Verwendung von @objc() machte keinen sichtbaren Unterschied – Kristoffer

0

Eigentlich ist es der Grund, den wir zutiefst ausgraben sollten. Es ist möglich, Sie erstellen einen Archivpfad namens xxx.archive, dann entpacken Sie den Pfad (xxx.archive), jetzt ist alles in Ordnung. Aber wenn der Name des Zieles geändert wird, wenn Sie die Archivierung durchführen, ist der Absturz aufgetreten !!! Es ist, weil Archiv & das andere Objekt (die Wahrheit ist Archiv & unarchive target.obj, nicht nur das obj) archivieren. So einfach ist es, den Archivpfad zu löschen oder einfach einen anderen Archivpfad zu verwenden. Und dann sollten wir darüber nachdenken, wie man den Absturz vermeidet, try-catch ist unser Helfer, der von rintaro erwähnt wird.