Swift 防止 NSKeyedUnarchiver.decodeObject 崩溃的唯一方法?

Posted

技术标签:

【中文标题】Swift 防止 NSKeyedUnarchiver.decodeObject 崩溃的唯一方法?【英文标题】:Swift only way to prevent NSKeyedUnarchiver.decodeObject crash? 【发布时间】:2015-12-30 12:35:44 【问题描述】:

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 - 这意味着 try - catch 似乎不是Swift 中的一个选项(没有throws 的方法无法包装AFAIK)。或者另一种解码数据的方法,如果解码失败,我可以捕捉到错误。我希望用户能够从 iCloud 驱动器或 Dropbox 导入文件 - 因此需要对其进行正确验证。我不能假设编码数据是安全的。

NSKeyedUnarchiver 方法 .unarchiveTopLevelObjectWithData.validateValue 都有 throws。也许有一些方法可以使用这些?在这种情况下,我什至无法弄清楚如何开始尝试实施validateValue。这甚至是一条可能的路线吗?还是我应该寻找其他方法之一来解决?

或者有没有人知道解决这个问题的唯一 Swift 2 替代方法?我相信我感兴趣的关键可能是$classname - 但是TBH我在尝试解决如何实施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

//...

【问题讨论】:

【参考方案1】:

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

【讨论】:

非常感谢。这是一个很好的答案。 再次感谢。 if try unarchiver.decodeTopLevelObject() != nil 以我实现它的方式为我工作。如果不是nil,那我可以走了。在寻找抛出的方法时,我错过了那个。您的原始答案也适用于我。 可悲的是,新的 Swift 扩展只在 ios 9 中 太棒了!谢谢一堆! :-)【参考方案2】:

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

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

任何需要的地方。

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

【讨论】:

这适用于新应用程序,但如果您的应用程序已经在商店中,请小心,因为在升级后的第一次启动时,任何现有数据都不会使用新的类名称存档,导致崩溃。确保您有一些代码以实现向后兼容性。一种方法是仍然使用委托返回正确的类名。 @JamesKuang 我认为他在谈论没有这个就永远无法工作的情况,即。您在类中编码并在扩展中解码,因此类名不匹配。 奇怪的是,我必须使用 NSKeyedUnarchiver.setClass 才能工作(在嵌套的 Swift 类上)。但仅在从 TestFlight 获取时。在我的测试手机上从 Xcode (8.3.1) 运行它运行良好。并且使用 @objc() 并没有任何明显的区别【参考方案3】:

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

【讨论】:

【参考方案4】:

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

@objc(YourClass)
class YourClassName: NSObject 

【讨论】:

以上是关于Swift 防止 NSKeyedUnarchiver.decodeObject 崩溃的唯一方法?的主要内容,如果未能解决你的问题,请参考以下文章

Swift:防止 ABPeoplePickerNavigationController 关闭

Swift - 在 UIViewController 中防止返回事件

Swift 防止 NSKeyedUnarchiver.decodeObject 崩溃的唯一方法?

swift 防止屏幕锁定应用

Swift 防止 TabBar 在键盘处于活动状态时向上移动

如何防止 Swift 4 重叠或独占访问错误?