陈旧的 Core Data 文档终于在 iOS 9 正式发布的当天更新了,文档更新历史上写着此次更新重写了文档,是关于当前 API 实践的重大更新。然而本文的部分被误删了是吧,就剩下下面一段话了。原本关于这部分的内容就我一直很困惑,如今倒好文档也给删了......
不过,从 google 的缓存还可以看以前的文档,选择「Text-only version」模式,阅读上会舒服一些。
Core Data 支持的标准数据类型包括:
1.不同类型的数字,包括16、32、64位的 Int,两种浮点数,Decimal,布尔值,这些数据都会被封装成 NSNumber,在生成子类的时候可以选择是否使用原始的数据类型;
2.String, 使用 Unicode 存储,由于 Unicode 的复杂性,对 String 类型的属性进行排序和搜索会比较复杂;
3.Date,仅仅是对 NSTimeInterval (也就是 Double)的封装,不包含时区信息,你需要自行保存时区的信息,默认在子类中使用 NSDate 类型,可以在生成子类时使用原始数据类型,会使用 NSTimeInterval;
4.BinaryData,也就是 NSData 类型。
以上这些标准类型都是不可变的类型,剩下的两种:Transformable 和 Undefined 用于支持非标准类型的数据。对于 Undefined 类型,记得在右侧的 Data Model Inspector 里勾选 Transient 选项。支持非标准类型时,你最好也使用不可变类型,不然 Core Data 无法跟踪数据的变化。也就是说每次给属性设置一个新的值,Core Data 才知道你的 NSManagedObject 发生了变化,这点在你使用自定义数据类型时很重要。
那不是以上这些类型的对象和结构体怎么办?总体来说,支持非标准类型有两条路:Transformable Attribute 和 Transient Attribute。
Transformable Attribute
使用这种属性时工作量很小,适合遵守 NSCoding 协议的类型,在子类中将该属性类型更改为你需要的类型即可,剩下的工作由 Core Data 替我们完成。缺点是这种属性的内部存储由于是使用属性列表 property list 这种格式,所以比较浪费空间,在性能上也不太好。如果你对性能和空间没有很特别的要求,使用这种就可以了。
如果你想更加高效地存储,你可以提供自定义的 value transformer 来实现数据的转化。(我是不会这个的)
Undefined(Transient Attribute)
对于 transient attribute,必须指定 Attribute Type 为 Undefined,而且要选中「Transient」。「Allows External Storage」不是必选选项,选中该项后由 Core Data 决定数据是否存放在其他地方;@NSManaged 修饰符表示由 Core Data 来负责动态生成存取方法,对于 transient attribute,无法使用 @NSManaged 修饰符,因为这是需要开发者负责的。
Transient Attribute 类似 Swift 里的计算属性,需要根据其他属性来生成。Core Data 在 fetch 时并不会处理这种属性,不会存储这种属性,也不会跟踪这种属性的变化。总之就是对这种属性不管不问,完全由开发者自己负责。不过,处理这种属性的全过程都可以自定义,应对各种需求。因为 Core Data 对这种属性完全是放任自流的,这种方式有额外的工作:需要我们实现属性的存取方法 accessor methods。比如,处理 UIImage 对象,需要先用一个备份属性使用 Core Data 支持的类型,然后 transient attribute 利用前者生成我们需要的 UIImage 对象。
使用 transient attribute,必须要提到 primitive property,这是 Core Data 对属性的内部实现。可以通过 primitiveValueForKey(key)
和 setPrimitiveValue(newValue, forKey: )
两个方法来获得对 primitive property 的访问。
@NSManaged private var imageData: NSData?
var image: UIImage?{
get {
let ImageKey: String = "image"
willAccessValueForKey(ImageKey)
var imageObject = primitiveValueForKey(ImageKey) as? UIImage
didAccessValueForKey(ImageKey)
if imageObject == nil{
imageObject = UIImage(data: imageData!)
}
return imageObject
}
set {
let ImageKey: String = "image"
let ImageDataKey = "imageData"
willChangeValueForKey(ImageKey)
self.setPrimitiveValue(newValue, forKey: ImageKey)
didChangeValueForKey(ImageKey)
let imageRawData = UIImageJPEGRepresentation(newValue!, 1.0)!
//更新persistent attribute 时使用 KVC 方法,这样 Core Data 才知道该属性变化了。
self.setValue(imageRawData, forKey: ImageDataKey)
}
}
由于Core Data 基本上对 transient attribute 不管不问,同时不支持的数据类型也通常会有访问代价大的问题。因此,可以基于不同的优化策略对 transient attribute 进行定制。
获取策略有两种:
1.按需获取 The On-demand Get
上面的代码就是。
2.预获取 The Pre-calculated Get
在对象被 fetch 时获取对象
override func awakeFromFetch() {
super.awakeFromFetch()
if let imageRawData = self.imageData{
let imageKey = "image"
let imageObject = UIImage(data: imageRawData)
self.setPrimitiveValue(imageObject, forKey: imageKey)
}
}
更新策略有两种:
1.即时更新 The Immediate-Update Set
上面的示例里就是
2.延迟更新 The Delayed-Update Set
在对象被保存时才更新
override func willSave() {
super.willSave()
let imageKey = "image"
let imageDataKey = "imageData"
//这里不必使用 KVC 方法
if let image = self.primitiveValueForKey(imageKey) as? UIImage {
let imageRawData = UIImageJPEGRepresentation(image, 1.0)
self.setPrimitiveValue(imageRawData, forKey: imageDataKey)
}else{
self.setPrimitiveValue(nil, forKey: imageDataKey)
}
}
同时还需要更改 transient attribute 的 set 方法,不要在 set 里更新用作备份的属性。
set {
let ImageKey: String = "image"
willChangeValueForKey(ImageKey)
self.setPrimitiveValue(newValue, forKey: ImageKey)
didChangeValueForKey(ImageKey)
}
Binary Large Data Objects (BLOBs)
在上一节中,使用了 NSData 作为备份属性来存储图像的二进制数据,这里也可以不用 transient attribute 这种方式,直接用 NSData 类型存储,那么有几个问题需要考虑。
首先是内存占用,图像一般比其他数据大得多。下面的 persistent store 类型中,面对会占用大量内存的数据,SQLite store 是最佳选择。
其次,选择更合理的访问策略。与其将 BLOBs 对象直接作为属性,不如将其作为其他
NSManagedObject
的属性并将两者使用关系(relationship)联系起来。因为访问关系默认是 faults 状态,只有访问属性时才会填充数据,这样可以最大限度地降低内存占用。
另外,对于这种 BLOBs,可以选择在文件系统中存储,而在 Core Data 里只需要维持该资源的 URL,在需要的时候才获取该资源。
总结
对于不支持的类型,基本策略是转化为被支持的类型。
大部分对象类型都遵守 NSCoding 协议,可以使用 Transformable Attribute,这是最简单的方法,可能会有一些性能上的问题,需要优化或是同时做其他事情的话就使用 Transient Attribute。
对不支持 NSCoding 协议的类型,比如 CGRect,CGSize 这些结构体,可以转化为遵守 NSCoding 协议的 NSValue 对象,这样可以使用 Transformable Attribute 来保存了。又或者将类型的成员数据拆分,比如 CGSize 有 width 和 height 两个都是 CGFloat 成员变量,可以将两个成员变量转换为 float 类型,这样就可以用 transient attribute 来获取对应的 CGSize 数据了。
这部分内容不适合 relationship。另外,如果你自定义某个类作为NSManagedObject
子类的属性,直接这样做是不合适的,你可以考虑将这个类用NSManagedObject
的子类来实现并用 relationship 来连接前者。