【原】iOS動態性(四):一行代碼實現iOS序列化與反序列化(runtime)

爲取得更好的排版效果,本文一樣發佈在簡書上,強烈建議跳轉到[1]http://www.jianshu.com/p/fed1dcb1ac9fios

1、變量聲明

爲便於下文討論,提早建立父類Biology以及子類Persongit

Biology:github

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

@implementation Biology
@end

Person:函數

#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

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


2、問題引入

在iOS中一個自定義對象是沒法直接存入到文件中的,必須先轉化成二進制流才行。從對象到二進制數據的過程咱們通常稱爲對象的序列化(Serialization),也稱爲歸檔(Archive)。同理,從二進制數據到對象的過程通常稱爲反序列化或者反歸檔。
在序列化實現中不可避免的須要實現NSCoding以及NSCopying(非必須)協議的如下方法:優化

- (id)initWithCoder:(NSCoder *)coder;
- (void)encodeWithCoder:(NSCoder *)coder;
- (id)copyWithZone:(NSZone *)zone;

假設咱們如今須要對直接繼承自NSObject的Person類進行序列化,代碼通常長這樣子:this

//對變量編碼
- (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而是有多層的父類呢?(請注意,序列化的原則是全部層級的父類的屬性變量也要須要序列化);

若是採用開始的傳統的序列化方式進行序列化,在碰到以上問題時容易暴露出如下缺陷(僅僅是缺陷,不能稱爲問題):atom

  • 工程代碼中冗餘代碼不少
  • 父類層級複雜容易致使遺漏點一些父類中的屬性變量

那是否是有更優雅的方案來回避以上問題呢?那是必須的。這裏咱們將共同探討使用runtime來實現一種接口簡潔而且十分通用的iOS序列化與反序列方案。spa


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

3.1 整體思路

觀察上面的initWithCoder代碼咱們能夠發現,序列化與反序列化中最重要的環節是遍歷類的變量,保證不能遺漏。

這裏須要特別注意的是:
編解碼的範圍不能僅僅是自身類的變量,還應當把除NSObject類外的全部層級父類的屬性變量也進行編解碼!

因而可知,這幾乎是個純體力活。而runtime在遍歷變量這件事情上能爲咱們提供什麼幫助呢?咱們能夠經過runtime在運行時獲取自身類的全部變量進行編解碼;而後對父類進行遞歸,獲取除NSObject外每一個層級父類的屬性(非私有變量),進行編解碼。

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

runtime中獲取某類的全部變量(屬性變量以及實例變量)API:

Ivar *class_copyIvarList(Class cls, unsigned int *outCount)

獲取某類的全部屬性變量API:

objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)

runtime的全部開放API都放在objc/runtime.h裏面。上面的一些數據類型有些同窗可能沒見過,這裏咱們先簡單地介紹一下,更詳細的介紹請自行查閱其餘資料,強烈建議打開
Ivar是runtime對於變量的定義,本質是一個結構體:

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: 基地址偏移字節數,能夠不用理會
    獲取全部變量的代碼通常長這樣子:
unsigned int numIvars; //成員變量個數
    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)]; //獲取成員變量的數據類型
        NSLog(@"variable type :%@", key);
    }
    free(vars);//記得釋放掉

objc_property_t是runtime對於屬性變量的定義,本質上也是一個結構體(事實上OC是對C的封裝,大多數類型的本質都是C結構體)。在runtime.h頭文件中只有typedef struct objc_property *objc_property_t,並無更詳細的結構體介紹。雖然runtime的源碼是開源的,但這裏並不打算深刻介紹,這並不影響咱們今天的主題。與Ivar的應用同理,獲取類的屬性變量的代碼通常長這樣子:

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);

3.3 用runtime實現序列化與反序列化

有了前面兩節的鋪墊,到這裏天然就水到渠成了。咱們能夠在initWithCoder:以及encoderWithCoder:中遍歷類的全部變量,取得變量名做爲KEY值,最後使用KVC強制取得或者賦值給對象。因而咱們能夠獲得以下的自動序列化與發序列化代碼,關鍵部分有註釋:

@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);//記得釋放內存
    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對應的變量值
              if (varValue) {
                  [coder encodeObject:varValue forKey:key];
              }
        }
        free(ivarList);
      }

3.4 優化

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

編解碼的範圍不能僅僅是自身類的變量,還應當把除NSObject類外的全部層級父類的屬性變量也進行編解碼!

所以在上面代碼的基礎上咱們咱們須要注意一下細節,設一個指針,先指向自己類,處理完指向SuperClass,處理完再指向SuperClass的SuperClass...。代碼以下(這裏僅以encodeWithCoder:爲例,畢竟initWithCoder:同理):

- (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); //指針指向當前類的父類
    }   
}

這樣真的結束了嗎?不是的。當你的跑上面的代碼時程序有可能會crash掉,crash的地方在[self objectForKey:key]這一句上。原來是這裏的KVC沒法獲取到父類的私有變量(即實例變量)。所以,在處理到父類時不能簡單粗暴地使用class_copyIvarList,而只能取父類的屬性變量。這時候3.2節部分的class_copyPropertyList就派上用場了。在處理父類時用後者代替前者。因而最終的代碼(額~其實還不算最終):

- (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];   
            if (varValue) { 
                [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];   
            if (varValue) { 
                [coder encodeObject:varValue forKey:key];   
            }   
        }   
        free(ivarList); 
        free(propList); 
        cls = class_getSuperclass(cls); 
    }   
}

3.5 最終的封裝

在邏輯上,上面的代碼應該是目前爲止比較完美的自動序列化與反序列解決方案了。即便某個類的繼承深度極其深,變量極其多,序列化的代碼也就以上這些。可是咱們回到文章第二節提出的幾點場景假設,其中有一點提到:

若你的工程中有不少像Person的自定義類須要作序列化操做呢?

若是是在以上場景下,每一個Model類都須要寫一次上面的代碼。這在必定程度上也形成冗餘了。同時,你也會以爲這篇文章的標題就是瞎扯淡,根本就不是一行代碼的事。上面的代碼冗餘,我這種對代碼有很強潔癖的程序旺是萬萬接受不了的。那就再封裝一層!這裏我採用宏的方式(也能夠放到某個工具類裏面)將上述代碼濃縮成一行,放到一個叫WZLSerializeKit.h的頭文件中:

#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];   \
            if (varValue) { \
                [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];   \
            if (varValue) { \
                [coder encodeObject:varValue forKey:key];   \
            }   \
        }   \
        free(ivarList); \
        free(propList); \
        cls = class_getSuperclass(cls); \
    }   \
}

以後須要序列化的地方只要兩步:一、import "WZLSerializeKit.h" 二、調用WZLSERIALIZE_CODER_DECODER();便可。兩個字:清爽。
此外,copyWithZone中一樣能夠用相同的原理對變量進行自動化copy。一樣地,咱們也能夠用一個宏封裝掉copyWithZone方法。這裏就再也不贅述。
值得一提的是,以上代碼我已經放到個人Github中,而且提供了CocoaPods支持。使用的時候只須要pod WZLSerializeKit。點 [此處][2] 跳轉到個人Github. [2]:https://github.com/weng1250/WZLSerializeKit

相關文章
相關標籤/搜索