我有一个复杂的类,其中有许多嵌套类到多个深层,这些类被序列化并存储在CoreData表中。问题是,由于升级到Swift 3类的实例无法反序列化,因此在尝试解码嵌套类的实例时失败。
为了说明问题,使用实现NSCoding的内部类创建一个类定义:
导入基金会
class Foo : NSObject, NSCoding {
class Bar : NSObject, NSCoding {
var x : Int
init(x:Int) {
self.x = x
}
required init?(coder aDecoder: NSCoder) {
self.x = aDecoder.decodeInteger(forKey: "x")
}
func encode(with aCoder: NSCoder) {
aCoder.encode(x, forKey:"x")
}
}
var bar:Bar
init(x:Int) {
self.bar = Bar(x: x)
}
required init?(coder aDecoder: NSCoder) {
self.bar = aDecoder.decodeObject(forKey: "bar") as! Bar
}
func encode(with aCoder: NSCoder) {
aCoder.encode(bar, forKey:"bar")
}
}
然后构建单元测试:
import XCTest
@testable import SerializationDemo
class SerializationDemoTests: XCTestCase {
func fileURL() -> URL {
let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
return dir.appendingPathComponent("foo.dat")
}
func testSerialize() {
let foo = Foo(x:42)
let data = NSKeyedArchiver.archivedData(withRootObject: foo)
let url = fileURL()
do {
try data.write(to: url)
} catch {
XCTFail("\(error)")
}
}
func testDeserialise() {
let url = fileURL()
do {
let data = try Data(contentsOf: url)
let myfoo = NSKeyedUnarchiver.unarchiveObject(with: data) as! Foo
XCTAssertEqual(myfoo.bar.x, 42)
} catch {
XCTFail("\(error)")
}
}
}
现在通过选择测试用例本身在一个会话中运行两个测试。这将按预期正常工作,正确地序列化和反序列化对象。
现在尝试独立运行两个单元测试。首先序列化。反序列化。第二次测试失败,
失败:捕获" NSInvalidUnarchiveOperationException"," *** - [NSKeyedUnarchiver decodeObjectForKey:]:无法解码类的对象(_TtCC17SerializationDemo3Foo3Bar)为key(bar);班级可以 在源代码或未链接的库中定义"
一种解决方案是重构代码,以便所有内部类都是顶级的,这也需要重命名这些类,使它们在应用程序中全局唯一。我宁愿不用这种方式重写代码
答案 0 :(得分:6)
我确定这是Swift 3中的一个错误,可能是尝试优化类加载器。在通过其他方式加载类定义之前,类加载器似乎不会在反序列化期间直接加载内部类。
在样本测试中添加一行
_ = Foo(x: 0)
testDeserialise()修复了问题。显然是通过预加载必要的类定义。
我能够通过在复杂类中添加一些代码来解决我的应用程序中的问题,以便在第一次尝试反序列化实例时递归地预加载每个内部类的实例:
基本上:
if !preloaded {
for x in allElements() {
_ = x.allOperations()
}
preloaded = true
}
其中allElements()返回一个实例化每个内部类的一个实例的列表,allOperations()返回每个内部类的所有内部类。
<强>更新强>
我已向Apple提交了一份错误报告#30034842
Apple工程公司也提出了另一个临时解决方法。在解码对象知道可以解码的可能类时使用NSCoder.decodeObject(of: [AnyClass]?, forKey: String)
。
他们也注意到了:
您还应该为Bar指定@objc名称(例如@objc(Bar));我们不 保证Swift错位名称的稳定性 - 对于 向后兼容,你应该给它一个规范 名。
更新 2018年3月
这在Xcode 9中得到修复,即使在使用Swift语言版本3.2进行编译时也是如此。此外,在编码期间在运行时强制分配@objc名称。