问题
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
}
//...
回答1:
When NSKeyedUnarchiver
encounters unknown classes, unarchiver(_:cannotDecodeObjectOfClassName:originalClasses:) delegate method is called.
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
.
So, you can implement the delegate like this:
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
}
}
Then:
let unarchiver = NSKeyedUnarchiver(forReadingWithData: dat)
let delegate = MyUnArchiverDelegate()
unarchiver.delegate = delegate
unarchiver.decodeObjectForKey("root")
// -> `nil` if the root object is unknown class.
ADDED:
I didn't noticed that NSCoder
has extension
with more swifty methods:
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?
}
You can:
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}
回答2:
another way is to fix the name of the class used for NSCoding. You simply have to use:
NSKeyedArchiver.setClassName("List", forClass: List.self
before serializingNSKeyedUnarchiver.setClass(List.self, forClassName: "List")
before deserializing
wherever needed.
Looks like iOS extensions prefix the class name with the extension's name.
回答3:
Actually, it's the reason which we should dig deeply matters. There's a possible, you create a archive path named xxx.archive, then you unarchive from the path(xxx.archive), now everything is ok. But if change target name, when you unarchive, the crash occurred!!! It's because archive&unarchive the different object(the truth is we archive&unarchive target.obj, not just the obj). so simple way is to delete the archive path or just use a different archive path. And then we should consider how avoid the crash, try-catch is our helper mentioned by rintaro.
回答4:
I was having same issue. Adding @objc to class declaration worked for me.
@objc(YourClass)
class YourClassName: NSObject {
}
来源:https://stackoverflow.com/questions/32904811/swift-only-way-to-prevent-nskeyedunarchiver-decodeobject-crash