為便于下文讨論,提前建立父類<code>Biology</code>以及子類<code>Person</code>:
Biology:
Person:
補充說明
凡是在父類中定義的屬性或者變量,末尾都有InBiology标志;反之也成立
在iOS中一個自定義對象是無法直接存入到檔案中的,必須先轉化成二進制流才行。從對象到二進制資料的過程我們一般稱為對象的序列化(Serialization),也稱為歸檔(Archive)。同理,從二進制資料到對象的過程一般稱為反序列化或者反歸檔。
在序列化實作中不可避免的需要實作NSCoding以及NSCopying(非必須)協定的以下方法:
假設我們現在需要對直接繼承自NSObject的Person類進行序列化,代碼一般長這樣子:
似乎so easy?至少到目前為止是這樣的。但是請考慮以下問題:
若Person是個很大的類,有非常多的變量需要進行encode/decode處理呢?
若你的工程中有很多像Person的自定義類需要做序列化操作呢?
若Person不是直接繼承自NSObject而是有多層的父類呢?(請注意,序列化的原則是所有層級的父類的屬性變量也要需要序列化);
如果采用開始的傳統的序列化方式進行序列化,在碰到以上問題時容易暴露出以下缺陷(僅僅是缺陷,不能稱為問題):
工程代碼中備援代碼很多
父類層級複雜容易導緻遺漏點一些父類中的屬性變量
那是不是有更優雅的方案來回避以上問題呢?那是必須的。這裡我們将共同探讨使用runtime來實作一種接口簡潔并且十分通用的iOS序列化與反序列方案。
觀察上面的<code>initWithCoder</code>代碼我們可以發現,序列化與反序列化中最重要的環節是周遊類的變量,保證不能遺漏。
這裡需要特别注意的是: 編解碼的範圍不能僅僅是自身類的變量,還應當把除NSObject類外的所有層級父類的屬性變量也進行編解碼!
由此可見,這幾乎是個純體力活。而runtime在周遊變量這件事情上能為我們提供什麼幫助呢?我們可以通過runtime在運作時擷取自身類的所有變量進行編解碼;然後對父類進行遞歸,擷取除NSObject外每個層級父類的屬性(非私有變量),進行編解碼。
runtime中擷取某類的所有變量(屬性變量以及執行個體變量)API:
擷取某類的所有屬性變量API:
runtime的所有開放API都放在<code>objc/runtime.h</code>裡面。上面的一些資料類型有些同學可能沒見過,這裡我們先簡單地介紹一下,更詳細的介紹請自行查閱其他資料,強烈建議打開
Ivar是runtime對于變量的定義,本質是一個結構體:
ivar_name:變量名,對于一個給定的Ivar,可以通過<code>const char *ivar_getName(Ivar v)</code>函數獲得<code>char *</code>類型的變量名;
ivar_type: 變量類型,在runtime中變量類型用字元串表示,例如用@表示id類型,用i表示int類型...。這不在本文讨論之列。類似地,可以通過<code>const char *ivar_getTypeEncoding(Ivar v)</code>函數獲得變量類型;
ivar_offset: 基位址偏移位元組數,可以不用理會
擷取所有變量的代碼一般長這樣子:
objc_property_t是runtime對于屬性變量的定義,本質上也是一個結構體(事實上OC是對C的封裝,大多數類型的本質都是C結構體)。在<code>runtime.h</code>頭檔案中隻有<code>typedef struct objc_property *objc_property_t</code>,并沒有更詳細的結構體介紹。雖然runtime的源碼是開源的,但這裡并不打算深入介紹,這并不影響我們今天的主題。與Ivar的應用同理,擷取類的屬性變量的代碼一般長這樣子:
有了前面兩節的鋪墊,到這裡自然就水到渠成了。我們可以在<code>initWithCoder:</code>以及<code>encoderWithCoder:</code>中周遊類的所有變量,取得變量名作為KEY值,最後使用KVC強制取得或者指派給對象。于是我們可以得到如下的自動序列化與發序列化代碼,關鍵部分有注釋:
上面代碼有個缺陷,在擷取變量時都是指定目前類,也就是<code>[self class]</code>。當你的Model對象并不是直接繼承自NSObject時容易遺漏掉父類的屬性。請牢記3.1節我們提到的:
是以在上面代碼的基礎上我們我們需要注意一下細節,設一個指針,先指向本身類,處理完指向SuperClass,處理完再指向SuperClass的SuperClass...。代碼如下(這裡僅以<code>encodeWithCoder:</code>為例,畢竟<code>initWithCoder:</code>同理):
這樣真的結束了嗎?不是的。當你的跑上面的代碼時程式有可能會crash掉,crash的地方在<code>[self valueForKey:key]</code>這一句上。原來是這裡的KVC無法擷取到父類的私有變量(即執行個體變量)。是以,在處理到父類時不能簡單粗暴地使用<code>class_copyIvarList</code>,而隻能取父類的屬性變量。這時候3.2節部分的<code>class_copyPropertyList</code>就派上用場了。在處理父類時用後者代替前者。
2016.0804補充 在最近的iOS中列印propertyList會發現有 <code>superClass</code>、<code>description</code>、<code>debugDescription</code>、<code>hash</code>等四個屬性。對這幾個屬性進行encode操作會導緻crash。是以在encode前需要屏蔽掉這些key
于是最終的代碼(額~其實還不算最終):
在邏輯上,上面的代碼應該是目前為止比較完美的自動序列化與反序列解決方案了。即使某個類的繼承深度極其深,變量極其多,序列化的代碼也就以上這些。但是我們回到文章第二節提出的幾點場景假設,其中有一點提到:
如果是在以上場景下,每個Model類都需要寫一次上面的代碼。這在一定程度上也造成備援了。同時,你也會覺得這篇文章的标題就是瞎扯淡,根本就不是一行代碼的事。上面的代碼備援,我這種對代碼有很強潔癖的程式旺是萬萬接受不了的。那就再封裝一層!這裡我采用宏的方式将上述代碼濃縮成一行,放到一個叫WZLSerializeKit.h的頭檔案中:
之後需要序列化的地方隻要兩步:1、import "WZLSerializeKit.h" 2、調用<code>WZLSERIALIZE_CODER_DECODER();</code>即可。兩個字:清爽。
此外,<code>copyWithZone</code>中同樣可以用相同的原理對變量進行自動化copy。同樣地,我們也可以用一個宏封裝掉<code>copyWithZone</code>方法。這裡就不再贅述。
值得一提的是,以上代碼我已經放到我的Github中,并且提供了CocoaPods支援。使用的時候隻需要pod `WZLSerializeKit`。點 此處 跳轉到我的Github.