34

NSKeyedUnarchiver.decodeObject will cause a crash / SIGABRT if the original class is unknown. The only solution I have seen to catching this issue dates from Swift's early history and required using Objective C (also pre-dated Swift 2's implementation of guard, throws, try & catch). I could figure out the Objective C route - but I would prefer to understand a Swift-only solution if possible.

For example - the data has been encoded with NSPropertyListFormat.XMLFormat_v1_0. The following code will fail at unarchiver.decodeObject() if the class of the encoded data is unknown.

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

I am looking for a Swift 2 only way to test whether the data is valid before attempting .decodeObject - since .decodeObject has no throws - which means that try - catch does not seem to be an option in Swift (methods without throws cannot be wrapped AFAIK). Or else an alternative way of decoding the data which will throw an error I can catch if the decode fails. I want the user to be able to import a file from iCloud drive or Dropbox - therefore it needs to be properly validated. I cannot assume that the encoded data is safe.

The NSKeyedUnarchiver methods .unarchiveTopLevelObjectWithData & .validateValue both have throws. Is there perhaps some way that these could be used? I cannot work out how to even begin to attempt to implement validateValue in this context. Is this even a possible route? Or should I be looking to one of the other methods for a solution?

Or does anyone know an alternative Swift 2 only way of addressing this issue? I believe that the key I am interested in is probably entitled $classname - but TBH I am out of my depth with respect to trying to work out how to implement validateValue - or even whether that would be the correct route to persevere with. I have the sense that I am missing something obvious.


EDIT: Here is a solution - thanks to rintaro's great answer(s) below

The initial answer solved the issue for me - i.e. implementing a delegate.

For now however I have gone with a solution built around rintaro's additional edited response as follows:

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

4 回答 4

29

NSKeyedUnarchiver遇到未知类时, unarchiver(_:cannotDecodeObjectOfClassName:originalClasses:)调用委托方法。

例如,委托可以加载一些代码以将类引入运行时并返回该类,或者替换一个不同的类对象。如果委托返回nil,则取消归档中止并且该方法引发NSInvalidUnarchiveOperationException.

因此,您可以像这样实现委托:

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

然后:

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

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

添加

我没有注意到NSCoderextension更快速的方法:

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

你可以:

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}
于 2015-10-02T12:33:21.063 回答
18

另一种方法是固定用于 NSCoding 的类的名称。您只需使用:

  • NSKeyedArchiver.setClassName("List", forClass: List.self序列化之前
  • NSKeyedUnarchiver.setClass(List.self, forClassName: "List")在反序列化之前

任何需要的地方。

看起来 iOS 扩展在类名前面加上扩展名。

于 2016-05-11T12:46:15.573 回答
1

其实,这才是我们应该深入挖掘的重要原因。有一种可能,您创建一个名为 xxx.archive 的存档路径,然后从路径(xxx.archive)取消存档,现在一切正常。但是如果更改目标名称,当您取消归档时,就会发生崩溃!!!这是因为归档和取消归档不同的对象(事实是我们归档和取消归档 target.obj,而不仅仅是 obj)。如此简单的方法是删除存档路径或仅使用不同的存档路径。然后我们应该考虑如何避免崩溃,try-catch 是rintaro提到的我们的助手。

于 2018-03-01T07:46:51.927 回答
0

我遇到了同样的问题。将@objc 添加到类声明中对我有用。

@objc(YourClass)
class YourClassName: NSObject {
}
于 2019-02-12T05:52:22.887 回答