Swift 防止 NSKeyedUnarchiver.decodeObject 崩溃的唯一方法?
Swift only way to prevent NSKeyedUnarchiver.decodeObject crash?
NSKeyedUnarchiver.decodeObject
会导致崩溃 / SIGABRT
如果原来的 class 未知。我所看到的解决此问题的唯一解决方案来自 Swift 的早期历史,并且需要使用 Objective C(还有 pre-dated Swift 2 对 guard
的实现, throws
、try
和 catch
)。我可以找出 Objective C 路线 - 但如果可能的话,我更愿意了解 Swift-only 解决方案。
例如 - 数据已使用 NSPropertyListFormat.XMLFormat_v1_0
编码。如果编码数据的 class 未知,则以下代码将在 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
- 但事实上我无法理解如何实现 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
}
//...
当NSKeyedUnarchiver
遇到未知的类时,unarchiver(_:cannotDecodeObjectOfClassName:originalClasses:)
委托方法被调用。
The delegate may, for example, load some code to introduce the class to the runtime and return the class, or substitute a different class object. If the delegate returns nil
, unarchiving aborts and the method raises an 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
有 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}
另一种方法是修复用于 NSCoding 的 class 的名称。您只需使用:
NSKeyedArchiver.setClassName("List", forClass: List.self
序列化前
NSKeyedUnarchiver.setClass(List.self, forClassName: "List")
反序列化前
任何需要的地方。
看起来 iOS 扩展名在 class 名称前加上扩展名。
其实这才是我们应该深挖的原因。有一种可能,您创建一个名为 xxx.archive 的存档路径,然后您从该路径 (xxx.archive) 中取消存档,现在一切正常。但是,如果更改目标名称,当您取消存档时,就会发生崩溃!这是因为归档和取消归档不同的对象(事实是我们归档和取消归档 target.obj,而不仅仅是对象)。
如此简单的方法是删除存档路径或只使用不同的存档路径。然后我们应该考虑如何避免崩溃,try-catch是rintaro提到的我们的帮手。
我遇到了同样的问题。将 @objc 添加到 class 声明对我有用。
@objc(YourClass)
class YourClassName: NSObject {
}
NSKeyedUnarchiver.decodeObject
会导致崩溃 / SIGABRT
如果原来的 class 未知。我所看到的解决此问题的唯一解决方案来自 Swift 的早期历史,并且需要使用 Objective C(还有 pre-dated Swift 2 对 guard
的实现, throws
、try
和 catch
)。我可以找出 Objective C 路线 - 但如果可能的话,我更愿意了解 Swift-only 解决方案。
例如 - 数据已使用 NSPropertyListFormat.XMLFormat_v1_0
编码。如果编码数据的 class 未知,则以下代码将在 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
- 但事实上我无法理解如何实现 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
}
//...
当NSKeyedUnarchiver
遇到未知的类时,unarchiver(_:cannotDecodeObjectOfClassName:originalClasses:)
委托方法被调用。
The delegate may, for example, load some code to introduce the class to the runtime and return the class, or substitute a different class object. If the delegate returns
nil
, unarchiving aborts and the method raises anNSInvalidUnarchiveOperationException
.
因此,您可以像这样实现委托:
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
有 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}
另一种方法是修复用于 NSCoding 的 class 的名称。您只需使用:
NSKeyedArchiver.setClassName("List", forClass: List.self
序列化前NSKeyedUnarchiver.setClass(List.self, forClassName: "List")
反序列化前
任何需要的地方。
看起来 iOS 扩展名在 class 名称前加上扩展名。
其实这才是我们应该深挖的原因。有一种可能,您创建一个名为 xxx.archive 的存档路径,然后您从该路径 (xxx.archive) 中取消存档,现在一切正常。但是,如果更改目标名称,当您取消存档时,就会发生崩溃!这是因为归档和取消归档不同的对象(事实是我们归档和取消归档 target.obj,而不仅仅是对象)。 如此简单的方法是删除存档路径或只使用不同的存档路径。然后我们应该考虑如何避免崩溃,try-catch是rintaro提到的我们的帮手。
我遇到了同样的问题。将 @objc 添加到 class 声明对我有用。
@objc(YourClass)
class YourClassName: NSObject {
}