web-dev-qa-db-fra.com

Seul moyen rapide d'éviter le crash de NSKeyedUnarchiver.decodeObject?

NSKeyedUnarchiver.decodeObject provoquera un crash/SIGABRT si la classe d'origine est inconnue. La seule solution que j'ai trouvée pour résoudre ce problème remonte aux débuts de Swift et nécessitait l'utilisation de l'objectif C (également antérieure à la mise en œuvre par Swift 2 de guard, throws, try & catch). Je pourrais trouver la voie de l’objectif C, mais je préférerais, si possible, comprendre une solution exclusivement Swift.

Par exemple, les données ont été codées avec NSPropertyListFormat.XMLFormat_v1_0. Le code suivant échouera à unarchiver.decodeObject() si la classe des données codées est inconnue.

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

Je suis à la recherche d'un seul moyen pour Swift 2 de vérifier si les données sont valides avant de tenter .decodeObject - puisque .decodeObject n'a pas de variable throws - ce qui signifie que try - catch ne semble pas être une option dans Swift (les méthodes sans throws ne peuvent pas être encapsulées AFAIK) . Ou bien, une autre façon de décoder les données, ce qui jettera une erreur que je peux détecter si le décodage échoue. Je veux que l'utilisateur puisse importer un fichier à partir d'un lecteur iCloud ou de Dropbox - il doit donc être correctement validé. Je ne peux pas supposer que les données encodées sont en sécurité.

Les méthodes NSKeyedUnarchiver.unarchiveTopLevelObjectWithData & .validateValue ont toutes les deux throws. Existe-t-il un moyen de les utiliser? Je n'arrive pas à comprendre comment même commencer à essayer d'implémenter validateValue dans ce contexte. Est-ce même un itinéraire possible? Ou devrais-je me tourner vers l'une des autres méthodes pour trouver une solution?

Ou est-ce que quelqu'un connaît un autre moyen alternatif pour Swift 2 de résoudre ce problème? Je crois que la clé qui m'intéresse est probablement intitulée $classname - mais TBH ne me permet pas d'essayer de trouver comment mettre en œuvre validateValue - ou même si ce serait le bon moyen de persévérer. . J'ai l'impression qu'il me manque quelque chose d'évident.


EDIT: Voici une solution - grâce à l'excellente réponse de rintaro ci-dessous

La réponse initiale a résolu le problème pour moi - c’est-à-dire la mise en place d’un délégué.

Pour l'instant cependant, je suis parti avec une solution construite autour de la réponse modifiée supplémentaire de rintaro comme suit:

//...
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
}
//...
33
simons

Lorsque NSKeyedUnarchiver rencontre des classes inconnues, la méthode unarchiver(_:cannotDecodeObjectOfClassName:originalClasses:) delegate est appelée.

Le délégué peut, par exemple, charger du code pour introduire la classe dans le moteur d'exécution et la retourner, ou substituer un autre objet de classe. Si le délégué renvoie nil, la désarchivage est abandonnée et la méthode lève une NSInvalidUnarchiveOperationException.

Donc, vous pouvez implémenter le délégué comme ceci:

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

Ensuite:

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

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

AJOUTÉE

Je n'ai pas remarqué que NSCoder a extension avec des méthodes plus rapides:

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

Vous pouvez:

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

une autre solution consiste à corriger le nom de la classe utilisée pour NSCoding. Vous devez simplement utiliser:

  • NSKeyedArchiver.setClassName("List", forClass: List.self avant la sérialisation
  • NSKeyedUnarchiver.setClass(List.self, forClassName: "List") avant de désérialiser

partout où cela est nécessaire.

On dirait que les extensions iOS préfixent le nom de la classe avec le nom de l'extension.

16
teriiehina

En fait, c’est la raison pour laquelle nous devrions creuser profondément. Il y a une possibilité, vous créez un chemin d'archive nommé xxx.archive, puis vous désarchivez du chemin (xxx.archive), maintenant tout va bien. Mais si vous changez le nom de la cible, quand vous désarchivez, le crash est survenu !!! C’est parce que archive & désarchiver les différents objets (la vérité est que nous archivons & désarchivons target.obj, pas seulement obj) . Le moyen le plus simple est de supprimer le chemin d’archive ou d’utiliser un autre chemin d’archive. Et ensuite, nous devrions réfléchir à la manière d'éviter le crash, try-catch est notre assistant mentionné par rintaro .

1
responser

J'avais le même problème. Ajouter @objc à la déclaration de classe a fonctionné pour moi.

@objc(YourClass)
class YourClassName: NSObject {
}
0
Van