本文是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.