本文是「打造強大的BaseModel」的篇三篇,第一篇文章請見此:讓Model自我描述 。第二篇文章請見此:讓Model自動轉換。相對於讓Model實現自我描述和自動轉換,讓Model實現自動歸檔會難一點(事實後來我發現一點也不難)。我相信能夠好好看完這三篇文章的人,絕對是有大收穫的。
什麼是iOS的歸檔
歸檔–NSKeyedArchiver,是iOS開發中基本的資料儲存方式之一,和其他的資料儲存方式相比,歸檔不僅能夠儲存任意型別的資料,而且使用起來也很簡單。歸檔能將資料處理成NSData的形式,所以很容易以檔案的形式儲存在APP的沙盒中,而解歸和歸檔相反,它是將儲存在APP沙盒的歸檔檔案逆歸檔,轉換成歸檔前的狀態。
傳統的iOS歸檔方式
要想讓一個自定義物件可以使用歸檔,必須要讓其符合NSCoding協議,
1 2 3 4 5 |
public protocol NSCoding { public func encodeWithCoder(aCoder: NSCoder) public init?(coder aDecoder: NSCoder) // NS_DESIGNATED_INITIALIZER } @end |
上面的程式碼是iOS中NSCoding協議的定義。裡面包含兩個方法,其中一個是構造器。第一個方法
1 |
public func encodeWithCoder(aCoder: NSCoder) |
就是歸檔方法,它是為了告訴NSKeyedArchiver物件如何將資料歸檔成檔案的。第二個方法(構造器)
1 |
public init?(coder aDecoder: NSCoder) // NS_DESIGNATED_INITIALIZER |
就是解檔方法了。它是告訴NSKeyedUnArchiver是如何將歸檔好的物件解檔成原來的資料的
下面來看看傳統的iOS歸檔方式,先定義一個類,讓其符合NSCoding協議
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class DemoArchiver:GrandModel,NSCoding { var demoString:String? var demoInt = 100 var demoFloat:Float = 0.0 var demoDate = NSDate() required init(){ } @objc func encodeWithCoder(aCoder: NSCoder) {//歸檔需要實現的方法 aCoder.encodeObject(demoString, forKey: "demoString") aCoder.encodeInteger(demoInt, forKey: "demoInt") aCoder.encodeFloat(demoFloat, forKey: "demoFloat") aCoder.encodeObject(demoDate, forKey: "demoDate") } @objc required init?(coder aDecoder: NSCoder) {//解檔需要實現的構造器 demoString = aDecoder.decodeObjectForKey("demoString") as? String demoInt = aDecoder.decodeIntegerForKey("demoInt") demoFloat = aDecoder.decodeFloatForKey("demoFloat") demoDate = aDecoder.decodeObjectForKey("demoDate") as! NSDate //存在強制轉換情況 } } |
我們需要在正確地重寫這兩個方法。這裡面最需要注意的點有兩個,一是不要把資料型別搞錯。二是key名不要弄錯了。然後下面開始測試
1 2 3 4 5 6 7 8 9 10 |
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,見Accessorize)可以使用,只需要把所有屬性放進去,就可以生成所有屬性的歸檔解檔方法。遺憾的是Swift目前還沒有這種工具可以用(或者有了但是我不知道),只有老實的讓每個Model符合NSCoding協議,再寫出每個屬性的歸檔&解檔方法。其中最讓人疼的是有些屬性還需要強制轉換。而一般情況下一個專案的Model數都超過了兩位數,雖然不一定每個Model都需要歸檔功能,但是如果一個類裡面屬性太多的話,寫起來會讓人很鬱悶的。
使用RunTime實現自動歸檔
如果讀者看了我先前的兩篇–打造強大的BaseModel文章,腦子了應該可以很快構思出使用RunTime和KVC來實現自動歸檔的思路。先用RunTime獲取Model中所有屬性名,再用KVC獲取每一個屬性的值。再呼叫encodeWithCoder就能實現歸檔了。嗯,這種想法不錯,下面直接寫程式碼吧。
還是和以前一樣,先寫一個返回該類所有屬性名的方法
1 2 3 4 5 6 7 8 9 10 11 12 |
func getSelfProperty()->[String]{ //這裡不能用靜態方法,因為父類沒法獲取執行時的子類,只能獲取執行時子類的例項 var selfProperties = [String]() let count:UnsafeMutablePointer = UnsafeMutablePointer() var properties = class_copyPropertyList(self.dynamicType, count) while properties.memory.debugDescription != "0x0000000000000000"{ let t = property_getName(properties.memory) let n = NSString(CString: t, encoding: NSUTF8StringEncoding) selfProperties.append(n! as String) properties = properties.successor() } return selfProperties } |
和先前一樣,利用Objc執行時的一系列方法可以從該類獲取所有的屬性名,下面是測試
1 2 3 4 5 6 7 8 |
class DemoArchiver:GrandModel { var demoString:String? var demoInt = 0 var demoFloat:Float = 0.0 var demoDate = NSDate() } print(DemoArchiver().getSelfProperty()) //列印出**["demoString", "demoInt", "demoFloat", "demoDate"]** |
下面來讓GrandModel實現NSCoding協議,注意,實現NSCoding協議不能使用extension,因為指定構造器不能宣告在extension中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class GrandModel:NSObject,NSCoding{ //歸檔方法 func encodeWithCoder(aCoder: NSCoder) { let item = self.dynamicType.init() let properties = item.getSelfProperty() for propertyName in properties{ let value = self.valueForKey(propertyName) aCoder.encodeObject(value, forKey: propertyName) } } //解檔方法 required init?(coder aDecoder: NSCoder) { super.init() let item = self.dynamicType.init() let properties = item.getSelfProperty() for propertyName in properties{ let value = aDecoder.decodeObjectForKey(propertyName) self.setValue(value, forKey: propertyName) } } } |
沒想到這麼快就寫好了,看起來也不難嘛,但是實際上這裡這裡存在一個顯而易見的問題,就是歸檔方法中需要根據屬性的型別呼叫不同的encode(屬性型別)方法,本文的第一個例子裡很清楚,對於Int型別的屬性,需要呼叫aCoder.encodeInteger方法,Float和Double也不一樣。如果統一使用 aCoder.encodeObject方法,就會造成資料型別丟失,
測試使用RunTime實現自動歸檔是否有效
這裡可以測試一下。還是用文章開頭的例子的哪個類,只不過需要去掉裡面其他所有的方法只保留屬性,並且新增了一些屬性用來測試
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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在組屬性賦值並不需要知道每個屬性是什麼樣的資料型別,都可以正確地賦值。難道事情就這樣解決了嗎?我們來看看下個測試用例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class DemoArchiver:GrandModel { var demoString:String? var demoInt = 10 var demoFloat:Float? var demoDouble:Double = 12 var demoDate:NSDate? } let demoTest = DemoArchiver() demoTest.demoFloat = 11.11 print(demoTest) let a = NSKeyedArchiver.archivedDataWithRootObject(demoTest) let b = NSKeyedUnarchiver.unarchiveObjectWithData(a) print(b) //列印結果為 **DemoArchiver:["demoDouble": 12, "demoInt": 10, "demoString": nil, "demoDate": nil] Optional(DemoArchiver:["demoDouble": 12, "demoInt": 10, "demoString": nil, "demoDate": nil])** |
結果比預料中好了很多,nil的屬性都可以正確列印出來。但是和以前一樣,demoFloat:Float?這個屬性又丟失了,這是很正常的,因為Objc不支援這種資料型別。讀過我這系列文章的讀者都可以明白。
那麼如果屬性型別是其他物件,或者是Array和字典型別呢?自動歸檔還能正常工作嗎?答案是肯定的,只要該物件(Array或者Dict裡儲存的物件)都繼承於GrandModel,都可以實現自動歸檔解檔。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
class DemoArchiver:GrandModel { var demoString:String? var demoInt = 10 var demoFloat:Float? var demoDouble:Double = 12 var demoDate:NSDate? var demoClass:demoArc? var demoArray:[demoArc]? var demoDict:[String:demoArc]? } class demoArc:GrandModel { var daString:String? 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.archivedDataWithRootObject(demoTest) let b = NSKeyedUnarchiver.unarchiveObjectWithData(a) print(b) //列印結果 **DemoArchiver:["demoDouble": 12, "demoInt": 10, "demoClass": demoArc:["daString": demoArc, "daInt": 8], "demoArray": ( "demoArc:["daString": a1, "daInt": 1]", "demoArc:["daString": a2, "daInt": 2]" ), "demoDict": { demo1 = "demoArc:["daString": a1, "daInt": 1]"; demo2 = "demoArc:["daString": a2, "daInt": 2]"; }, "demoDate": nil, "demoString": nil] Optional(DemoArchiver:["demoDouble": 12, "demoInt": 10, "demoClass": demoArc:["daString": demoArc, "daInt": 8], "demoArray": ( "demoArc:["daString": a1, "daInt": 1]", "demoArc:["daString": a2, "daInt": 2]" ), "demoDict": { demo1 = "demoArc:["daString": a1, "daInt": 1]"; demo2 = "demoArc:["daString": a2, "daInt": 2]"; }, "demoDate": nil, "demoString": nil])** |
結果完全符合預期。
總結
讓Model自動歸檔是iOS Runtime和KVC強大威力的又一次體現。這個組合就像一把鋒利的尖刀,可以準確高效地解決問題,避免寫很多重複的程式碼。缺點就是效率比正常程式碼要低一點,但是我認為這完全是可以接受的。這三篇文章所有的相關程式碼都可以在我的Github裡面找到GrandModel,希望讀者能給個Star。