一行代碼實現(xiàn)iOS序列化與反序列化(runtime)

一、變量聲明

為便于下文討論,提前創(chuàng)建父類Biology以及子類Person:

Biology:
<pre>@interface Biology : NSObject
{
NSInteger *_hairCountInBiology;
}
@property (nonatomic, copy) NSString *introInBiology;
@end

@implementation Biology
@end
</pre>

Person:
<pre>

import <Foundation/Foundation.h>

import "Biology.h"

import <objc/runtime.h>

@interface Person : Biology
{
NSString *_father;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end

@implementation Person

@end
</pre>

補充說明
凡是在父類中定義的屬性或者變量,末尾都有InBiology標志;反之也成立

二、問題引入

在iOS中一個自定義對象是無法直接存入到文件中的,必須先轉(zhuǎn)化成二進制流才行。從對象到二進制數(shù)據(jù)的過程我們一般稱為對象的序列化(Serialization),也稱為歸檔(Archive)。同理,從二進制數(shù)據(jù)到對象的過程一般稱為反序列化或者反歸檔。
在序列化實現(xiàn)中不可避免的需要實現(xiàn)NSCoding以及NSCopying(非必須)協(xié)議的以下方法:
<pre>

  • (id)initWithCoder:(NSCoder *)coder;
  • (void)encodeWithCoder:(NSCoder *)coder;
  • (id)copyWithZone:(NSZone *)zone;
    </pre>

假設(shè)我們現(xiàn)在需要對直接繼承自NSObject的Person類進行序列化,代碼一般長這樣子:

<pre>
//對變量編碼

  • (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
    </pre>

似乎so easy?至少到目前為止是這樣的。但是請考慮以下問題:

若Person是個很大的類,有非常多的變量需要進行encode/decode處理呢?
若你的工程中有很多像Person的自定義類需要做序列化操作呢?
若Person不是直接繼承自NSObject而是有多層的父類呢?(請注意,序列化的原則是所有層級的父類的屬性變量也要需要序列化);
如果采用開始的傳統(tǒng)的序列化方式進行序列化,在碰到以上問題時容易暴露出以下缺陷(僅僅是缺陷,不能稱為問題):

工程代碼中冗余代碼很多
父類層級復(fù)雜容易導(dǎo)致遺漏點一些父類中的屬性變量
那是不是有更優(yōu)雅的方案來回避以上問題呢?那是必須的。這里我們將共同探討使用runtime來實現(xiàn)一種接口簡潔并且十分通用的iOS序列化與反序列方案。

三、runtime: iOS序列化與反序列化利器

3.1 總體思路

觀察上面的initWithCoder代碼我們可以發(fā)現(xiàn),序列化與反序列化中最重要的環(huán)節(jié)是遍歷類的變量,保證不能遺漏。
<pre>
這里需要特別注意的是:
編解碼的范圍不能僅僅是自身類的變量,還應(yīng)當(dāng)把除NSObject類外的所有層級父類的屬性變量也進行編解碼!
</pre>

由此可見,這幾乎是個純體力活。而runtime在遍歷變量這件事情上能為我們提供什么幫助呢?我們可以通過runtime在運行時獲取自身類的所有變量進行編解碼;然后對父類進行遞歸,獲取除NSObject外每個層級父類的屬性(非私有變量),進行編解碼。

3.2 使用runtime獲取變量以及屬性

runtime中獲取某類的所有變量(屬性變量以及實例變量)API:
<pre>
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
</pre>

獲取某類的所有屬性變量API:
<pre>
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
</pre>

runtime的所有開放API都放在objc/runtime.h里面。上面的一些數(shù)據(jù)類型有些同學(xué)可能沒見過,這里我們先簡單地介紹一下,更詳細的介紹請自行查閱其他資料,強烈建議打開
Ivar是runtime對于變量的定義,本質(zhì)是一個結(jié)構(gòu)體:
<pre>
struct objc_ivar {
char *ivar_name;
char *ivar_type;
int ivar_offset;

ifdef LP64

int space;

endif

}
typedef struct objc_ivar *Ivar;
</pre>

ivar_name:變量名,對于一個給定的Ivar,可以通過const char *ivar_getName(Ivar v)函數(shù)獲得char *類型的變量名;

ivar_type: 變量類型,在runtime中變量類型用字符串表示,例如用@表示id類型,用i表示int類型...。這不在本文討論之列。類似地,可以通過const char *ivar_getTypeEncoding(Ivar v)函數(shù)獲得變量類型;

ivar_offset: 基地址偏移字節(jié)數(shù),可以不用理會
獲取所有變量的代碼一般長這樣子:
<pre>
unsigned int numIvars; //成員變量個數(shù)
Ivar *vars = class_copyIvarList(NSClassFromString(@"UIView"), &numIvars);
NSString *key=nil;
for(int i = 0; i < numIvars; i++) {
Ivar thisIvar = vars[i];
key = [NSString stringWithUTF8String:ivar_getName(thisIvar)]; //獲取成員變量的名字
NSLog(@"variable name :%@", key);
key = [NSString stringWithUTF8String:ivar_getTypeEncoding(thisIvar)]; //獲取成員變量的數(shù)據(jù)類型
NSLog(@"variable type :%@", key);
}
free(vars);//記得釋放掉
</pre>

objc_property_t是runtime對于屬性變量的定義,本質(zhì)上也是一個結(jié)構(gòu)體(事實上OC是對C的封裝,大多數(shù)類型的本質(zhì)都是C結(jié)構(gòu)體)。在runtime.h頭文件中只有typedef struct objc_property *objc_property_t,并沒有更詳細的結(jié)構(gòu)體介紹。雖然runtime的源碼是開源的,但這里并不打算深入介紹,這并不影響我們今天的主題。與Ivar的應(yīng)用同理,獲取類的屬性變量的代碼一般長這樣子:
<pre>
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList([self class], &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
NSString *propertyName = [[[NSString alloc] initWithCString:property_getName(property)] ;
NSLog(@"property name:%@", propertyName);
}
free(properties);
</pre>

3.3 用runtime實現(xiàn)序列化與反序列化

有了前面兩節(jié)的鋪墊,到這里自然就水到渠成了。我們可以在initWithCoder:以及encoderWithCoder:中遍歷類的所有變量,取得變量名作為KEY值,最后使用KVC強制取得或者賦值給對象。于是我們可以得到如下的自動序列化與發(fā)序列化代碼,關(guān)鍵部分有注釋:
<pre>
@implementation Person
//解碼

  • (id)initWithCoder:(NSCoder *)coder
    {
    unsigned int iVarCount = 0;
    Ivar *iVarList = class_copyIvarList([self class], &iVarCount);//取得變量列表,[self class]表示對自身類進行操作
    for (int i = 0; i < iVarCount; i++) {
    Ivar var = *(iVarList + i);
    const char * varName = ivar_getName(var);//取得變量名字,將作為key
    NSString *key = [NSString stringWithUTF8String:varName];
    //decode
    id value = [coder decodeObjectForKey:key];//解碼
    if (value) {
    [self setValue:value forKey:key];//使用KVC強制寫入到對象中
    }
    }
    free(iVarList);//記得釋放內(nèi)存
    return self;
    }
    //編碼
    • (void)encodeWithCoder:(NSCoder *)coder
      {
      unsigned int varCount = 0;
      Ivar *ivarList = class_copyIvarList([self class], &varCount);
      for (int i = 0; i < varCount; i++) {
      Ivar var = *(ivarList + i);
      const char *varName = ivar_getName(var);
      NSString *key = [NSString stringWithUTF8String:varName];
      id varValue = [self valueForKey:key];//使用KVC獲取key對應(yīng)的變量值
      if (varValue) {
      [coder encodeObject:varValue forKey:key];
      }
      }
      free(ivarList);
      }
      </pre>

3.4 優(yōu)化

上面代碼有個缺陷,在獲取變量時都是指定當(dāng)前類,也就是[self class]。當(dāng)你的Model對象并不是直接繼承自NSObject時容易遺漏掉父類的屬性。請牢記3.1節(jié)我們提到的:

<pre>編解碼的范圍不能僅僅是自身類的變量,還應(yīng)當(dāng)把除NSObject類外的所有層級父類的屬性變量也進行編解碼!</pre>

因此在上面代碼的基礎(chǔ)上我們我們需要注意一下細節(jié),設(shè)一個指針,先指向本身類,處理完指向SuperClass,處理完再指向SuperClass的SuperClass...。代碼如下(這里僅以encodeWithCoder:為例,畢竟initWithCoder:同理):

<pre>

  • (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 < iVarCount; i++) {
    const char varName = ivar_getName((ivarList + i));
    NSString key = [NSString stringWithUTF8String:varName];
    /
    valueForKey只能獲取本類所有變量以及所有層級父類的屬性,不包含任何父類的私有變量(會崩潰)*/
    id varValue = [self valueForKey:key];
    if (varValue) {
    [coder encodeObject:varValue forKey:key];
    }
    }
    free(ivarList);
    cls = class_getSuperclass(cls); //指針指向當(dāng)前類的父類
    }
    }
    </pre>

這樣真的結(jié)束了嗎?不是的。當(dāng)你的跑上面的代碼時程序有可能會crash掉,crash的地方在[self valueForKey:key]這一句上。原來是這里的KVC無法獲取到父類的私有變量(即實例變量)。因此,在處理到父類時不能簡單粗暴地使用class_copyIvarList,而只能取父類的屬性變量。這時候3.2節(jié)部分的class_copyPropertyList就派上用場了。在處理父類時用后者代替前者。

<pre>
在最近的iOS中打印propertyList會發(fā)現(xiàn)有 superClass、description、debugDescription、hash等四個屬性。
對這幾個屬性進行encode操作會導(dǎo)致crash。因此在encode前需要屏蔽掉這些key
</pre>

于是最終的代碼(額~其實還不算最終):
<pre>

  • (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 < sharedVarCount; i++) {  
          const char *varName = bIsSelfClass ? ivar_getName(*(ivarList + i)) : property_getName(*(propList + i)); 
          NSString *key = [NSString stringWithUTF8String:varName];   
          id varValue = [coder decodeObjectForKey:key]; 
          NSArray *filters = @[@"superclass", @"description", @"debugDescription", @"hash"];   
          if (varValue && [filters containsObject:key] == NO) { 
              [self setValue:varValue forKey:key];    
          }   
      }   
      free(ivarList); 
      free(propList); 
      cls = class_getSuperclass(cls); 
    

    }
    return self;
    }

  • (void)encodeWithCoder:(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 < sharedVarCount; i++) {  
          const char *varName = bIsSelfClass ? ivar_getName(*(ivarList + i)) : property_getName(*(propList + i)); 
          NSString *key = [NSString stringWithUTF8String:varName];    
          /*valueForKey只能獲取本類所有變量以及所有層級父類的屬性,不包含任何父類的私有變量(會崩潰)*/  
          id varValue = [self valueForKey:key];
          NSArray *filters = @[@"superclass", @"description", @"debugDescription", @"hash"];    
          if (varValue && [filters containsObject:key] == NO) { 
              [coder encodeObject:varValue forKey:key];   
          }   
      }   
      free(ivarList); 
      free(propList); 
      cls = class_getSuperclass(cls); 
    

    }
    }
    </pre>

3.5 最終的封裝

在邏輯上,上面的代碼應(yīng)該是目前為止比較完美的自動序列化與反序列解決方案了。即使某個類的繼承深度極其深,變量極其多,序列化的代碼也就以上這些。但是我們回到文章第二節(jié)提出的幾點場景假設(shè),其中有一點提到:
<pre>若你的工程中有很多像Person的自定義類需要做序列化操作呢?</pre>

如果是在以上場景下,每個Model類都需要寫一次上面的代碼。這在一定程度上也造成冗余了。同時,你也會覺得這篇文章的標題就是瞎扯淡,根本就不是一行代碼的事。上面的代碼冗余,我這種對代碼有很強潔癖的程序旺是萬萬接受不了的。那就再封裝一層!這里我采用宏的方式將上述代碼濃縮成一行,放到一個叫WZLSerializeKit.h的頭文件中:

<pre>

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 < sharedVarCount; i++) {
    const char varName = bIsSelfClass ? ivar_getName((ivarList + i)) : property_getName(
    (propList + i));
    NSString *key = [NSString stringWithUTF8String:varName];
    id varValue = [coder decodeObjectForKey:key];
    NSArray *filters = @[@"superclass", @"description", @"debugDescription", @"hash"];
    if (varValue && [filters containsObject:key] == NO) {
    [self setValue:varValue forKey:key];
    }
    }
    free(ivarList);
    free(propList);
    cls = class_getSuperclass(cls);
    }
    return self;
    }
    \
  • (void)encodeWithCoder:(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 < sharedVarCount; i++) {
    const char varName = bIsSelfClass ? ivar_getName((ivarList + i)) : property_getName(
    (propList + i));
    NSString key = [NSString stringWithUTF8String:varName];
    /
    valueForKey只能獲取本類所有變量以及所有層級父類的屬性,不包含任何父類的私有變量(會崩潰)*/
    id varValue = [self valueForKey:key];
    NSArray *filters = @[@"superclass", @"description", @"debugDescription", @"hash"];
    if (varValue && [filters containsObject:key] == NO) {
    [coder encodeObject:varValue forKey:key];
    }
    }
    free(ivarList);
    free(propList);
    cls = class_getSuperclass(cls);
    }
    }
    </pre>

之后需要序列化的地方只要兩步:1、import "WZLSerializeKit.h" 2、調(diào)用WZLSERIALIZE_CODER_DECODER();
即可。兩個字:清爽。此外,copyWithZone
中同樣可以用相同的原理對變量進行自動化copy。同樣地,我們也可以用一個宏封裝掉copyWithZone
方法。這里就不再贅述。值得一提的是,以上代碼作者已經(jīng)放到Github中,并且提供了CocoaPods支持。使用的時候只需要pod WZLSerializeKit。點 此處 跳轉(zhuǎn)到Github.

本文是 [編程小翁] 原創(chuàng)
地址:http://www.lxweimin.com/p/fed1dcb1ac9f

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內(nèi)容