本文是iOS开发技巧系列---打造强大的BaseModel中的篇二,第一篇文章请见此:
让Model自我描述 ,相对于让Model实现自我描述,让Model实现自动归档的难度大得多。我相信能够好好看完这几篇文章的人,绝对是有大收获的。另外,些文章不适合新手,只适合有一定有Swift开发经验的人。
2018年Swift4已经发布,现在需要更新这些文章了,里面的代码可能都跑不起了。所以我要修正这些代码让其跑起来。我把这些代码都放在iOSDemo项目里
https://github.com/DuckDeck/iOSDemo
什么是iOS的归档
归档--NSKeyedArchiver,是iOS开发中基本的数据存储方式之一,和其他的数据存储方式相比,归档不仅能够存储任意类型的数据,而且使用起来也很简单。归档能将数据处理成NSData的形式,所以很容易以文件的形式保存在APP的沙盒中,而解归和归档相反,它是将保存在APP沙盒的归档文件逆归档,转换成归档前的状态。
传统的iOS归档方式
要想让一个自定义对象可以使用归档,必须要让其符合NSCoding协议,
public protocol NSCoding {
public func encodeWithCoder(aCoder: NSCoder)
public init?(coder aDecoder: NSCoder) // NS_DESIGNATED_INITIALIZER
}
@end
上面的代码是iOS中NSCoding协议的定义。里面包含两个方法,其中一个是构造器。第一个方法
public func encodeWithCoder(aCoder: NSCoder)
就是归档方法,它是为了告诉NSKeyedArchiver对象如何将数据归档成文件的。第二个方法(构造器)
public init?(coder aDecoder: NSCoder) // NS_DESIGNATED_INITIALIZER
就是解档方法了。它是告诉NSKeyedUnArchiver是如何将归档好的对象解档成原来的数据的
下面来看看传统的iOS归档方式,先定义一个类,让其符合NSCoding协议
@objcMembers class DemoArchiver:GrandModel,NSCoding {
var demoString:String?
var demoInt = 100
var demoFloat:Float = 0.0
var demoDate = Date()
override init() { }
func encode(with aCoder: NSCoder) {//归档需要实现的方法
aCoder.encode(demoString, forKey: "demoString")
aCoder.encode(demoInt, forKey: "demoInt")
aCoder.encode(demoFloat, forKey: "demoFloat")
aCoder.encode(demoDate, forKey: "demoDate")
}
@objc required init?(coder aDecoder: NSCoder) {//解档需要实现的构造器
demoString = aDecoder.decodeObject(forKey: "demoString") as? String
demoInt = aDecoder.decodeInteger(forKey: "demoInt")
demoFloat = aDecoder.decodeFloat(forKey: "demoFloat")
demoDate = aDecoder.decodeObject(forKey: "demoDate") as! Date //存在强制转换情况
}
}
我们需要在正确地重写这两个方法。这里面最需要注意的点有两个,一是不要把数据类型搞错。二是key名不要弄错了。然后下面开始测试
let demoTest = DemoArchiver()
demoTest.demoString = "ABCDEFG"
demoTest.demoFloat = 11.11
print(demoTest)
let a = NSKeyedArchiver.archivedDataWithRootObject(demoTest)
let b = NSKeyedUnarchiver.unarchiveObjectWithData(a)
print(b)
//打印结果
DemoArchiver:["demoInt": 100, "demoString": ABCDEFG, "demoFloat": 11.11, "demoDate": 2016-03-09 13:03:17 +0000]
Optional(DemoArchiver:["demoInt": 100, "demoString": ABCDEFG, "demoFloat": 11.11, "demoDate": 2016-03-09 13:03:17 +0000])
可见经过归档再解档后的数据又恢复了原样。这里需要说明一下的是,一般是需要把归档后的文件保存在APP的沙盒目录内的,需要使用时再取出来解档。这里为了测试方便就不这么做了。
传统的iOS归档方式的弊端
相信大家很容易看出使用传统的iOS归档方式的不足之处,还是和以前一样,需要写太多的重复啰嗦代码了。目前对于Objc语言来说,有一个代码生成器(Accessorizer,见(http://www.kevincallahan.org/software/accessorizer.html))可以使用,只需要把所有属性放进去,就可以生成所有属性的归档解档方法。遗憾的是Swift目前还没有这种工具可以用(或者有了但是我不知道),2018年应该有了,只是我不太想用这东西。只有老实的让每个Model符合NSCoding协议,再写出每个属性的归档&解档方法。其中最让人疼的是有些属性还需要强制转换。而一般情况下一个项目的Model数都超过了两位数,虽然不一定每个Model都需要归档功能,但是如果一个类里面属性太多的话,写起来会让人很郁闷的。
使用RunTime实现自动归档
如果读者看了我先前的两篇--打造强大的BaseModel文章,脑子了应该可以很快构思出使用RunTime和KVC来实现自动归档的思路。先用RunTime获取Model中所有属性名,再用KVC获取每一个属性的值。再调用encodeWithCoder就能实现归档了。嗯,这种想法不错,下面直接写代码吧。
还是和以前一样,先写一个返回该类所有属性名的方法
func getSelfProperty()->[String]{ //和description属性一样
var selfProperties = [String]()
var count:UInt32 = 0
let vars = class_copyIvarList(type(of: self), &count)
for i in 0..<count {
let t = ivar_getName((vars?[Int(i)])!)
if let n = NSString(cString: t!, encoding: String.Encoding.utf8.rawValue) as String?
{
selfProperties.append(n)
}
}
free(vars)
return selfProperties
}
和先前一样,利用Objc运行时的一系列方法可以从该类获取所有的属性名,下面是测试
@objcMembers class DemoArchiver:GrandModel {
var demoString:String?
var demoInt = 0
var demoFloat:Float = 0.0
var demoDate = NSDate()
override init(){}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
print(DemoArchiver().getSelfProperty())
//打印出**["demoString", "demoInt", "demoFloat", "demoDate"]**
下面来让GrandModel实现NSCoding协议,注意,实现NSCoding协议不能使用extension,因为指定构造器不能声明在extension中
class GrandModel:NSObject,NSCoding{
//归档方法
func encode(with aCoder: NSCoder) {
let item = type(of: self).init()
let properties = item.getSelfProperty()
for propertyName in properties{
let value = self.value(forKey: propertyName)
aCoder.encode(value, forKey: propertyName)
}
}
//解档方法
required init?(coder aDecoder: NSCoder) {
super.init()
let item = type(of: self).init()
let properties = item.getSelfProperty()
for propertyName in properties{
let value = aDecoder.decodeObject(forKey: propertyName)
self.setValue(value, forKey: propertyName)
}
}
}
没想到这么快就写好了,看起来也不难嘛,但是实际上这里这里存在一个显而易见的问题,就是归档方法中需要根据属性的类型调用不同的encode(属性类型)方法,本文的第一个例子里很清楚,对于Int类型的属性,需要调用aCoder.encodeInteger方法,Float和Double也不一样。如果统一使用 aCoder.encodeObject方法,就会造成数据类型丢失,
测试使用RunTime实现自动归档是否有效
这里可以测试一下。还是用文章开头的例子的哪个类,只不过需要去掉里面其他所有的方法只保留属性,并且添加了一些属性用来测试
class DemoArchiver:GrandModel {
var demoString:String?
var demoInt = 10
var demoFloat:Float = 11.0
var demoDouble:Double = 22.0
var demoDate = NSDate()
var demoRect = CGRect(x: 1, y: 1, width: 1, height: 1)
}
let demoTest = DemoArchiver()
demoTest.demoString = "ABCDEFG"
demoTest.demoFloat = 11.11
print(demoTest)
let a = NSKeyedArchiver.archivedDataWithRootObject(demoTest)
let b = NSKeyedUnarchiver.unarchiveObjectWithData(a)
print(b)
//打印结果为
**DemoArchiver:["demoDouble": 22, "demoInt": 10, "demoRect": NSRect: {{1, 1}, {1, 1}}, "demoString": ABCDEFG, "demoFloat": 11.11, "demoDate": 2016-03-12 07:57:57 +0000]
Optional(DemoArchiver:["demoDouble": 22, "demoInt": 10, "demoRect": NSRect: {{1, 1}, {1, 1}}, "demoString": ABCDEFG, "demoFloat": 11.11, "demoDate": 2016-03-12 07:57:57 +0000])**
实际上测试结果出乎我意料之外,非常完美,所有属性都成功地归档保存下来,解档后数据没有出现丢失的情况。对此我的分析是:这一切都是KVC的功劳。因为KVC取出的属性都是为AnyObject?类型,那么归档也就可以很方便地调用aCoder.encodeObject这个方法,所以数据以AnyObject类型保存。取出来时正好相反,用aDecoder.decodeObjectForKey这个角档方法取出来的数据类型都是AnyObject?类型的。然后KVC在组属性赋值并不需要知道每个属性是什么样的数据类型,都可以正确地赋值。难道事情就这样解决了吗?我们来看看下个测试用例
@objcMembers class DemoArchiver:GrandModel {
var demoString:String? = ""
var demoInt = 0
var demoFloat:Float = 0.0
var demoDate = NSDate()
var demoRect = CGRect(x: 1, y: 1, width: 1, height: 1)
}
let demoTest = DemoArchiver()
demoTest.demoString = "ABCDEFG"
demoTest.demoFloat = 11.11
print(demoTest)
let a = NSKeyedArchiver.archivedData(withRootObject: demoTest)
let b = NSKeyedUnarchiver.unarchiveObject(with: a)
print("------------归档后的数据------------")
print(b)
//打印结果为
**DemoArchiver:["demoFloat": 11.11, "demoString": ABCDEFG, "demoInt": 0, "demoRect": NSRect: {{1, 1}, {1, 1}}, "demoDate": 2018-03-29 09:14:29 +0000]
------------归档后的数据------------
Optional(DemoArchiver:["demoFloat": 11.11, "demoString": ABCDEFG, "demoInt": 0, "demoRect": NSRect: {{1, 1}, {1, 1}}, "demoDate": 2018-03-29 09:14:29 +0000])**
结果比预料中好了很多,nil的属性都可以正确打印出来。但是和以前一样,demoFloat:Float?这个属性又丢失了,这是很正常的,因为Objc不支持这种数据类型。读过我这系列文章的读者都可以明白。
从打印结果可以看出,归档后的数据Unarchiver后和原来的是一样的,说明GrandModel起到作用了。
那么如果属性类型是其他对象,或者是Array和字典类型呢?自动归档还能正常工作吗?答案是肯定的,只要该对象(Array或者Dict里保存的对象)都继承于GrandModel,都可以实现自动归档解档。
@objcMembers class DemoArchiver:GrandModel {
var demoString:String? = ""
var demoInt = 0
var demoFloat:Float = 0.0
var demoDate = NSDate()
var demoRect = CGRect(x: 1, y: 1, width: 1, height: 1)
var demoClass:demoArc?
var demoArray:[demoArc]?
var demoDict:[String:demoArc]?
}
@objcMembers class demoArc:GrandModel {
var daString:String? = "default"
var daInt:Int = 0
}
//下面测试
let demoTest = DemoArchiver()
demoTest.demoFloat = 11.11
demoTest.demoClass = demoArc()
demoTest.demoClass?.daInt = 8
demoTest.demoClass?.daString = "demoArc"
let a1 = demoArc()
let a2 = demoArc()
a1.daString = "a1"
a1.daInt = 1
a2.daInt = 2
a2.daString = "a2"
demoTest.demoArray = [a1,a2]
demoTest.demoDict = ["demo1":a1,"demo2":a2]
print(demoTest)
let a = NSKeyedArchiver.archivedData(withRootObject: demoTest)
let b = NSKeyedUnarchiver.unarchiveObject(with: a)
print("------------归档后的数据------------")
print(b)
//打印结果
**DemoArchiver:["demoRect": NSRect: {{1, 1}, {1, 1}}, "demoDate": 2018-03-29 09:31:59 +0000, "demoFloat": 11.11, "demoString": , "demoInt": 0, "demoArray": <_TtGCs23_ContiguousArrayStorageC12ConsoleSwift7demoArc_ 0x100f617a0>(
demoArc:["daInt": 1, "daString": a1],
demoArc:["daInt": 2, "daString": a2]
)
, "demoClass": demoArc:["daInt": 8, "daString": demoArc], "demoDict": {
demo1 = "demoArc:[\"daInt\": 1, \"daString\": a1]";
demo2 = "demoArc:[\"daInt\": 2, \"daString\": a2]";
}]
------------归档后的数据------------
Optional(DemoArchiver:["demoRect": NSRect: {{1, 1}, {1, 1}}, "demoDate": 2018-03-29 09:31:59 +0000, "demoFloat": 11.11, "demoString": , "demoInt": 0, "demoArray": <__NSArrayI 0x101851250>(
demoArc:["daInt": 1, "daString": a1],
demoArc:["daInt": 2, "daString": a2]
)
, "demoClass": demoArc:["daInt": 8, "daString": demoArc], "demoDict": {
demo1 = "demoArc:[\"daInt\": 1, \"daString\": a1]";
demo2 = "demoArc:[\"daInt\": 2, \"daString\": a2]";
}])**
结果完全符合预期。
总结
让Model自动归档是iOS Runtime和KVC强大威力的又一次体现。这个组合就像一把锋利的尖刀,可以准确高效地解决问题,避免写很多重复的代码。缺点就是效率比正常代码要低一点,但是我认为这完全是可以接受的。这三篇文章所有的相关代码都可以在我的Github里面找到(https://github.com/DuckDeck/iOSDemo),你们读者能给个Star.
网友评论