Swift中防止NSKeyedUnarchiver.decodeObject崩溃的唯一方法是什么?

34
NSKeyedUnarchiver.decodeObject如果原始类未知,将导致崩溃/SIGABRT。目前我所见到的唯一解决方法可以追溯到Swift早期的历史,需要使用Objective C(也是在Swift 2实现guardthrowstrycatch之前)。我可以找出Objective C的路线-但如果可能的话,我更愿意了解仅限于Swift的解决方案。

例如-数据已使用NSPropertyListFormat.XMLFormat_v1_0进行编码。如果编码数据的类未知,则以下代码将在unarchiver.decodeObject()处失败。

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

我正在寻找一种仅适用于Swift 2的方法,在尝试使用.decodeObject之前测试数据是否有效 - 因为.decodeObject没有throws - 这意味着在Swift中try-catch似乎不是一个选项(据我所知,没有throws的方法不能被包装)。否则,需要另一种解码数据的替代方式,如果解码失败,则会抛出错误。我希望用户能够从iCloud驱动器或Dropbox导入文件 - 因此需要进行适当的验证。我不能假设编码的数据是安全的。

NSKeyedUnarchiver方法.unarchiveTopLevelObjectWithData.validateValue都有throws。也许可以使用这些方法之一吗?我甚至无法开始尝试在这种情况下实现validateValue。这是否是一个可行的路线?还是我应该寻找其他方法来解决这个问题?

还是有人知道另一种解决这个问题的Swift 2的方法吗?我相信我感兴趣的密钥可能标题为$classname - 但是说实话,我在尝试解决如何实现validateValue方面超出了我的能力范围 - 甚至不知道这是否是坚持的正确路线。我有一种感觉自己错过了什么显而易见的东西。


编辑:这里提供一个解决方案 - 感谢下面rintaro的出色答案

最初的答案为我解决了这个问题 - 即实现委托。

但现在,我选择了基于rintaro的额外编辑响应的解决方案,如下:

//...
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个回答

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.

新增内容:

我没有注意到 NSCoder 有更多的 swifty 方法,是通过 extension 添加的:

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}

非常感谢。这是一个很棒的答案。 - simons
再次感谢。if try unarchiver.decodeTopLevelObject() != nil 对我来说是可行的,因为我已经实现了它。如果它不是 nil,那么我就可以继续了。当我寻找抛出方法时,我错过了这个。你原来的答案对我也有用。 - simons
令人遗憾的是,新的Swift扩展仅适用于iOS 9。 - davidgyoung
太棒了!非常感谢! :-) - Hernan Arber

18

另一种方法是修复用于 NSCoding 的类名。您只需要使用:

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

按需要进行设置。

看起来 iOS扩展会在类名前缀中添加扩展的名称。


3
对于新应用这个方法很好用,但如果你的应用已经在商店上了,就要小心了。因为在升级后的第一次启动时,任何现有数据都没有归档到新的类名中,这会导致崩溃。确保你采用了一些向后兼容的代码。一种方法是仍然使用委托返回正确的类名。 - James Kuang
@JamesKuang 我认为他指的是如果没有这个,它永远不会起作用的情况,即你在类中编码,在扩展中解码,所以类名不匹配。 - xaphod
有趣的是,我不得不使用NSKeyedUnarchiver.setClass才能使其正常工作(在嵌套的Swift类上)。但只有从TestFlight获取时才需要。在我的测试手机上从Xcode(8.3.1)运行很好。而且使用@objc()似乎没有任何明显的区别。 - Kristoffer

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

0

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

@objc(YourClass)
class YourClassName: NSObject {
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接