一、變數宣告
為便於下文討論,提前建立父類Biology
以及子類Person
:
Biology:
1 2 3 4 5 6 7 8 9 |
@interface Biology : NSObject { NSInteger *_hairCountInBiology; } @property (nonatomic, copy) NSString *introInBiology; @end @implementation Biology @end |
Person:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#import #import "Biology.h" #import @interface Person : Biology { NSString *_father; } @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) NSInteger age; @end @implementation Person @end |
補充說明
凡是在父類中定義的屬性或者變數,末尾都有InBiology標誌;反之也成立
二、問題引入
在iOS中一個自定義物件是無法直接存入到檔案中的,必須先轉化成二進位制流才行。從物件到二進位制資料的過程我們一般稱為物件的序列化(Serialization),也稱為歸檔(Archive)。同理,從二進位制資料到物件的過程一般稱為反序列化或者反歸檔。
在序列化實現中不可避免的需要實現NSCoding以及NSCopying(非必須)協議的以下方法:
1 2 3 |
- (id)initWithCoder:(NSCoder *)coder; - (void)encodeWithCoder:(NSCoder *)coder; - (id)copyWithZone:(NSZone *)zone; |
假設我們現在需要對直接繼承自NSObject的Person類進行序列化,程式碼一般長這樣子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//對變數編碼 - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:self.name forKey:@"name"]; [coder encodeObject:@(self.age) forKey:@"age"]; [coder encodeObject:_father forKey:@"_father"]; //... ... other instance variables } //對變數解碼 - (id)initWithCoder:(NSCoder *)coder { self.name = [coder decodeObjectForKey:@"name"]; self.age = [[coder decodeObjectForKey:@"age"] integerValue]; _father = [coder decodeObjectForKey:@"_father"]; //... ... other instance variables |
似乎so easy?至少到目前為止是這樣的。但是請考慮以下問題:
- 若Person是個很大的類,有非常多的變數需要進行encode/decode處理呢?
- 若你的工程中有很多像Person的自定義類需要做序列化操作呢?
- 若Person不是直接繼承自NSObject而是有多層的父類呢?(請注意,序列化的原則是所有層級的父類的屬性變數也要需要序列化);
如果採用開始的傳統的序列化方式進行序列化,在碰到以上問題時容易暴露出以下缺陷(僅僅是缺陷,不能稱為問題):
- 工程程式碼中冗餘程式碼很多
- 父類層級複雜容易導致遺漏點一些父類中的屬性變數
那是不是有更優雅的方案來回避以上問題呢?那是必須的。這裡我們將共同探討使用runtime來實現一種介面簡潔並且十分通用的iOS序列化與反序列方案。
三、runtime: iOS序列化與反序列化利器
3.1 總體思路
觀察上面的initWithCoder
程式碼我們可以發現,序列化與反序列化中最重要的環節是遍歷類的變數,保證不能遺漏。
這裡需要特別注意的是:
編解碼的範圍不能僅僅是自身類的變數,還應當把除NSObject類外的所有層級父類的屬性變數也進行編解碼!
由此可見,這幾乎是個純體力活。而runtime在遍歷變數這件事情上能為我們提供什麼幫助呢?我們可以通過runtime在執行時獲取自身類的所有變數進行編解碼;然後對父類進行遞迴,獲取除NSObject外每個層級父類的屬性(非私有變數),進行編解碼。
3.2 使用runtime獲取變數以及屬性
runtime中獲取某類的所有變數(屬性變數以及例項變數)API:
1 |
Ivar *class_copyIvarList(Class cls, unsigned int *outCount) |
獲取某類的所有屬性變數API:
1 |
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount) |
runtime的所有開放API都放在objc/runtime.h
裡面。上面的一些資料型別有些同學可能沒見過,這裡我們先簡單地介紹一下,更詳細的介紹請自行查閱其他資料,強烈建議開啟
Ivar是runtime對於變數的定義,本質是一個結構體:
1 2 3 4 5 6 7 8 9 |
struct objc_ivar { char *ivar_name; char *ivar_type; int ivar_offset; #ifdef __LP64__ int space; #endif } typedef struct objc_ivar *Ivar; |
- ivar_name:變數名,對於一個給定的Ivar,可以通過
const char *ivar_getName(Ivar v)
函式獲得char *
型別的變數名; - ivar_type: 變數型別,在runtime中變數型別用字串表示,例如用@表示id型別,用i表示int型別…。這不在本文討論之列。類似地,可以通過
const char *ivar_getTypeEncoding(Ivar v)
函式獲得變數型別; - ivar_offset: 基地址偏移位元組數,可以不用理會
獲取所有變數的程式碼一般長這樣子:
1234unsigned int numIvars; //成員變數個數Ivar *vars = class_copyIvarList(NSClassFromString(@"UIView"), &numIvars);NSString *key=nil;for(int i = 0; i
objc_property_t是runtime對於屬性變數的定義,本質上也是一個結構體(事實上OC是對C的封裝,大多數型別的本質都是C結構體)。在runtime.h
標頭檔案中只有typedef struct objc_property *objc_property_t
,並沒有更詳細的結構體介紹。雖然runtime的原始碼是開源的,但這裡並不打算深入介紹,這並不影響我們今天的主題。與Ivar的應用同理,獲取類的屬性變數的程式碼一般長這樣子:
123unsigned int outCount, i;objc_property_t *properties = class_copyPropertyList([self class], &outCount);for (i = 0; i3.3 用runtime實現序列化與反序列化
有了前面兩節的鋪墊,到這裡自然就水到渠成了。我們可以在
initWithCoder:
以及encoderWithCoder:
中遍歷類的所有變數,取得變數名作為KEY值,最後使用KVC強制取得或者賦值給物件。於是我們可以得到如下的自動序列化與發序列化程式碼,關鍵部分有註釋:1234567@implementation Person//解碼- (id)initWithCoder:(NSCoder *)coder{unsigned int iVarCount = 0;Ivar *iVarList = class_copyIvarList([self class], &iVarCount);//取得變數列表,[self class]表示對自身類進行操作for (int i = 0; i3.4 優化
上面程式碼有個缺陷,在獲取變數時都是指定當前類,也就是
[self class]
。當你的Model物件並不是直接繼承自NSObject時容易遺漏掉父類的屬性。請牢記3.1節我們提到的:編解碼的範圍不能僅僅是自身類的變數,還應當把除NSObject類外的所有層級父類的屬性變數也進行編解碼!
因此在上面程式碼的基礎上我們我們需要注意一下細節,設一個指標,先指向本身類,處理完指向SuperClass,處理完再指向SuperClass的SuperClass…。程式碼如下(這裡僅以encodeWithCoder:
為例,畢竟initWithCoder:
同理):
1 2 3 4 5 6 7 |
- (void)encodeWithCoder:(NSCoder *)coder { Class cls = [self class]; while (cls != [NSObject class]) {//對NSObject的變數不做處理 unsigned int iVarCount = 0; Ivar *ivarList = class_copyIvarList([cls class], &iVarCount);/*變數列表,含屬性以及私有變數*/ for (int i = 0; i |
這樣真的結束了嗎?不是的。當你的跑上面的程式碼時程式有可能會crash掉,crash的地方在[self objectForKey:key]
這一句上。原來是這裡的KVC無法獲取到父類的私有變數(即例項變數)。因此,在處理到父類時不能簡單粗暴地使用class_copyIvarList
,而只能取父類的屬性變數。這時候3.2節部分的class_copyPropertyList
就派上用場了。在處理父類時用後者代替前者。於是最終的程式碼(額~其實還不算最終):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
- (id)initWithCoder:(NSCoder *)coder { NSLog(@"%s",__func__); Class cls = [self class]; while (cls != [NSObject class]) { /*判斷是自身類還是父類*/ BOOL bIsSelfClass = (cls == [self class]); unsigned int iVarCount = 0; unsigned int propVarCount = 0; unsigned int sharedVarCount = 0; Ivar *ivarList = bIsSelfClass ? class_copyIvarList([cls class], &iVarCount) : NULL;/*變數列表,含屬性以及私有變數*/ objc_property_t *propList = bIsSelfClass ? NULL : class_copyPropertyList(cls, &propVarCount);/*屬性列表*/ sharedVarCount = bIsSelfClass ? iVarCount : propVarCount; for (int i = 0; i |
3.5 最終的封裝
在邏輯上,上面的程式碼應該是目前為止比較完美的自動序列化與反序列解決方案了。即使某個類的繼承深度極其深,變數極其多,序列化的程式碼也就以上這些。但是我們回到文章第二節提出的幾點場景假設,其中有一點提到:
若你的工程中有很多像Person的自定義類需要做序列化操作呢?
如果是在以上場景下,每個Model類都需要寫一次上面的程式碼。這在一定程度上也造成冗餘了。同時,你也會覺得這篇文章的標題就是瞎扯淡,根本就不是一行程式碼的事。上面的程式碼冗餘,我這種對程式碼有很強潔癖的程式旺是萬萬接受不了的。那就再封裝一層!這裡我採用巨集的方式將上述程式碼濃縮成一行,放到一個叫WZLSerializeKit.h的標頭檔案中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#define WZLSERIALIZE_CODER_DECODER() - (id)initWithCoder:(NSCoder *)coder { NSLog(@"%s",__func__); Class cls = [self class]; while (cls != [NSObject class]) { /*判斷是自身類還是父類*/ BOOL bIsSelfClass = (cls == [self class]); unsigned int iVarCount = 0; unsigned int propVarCount = 0; unsigned int sharedVarCount = 0; Ivar *ivarList = bIsSelfClass ? class_copyIvarList([cls class], &iVarCount) : NULL;/*變數列表,含屬性以及私有變數*/ objc_property_t *propList = bIsSelfClass ? NULL : class_copyPropertyList(cls, &propVarCount);/*屬性列表*/ sharedVarCount = bIsSelfClass ? iVarCount : propVarCount; for (int i = 0; i |
之後需要序列化的地方只要兩步:1、import “WZLSerializeKit.h” 2、呼叫WZLSERIALIZE_CODER_DECODER();
即可。兩個字:清爽。
此外,copyWithZone
中同樣可以用相同的原理對變數進行自動化copy。同樣地,我們也可以用一個巨集封裝掉copyWithZone
方法。這裡就不再贅述。
值得一提的是,以上程式碼我已經放到我的Github中,並且提供了CocoaPods支援。使用的時候只需要pod WZLSerializeKit
。點 此處 跳轉到我的Github.