利用Runtime實現簡單的字典轉模型

前言

咱們都知道,開發中會有這麼一個過程,就是將服務器返回的數據轉換成咱們本身定義的模型對象。固然服務器返回的數據結構有xml類型的,也有json類型的。本文只討論json格式的。html

你們在項目中通常是怎麼樣將服務器返回的json轉化成本身定義的模型類呢?ios

我在項目中通常都是使用的MJExtension
本文講解的也基本就是解讀MJExtension中的部分源碼。
好了,廢話很少說,直接上代碼,let's gogit

簡單字典轉模型

首先,從最簡單的字典開始,例如咱們須要將以下的字典轉化成自定義的模型。github

NSDictionary *dict = @{@"name":@"Scott",
                            @"icon" : @"lufy.png",
                            @"age" : @"20",
                            @"height" : @1.75,
                            @"money" : @"100.9",
                            @"sex" : @(SexMale),
                            @"gay" : @"ture",
                            };

咱們定義一個ScottUser類,而且定義好屬性名以下:json

#import <Foundation/Foundation.h>
    typedef NS_ENUM(NSInteger, Sex) {
        SexMale,    // 男
        SexFemale   // 女
    };
    @interface ScottUser : NSObject
    /**  姓名 */
    @property (nonatomic, copy) NSString *name;
    /**  頭像 */
    @property (nonatomic, copy) NSString *icon;
    /**  年齡 */
    @property (nonatomic, assign) unsigned int age;
    /**  身高 */
    @property (nonatomic, strong) NSNumber *height;
    /**  財富 */
    @property (nonatomic, copy) NSString *money;
    /**  性別 */
    @property (nonatomic, assign) Sex sex;
    /**  是否同性 */
    @property (nonatomic, assign, getter=isGay) BOOL gay;
    @end

到此爲止,咱們下一步的目標就是拿到字典裏面的值(value)對ScottUser模型屬性進行賦值,模型的屬性名對應着字典裏面的key數組

最直接的方法就是:緩存

ScottUser *user = [[ScottUser alloc] init];
    user.name = dict[@"name"];
    user.icon = dict[@"icon"];
    ...

可是,對於每一次的數據轉模型,你都要這樣去寫大量的重複代碼,毫無心義。
固然咱們利用setValuesForKeysWithDictionary:(NSDictionary *)dict進行kvc賦值。性能優化

KVC賦值服務器

  • 優勢:不須要去手動一個一個屬性賦值。
  • 缺點:當自定義的屬性和字典中的key不同的時候,會報錯。
  • 解決辦法:重寫- (void)setValue:(id)value forUndefinedKey:(NSString *)key方法。

咱們能夠經過寫一個框架自動幫咱們實現字典轉模型,大體思路就是:數據結構

  1. 遍歷模型中的屬性,而後拿到屬性名做爲鍵值去字典中尋找;
  2. 找到後,根據模型的屬性類型轉化成正確的類型;
  3. 屬性名賦值。

遍歷模型中的屬性,拿到屬性名做爲鍵值去字典中尋找

方法僞代碼:

[模型類 遍歷屬性的方法];

爲了方便使用,建立一個叫NSObject+ScottProperty的分類,寫一個獲取全部屬性的方法。

#import <Foundation/Foundation.h>
    @interface NSObject (ScottProperty)
    + (NSArray *)properties;
    @end

假設咱們在看不到一個類的.h.m文件的前提下,有什麼辦法能夠獲取它全部的實例變量呢?

答案是經過Runtime

#import "NSObject+ScottProperty.h"
    #import <objc/runtime.h>
    @implementation NSObject (ScottProperty)
    + (NSArray *)properties {
        NSMutableArray *propertiesArr = [NSMutableArray array];
        unsigned int outCount = 0;
        objc_property_t *properties = class_copyPropertyList(self, &outCount);
        for (int i=0; i<outCount; i++) {
            objc_property_t property = properties[i];
            [propertiesArr addObject:propertyObj];
            char *name = property_getName(property);
            char *att = property_getAttributes(property);
            NSLog(@"name:%s-----att:%s",name,att);
        }
        return propertiesArr;
    }

在外部調用+ (NSArray *)properties方法可以打印出一個類的全部屬性,如:

NSArray *arr = [ScottUser properties];

運行程序,可以看到控制檯的輸出:
圖1

從輸出中能夠看到經過property_getName()獲取每個objc_property_tname表示成員屬性的名字,經過property_getAttributes()獲取每個objc_property_tattributes表示成員屬性中的一些特性(如是什麼類,原子性仍是非原子性,是strong仍是weak仍是copy,生成的成員變量名等信息...)

從蘋果的官方文檔(Objective-C Runtime Programming Guide)能夠得知,attributes是一個類型編碼字符串,這個字符串以T做爲開始,接上@encode類型編碼和一個逗號,以V接上實例變量名做爲結尾,在他們之間是一些其餘信息,以逗號分隔,具體內容能夠查看官方文檔中詳細的表格。

在實際賦值過程當中,咱們並不關心該屬性的內存管理、生成的成員變量名、或者其餘什麼信息,在attributes中,只須要知道它所屬的或者知道什麼基本數據類型,即T第一個逗號以前中間的內容,若是是的話還須要將@""去掉。

實際上,Runtime已經給咱們提供獲取屬性名和屬性特性的函數了,也就是經過property_getName()property_getAttributes()

這時候咱們就能夠獲取到屬性名和屬性對應的屬性特性了。

找到後,根據屬性類型轉化成正確的類型

如今已經完成了第一步,而且拿到了屬性名,可是數據類型還須要咱們進一步截取,截取方法以下:

for (int i=0; i<outCount; i++) {
        objc_property_t property = properties[i];
        // 爲了之後方便使用,將C字符串轉化成OC對象
    //    char *name = property_getName(property);
        NSString *name = @(property_getName(property));
    //    char *att = property_getAttributes(property);
        NSString *att = @(property_getAttributes(property));
        NSUInteger loc = 1;
        NSUInteger len = [att rangeOfString:@","].location - loc;
        NSString *type = [att substringWithRange:NSMakeRange(loc, len)];
        NSLog(@"%@",type);
    }

控制檯結果顯示,咱們可以截取到其中的類型了:
圖2

迴歸到咱們拿到這些屬性類型的初衷,是爲了用字典中的值的類型與模型中屬性的類型進行對比,想要對比,須要拿到屬性的類型,所以須要將這些編碼轉換成一個表示類的類,建立一個類用來包裝類型。

#import <Foundation/Foundation.h>
    @interface ScottPropertyType : NSObject
    /**  是否爲id類型 */
    @property (nonatomic, readonly, getter=isIdType) BOOL idType;
    /**  是否爲基本數據類型(int、float等) */
    @property (nonatomic, readonly, getter=isNumberType) BOOL numberType;
    /**  是否爲bool類型 */
    @property (nonatomic, readonly, getter=isBoolType) BOOL boolType;
    /**  對象類型(若是是基本數據類型,此值爲nil) */
    @property (nonatomic, readonly) Class typeClass;
    @end

OC對象能夠經過Class來表示類型,而基本數據類型只能用布爾來標識。

把這些名字和類型遍歷出來,確定是爲了之後有用,因此須要把它們存起來,因爲它們是一個"總體",因此仍是設計一個類將他們包裝起來比較好,建立一個包裝成員屬性的類--ScottProperty

#import <Foundation/Foundation.h>
    @class ScottPropertyType;
    @interface ScottProperty : NSObject
    /**  屬性名 */
    @property (nonatomic, readonly) NSString *name;
    /**  成員屬性的類型 */
    @property (nonatomic, readonly) ScottPropertyType *type;
    @end

這時,代碼就能夠進行重構了,將屬於不一樣類的功能封裝到對應的類上,讓ScottProperty提供一個類方法用於返回一個將objc_property_t進行包裝的類。

for (int i=0; i<outCount; i++) {
            objc_property_t property = properties[i];
            ScottProperty *propertyObj = [ScottProperty propertyWithProperty:property];
            [propertiesArr addObject:propertyObj];
        }

propertyWithProperty:方法實現以下:

@implementation ScottProperty
    + (instancetype)propertyWithProperty:(objc_property_t)property {
        return [[ScottProperty alloc] initWithProperty:property];
    }
    - (instancetype)initWithProperty:(objc_property_t)property {
        if (self = [super init]) {
            _name = @(property_getName(property));
            _type = [ScottPropertyType propertiesWithAttributeString:@(property_getAttributes(property))];
        }
        return self;
    }
    @end

ScottPropertyType也提供類方法用於包裝類型:

#import "ScottPropertyType.h"
    @implementation ScottPropertyType
    + (instancetype)propertiesWithAttributeString:(NSString *)att {
        return [[ScottPropertyType alloc] initWithTypeString:att];
    }
    - (instancetype)initWithTypeString:(NSString *)typeString {   
        if (self = [super init]) {
            NSUInteger loc = 1;
            NSUInteger len = [typeString rangeOfString:@","].location - loc;
            NSString *typeCode = [typeString substringWithRange:NSMakeRange(loc, len)];
            NSLog(@"%@",typeCode);
        }
        return self;
    }
    @end

重構完成以後,結構顯得更加清晰,更加有利於接下來的工做,下面繼續完成typeCode的提取。
運行重構以後的代碼,能夠看到和重構以前是同樣的:
圖3

上面提到的這些類型,是類型編碼,在蘋果文檔中告訴咱們編碼對應的類型:
圖4
根據這個對應關係的圖表,咱們將經常使用的幾個編碼定義成常量字符串或者宏表示它所對應的類型,利於編碼和閱讀:
ScottPropertyType類定義如下屬性類型:

/**
     *  成員變量類型(屬性類型)
     */
    NSString *const ScottPropertyTypeInt = @"i";
    NSString *const ScottPropertyTypeShort = @"s";
    NSString *const ScottPropertyTypeFloat = @"f";
    NSString *const ScottPropertyTypeDouble = @"d";
    NSString *const ScottPropertyTypeLong = @"q";
    NSString *const ScottPropertyTypeChar = @"c";
    NSString *const ScottPropertyTypeBOOL1 = @"c";
    NSString *const ScottPropertyTypeBOOL2 = @"b";
    NSString *const ScottPropertyTypePointer = @"*";
    NSString *const ScottPropertyTypeIvar = @"^{objc_ivar=}";
    NSString *const ScottPropertyTypeMethod = @"^{objc_method=}";
    NSString *const ScottPropertyTypeBlock = @"@?";
    NSString *const ScottPropertyTypeClass = @"#";
    NSString *const ScottPropertyTypeSEL = @":";
    NSString *const ScottPropertyTypeId = @"@";

並寫一個方法用於提取每一個屬性的類型:

- (instancetype)initWithTypeString:(NSString *)typeString {
        if (self = [super init]) {
            NSUInteger loc = 1;
            NSUInteger len = [typeString rangeOfString:@","].location - loc;
            NSString *typeCode = [typeString substringWithRange:NSMakeRange(loc, len)];
            [self getTypeCode:typeCode];
        }
        return self;
    }
    - (void)getTypeCode:(NSString *)code {
        if ([code isEqualToString:ScottPropertyTypeId]) {
            _idType = YES;
        }else if (code.length > 3 && [code hasPrefix:@"@\""]){
            // 去掉@"和",截取中間的類型名稱
            code = [code substringWithRange:NSMakeRange(2, code.length - 3)];
            _typeClass = NSClassFromString(code);
            _numberType = (_typeClass == [NSNumber class] || [_typeClass isSubclassOfClass:[NSNumber class]]);
        }
        // 是否爲數字類型
        NSString *lowerCode = code.lowercaseString;
        NSArray *numberTypes = @[ScottPropertyTypeInt,
                                 ScottPropertyTypeShort,
                                 ScottPropertyTypeFloat,
                                 ScottPropertyTypeDouble,
                                 ScottPropertyTypeLong,
                                 ScottPropertyTypeChar,
                                 ScottPropertyTypeBOOL1,
                                 ScottPropertyTypeBOOL2];
        if ([numberTypes containsObject:lowerCode]) {
            _numberType = YES;
            if ([lowerCode isEqualToString:ScottPropertyTypeBOOL1] || [lowerCode isEqualToString:ScottPropertyTypeBOOL2]) {
                _boolType = YES;
            }
        }
    }

到這裏,咱們一個ScottProperty的骨架大體就搭好了。

NSObject+ScottProperty分類中遍歷屬性的時候,打印屬性名和屬性類型看看:

for (int i=0; i<outCount; i++) {
            objc_property_t property = properties[i];
            ScottProperty *propertyObj = [ScottProperty propertyWithProperty:property];
            [propertiesArr addObject:propertyObj];
            NSLog(@"name:%@--type:%@",propertyObj.name,propertyObj.type.typeClass);
        }

圖5
從圖中能夠看出,屬於基本類型的屬性打印出來的類型是null,其餘的都能正確打印出對應類型。

當咱們想要使用字典轉模型功能的時候,提供一個類方法方便轉換,該方法放在NSObject+ScottKeyValue分類中,該分類負責字典轉模型的方法實現。

+ (instancetype)objectWithKeyValues:(id)keyValues {
        if (!keyValues) return nil;
        return [[[self alloc] init] setKeyValues:keyValues];
    }
    - (instancetype)setKeyValues:(id)keyValues {
        NSArray *propertiesArray = [self.class properties];
        for (ScottProperty *property in propertiesArray) {
            ScottPropertyType *type = property.type;
            Class typeClass = type.typeClass;
            if (type.isBoolType) {
                NSLog(@"Bool");
            }else if (type.isIdType){
                NSLog(@"ID");
            }else if (type.isNumberType){
                NSLog(@"Number");
            }else{
                NSLog(@"%@",typeClass);
            }
        }
        return self;
    }

咱們想要字典轉模型的時候,直接以下使用:

NSDictionary *dict = @{@"name":@"Scott",
                           @"icon" : @"lufy.png",
                           @"age" : @"20",
                           @"height" : @1.75,
                           @"money" : @"100.9",
                           @"sex" : @(SexMale),
                           @"gay" : @"ture",
                           };
    ScottUser *userModel = [ScottUser objectWithKeyValues:dict];

ok,運行程序,能夠看到控制檯輸出ScottUser類中各屬性對應的類型:
圖6

咱們進行下一步:用該屬性名做爲鍵去字典中尋找對應的值

僞代碼:

[字典 valueForKey:屬性名];

此處的屬性名會有點問題,例如咱們定義屬性名的時候不能是關鍵字,而若是字典中的key是涉及到關鍵字的,那麼咱們須要轉換,可是也並不是全部的都有這種狀況,所以咱們能夠想到使用代理。咱們在NSObject+ScottKeyValue分類中寫一個ScottKeyValue協議,而且讓它遵照該協議:

@protocol ScottKeyValue <NSObject>
    @optional
    + (NSDictionary *)replacedKeyFromPropertyName;
    @end

而後咱們提供一個類方法,用於處理將屬性名與字典中的key達到一致。

+ (NSString *)propertyKey:(NSString *)propertyName {
        NSString *key;
        if ([self respondsToSelector:@selector(replacedKeyFromPropertyName)]) {
            key = [self replacedKeyFromPropertyName][propertyName];
        }
        return key ? key : propertyName;
    }

調用:

// 屬性名做爲鍵去尋找對應的值
    id value = [keyValues valueForKey:[self.class propertyKey:property.name]];
    if (!value) continue;
    NSLog(@"%@",value);

運行,咱們能夠看到已經可以拿到值了:
圖7

接下來,咱們拿到值後將值的類型轉換爲屬性對應的數據類型。

首先須要處理數字類型,若是模型的屬性是數字類型,即type.isNumberType == YES,若是字典中的值是字符串類型,須要將其轉成NSNumber類型,若是原本就是基本數據類型,則不用進行任何轉換。

if (type.isNumberType == YES) {
       // 字符串-->數字
       if ([value isKindOfClass:[NSString class]]) {
           value = [[[NSNumberFormatter alloc] init] numberFromString:value];
       }
    }

其中有一種狀況,是須要進行特殊處理的,當模型的屬性是char類型或者bool類型時,獲取到的編碼都是c,而且bool還有多是B編碼,它們都對應_boolType,由於數字類型包含布爾類型,因此bool類型要在數字類型的條件下進行額外判斷。

if (type.isNumberType == YES) {
        NSString *oldValue = value;
        // 字符串-->數字
        if ([value isKindOfClass:[NSString class]]) {
            value = [[[NSNumberFormatter alloc] init] numberFromString:value];
            if (type.isBoolType) {
                NSString *lower = [oldValue lowercaseString];
                if ([lower isEqualToString:@"yes"] || [lower isEqualToString:@"ture"]) {
                    value = @YES;
                }else if ([lower isEqualToString:@"no"] || [lower isEqualToString:@"false"]){
                    value = @NO;
                }
            }
        }
    } else { //  而後處理其餘類型轉化成字符串類型的狀況:
          if (typeClass == [NSString class]) {
              if ([value isKindOfClass:[NSNumber class]]) {
                  if (type.isNumberType)
                      // NSNumber -> NSString
                      value = [value description];
              }else if ([value isKindOfClass:[NSURL class]]){
                  // NSURL -> NSString
                  value = [value absoluteString];
              }
          }
      }
      // 最後賦值      
      [self setValue:value forKey:property.name];

最後咱們調用並打印

ScottUser *userModel = [ScottUser objectWithKeyValues:dict];
    NSLog(@"name:%@,icon:%@,age:%d,height:%@,money:%@,sex:%ld,gay:%d",userModel.name,userModel.icon,userModel.age,userModel.height,userModel.money,(long)userModel.sex,userModel.gay);

圖8

到這裏最簡單的字典轉模型大體完成了,固然還有不少的細節沒有完善,後面再作處理。

JSON字符串轉模型

定義一個json字符串轉成模型:

#pragma mark - JSON字符串轉模型
    void keyValues2object1(){
        // 1.定義一個json字符串
        NSString *jsonString = @"{\"name\":\"scott\",\"icon\":\"lufy.png\",\"age\":20}";
        // 2.將json字符串轉爲LZUser模型
        ScottUser *user = [ScottUser objectWithKeyValues:jsonString];
        // 3.打印模型屬性
        NSLog(@"name=%@, icon=%@, age=%d",user.name,user.icon,user.age);
    }

運行程序,這時程序會華麗麗的崩潰,由於程序原來只對字典類型做了處理:

// 咱們能夠定位到程序崩潰在這裏
       id value = [keyValues valueForKey:[self.class propertyKey:property.name]];

因此在這以前須要將JSON轉成Foundation框架中的對象,蘋果提供了強大的NSJSONSerialization,利用它,在剛開始傳入字典/JSON字符串的時候將其進行轉換。

- (instancetype)setKeyValues:(id)keyValues { 
        keyValues = [keyValues JSONObject]; 
        NSArray *propertiesArray = [self.class properties];
        ......
    }

該方法的實現以下,若是當前是字符串,則轉換成NSData再進行序列化。

- (id)JSONObject {
        id foundationObj;
        if ([self isKindOfClass:[NSString class]]) {
            NSString *str = (NSString *)self;
            foundationObj = [NSJSONSerialization  JSONObjectWithData:[str dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil];
        }else if ([self isKindOfClass:[NSData class]]){
            foundationObj = [NSJSONSerialization JSONObjectWithData:(NSData *)self options:kNilOptions error:nil];
        }
        // 若是foundationObj有值,則返回foundationObj,不然返回self
        return foundationObj ? : self;
    }

此時,運行程序,OK,可以看到控制檯能正確輸入結果:
圖9

複雜字典轉模型

定義一個模型中包含模型的複雜字典:

NSDictionary *dict = @{@"text":@"是啊,今每天氣確實不錯!",
                           @"user":@{
                                   @"name":@"scott",
                                   @"icon":@"lufy.png"
                                   },
                           @"retweetedStatus":@{
                                   @"text":@"是啊,今每天氣確實不錯",
                                   @"user":@{
                                           @"name":@"scott_status",
                                           @"icon":@"lufy_status.png"
                                           }
                                   }
                           };

對待這種字典的思路,應該想到遞歸,當碰到模型中的屬性類型是一個模型類時,將字典中的value做爲字典處理,而後再調用字典轉模型的方法返回一個模型類,因此在包裝類型時還要有個屬性表示它是不是自定義的模型類,才能做爲依據繼續遞歸,判斷的方法是看它是否來自於Foundation框架的類。
ScottPropertyType中添加一個屬性:

/**  是否來源於Foundation框架,好比NSString,NSArray等 */
    @property (nonatomic, readonly, getter=isFromFoundation) BOOL fromFoundation;

- (void)getTypeCode:(NSString *)code方法中添加這樣一條:

else if (code.length > 3 && [code hasPrefix:@"@\""]){
        // 去掉@"和",截取中間的類型名稱
        code = [code substringWithRange:NSMakeRange(2, code.length - 3)];
        _typeClass = NSClassFromString(code);
        _numberType = (_typeClass == [NSNumber class] || [_typeClass isSubclassOfClass:[NSNumber class]]);
        // 判斷是否來自於foundation框架
        _fromFoundation = [NSObject isClassFromFoundation:_typeClass];
    }

NSObject+ScottProperty分類中添加一個類方法:

// 用於判斷當前類是否來自於foundation框架
    + (BOOL)isClassFromFoundation:(Class)c;

那麼問題來了,若是判斷是否來自於Foundation框架呢? 下圖展現了Foundation框架(NSObject部分)下的類結構:
圖10

用一個NSSet(比用NSArray檢索效率更高),返回一些經常使用基本的Foundation框架下繼承自NSObject的類。

static NSSet *foundationClasses_;
    + (NSSet *)foundationClass {
        if (foundationClasses_ == nil) {
            foundationClasses_ = [NSSet setWithObjects:[NSURL class],
                                  [NSDate class],
                                  [NSValue class],
                                  [NSData class],
                                  [NSArray class],
                                  [NSDictionary class],
                                  [NSString class],
                                  [NSMutableString class], nil];
        }
        return foundationClasses_;
    }

因此判斷是不是foundation框架的類方法具體實現:

+ (BOOL)isClassFromFoundation:(Class)c {
        // 由於foundationClasses_裏面的類都是繼承NSObject,所以NSObject不能放到上面的集合,須要額外作判斷
        if (c == [NSObject class]) return YES;
        __block BOOL result = NO;
        [[self foundationClass] enumerateObjectsUsingBlock:^(Class foundationClass, BOOL *stop) {
            if ([c isSubclassOfClass:foundationClass]) {
                result = YES;
                *stop = YES;
            }
        }];
        return result;
    }

獲得結果後,須要在NSObject+ScottKeyValue分類中的setKeyValues:方法中添加以下

// 若是不是來自foundation框架的類而且不是基本數據類型 ,則遞歸,若是是基本數據類型,typeClass爲nil
    if (!type.isFromFoundation && typeClass) {
        value = [typeClass objectWithKeyValues:value];
    }

到這裏,複雜字典轉模型就算是完成了,具體調用的過程看源碼文章結尾會給地址

字典數組轉模型

稍微複雜的一種狀況是一個字典裏面帶有數組:

NSDictionary *dict = @{
                           @"statuses" : @[
                                   @{
                                       @"text" : @"今每天氣真不錯!",
                                       @"user" : @{
                                               @"name" : @"Rose",
                                               @"icon" : @"nami.png"
                                               }
                                       },
                                   @{
                                       @"text" : @"明天去旅遊了",
                                       @"user" : @{
                                               @"name" : @"Jack",
                                               @"icon" : @"lufy.png"
                                               }
                                       }
                                   ],
                           @"ads" : @[
                                   @{
                                       @"image" : @"ad01.png",
                                       @"url" : @"http://www.baidu.com"
                                       },
                                   @{
                                       @"image" : @"ad02.png",
                                       @"url" : @"http://www.sina.com"
                                       }
                                   ],
                           @"totalNumber" : @"2014",
                           @"previousCursor" : @"13476589",
                           @"nextCursor" : @"13476599"
                           };

上面定義了一個字典,建立一個ScottStatusResult模型,裏面有兩個數組,另外還有其餘3個鍵:

#import <Foundation/Foundation.h>
    @interface ScottStatusResult : NSObject
    /** 存放着某一頁微博數據(裏面都是Status模型) */
    @property (nonatomic, strong) NSMutableArray *statuses;
    /** 存放着一堆的廣告數據(裏面都是Ad模型) */
    @property (nonatomic, strong) NSArray *ads;
    /** 總數 */
    @property (nonatomic, strong) NSNumber *totalNumber;
    /** 上一頁的遊標 */
    @property (nonatomic, assign) long long previousCursor;
    /** 下一頁的遊標 */
    @property (nonatomic, assign) long long nextCursor;
    @end

對於一個數組來講,你必需要告訴方法裏面裝的是什麼模型,才能將字典中值爲數組的成員轉成模型。
MJExtension中,提供了兩種方式進行處理。

  • 方式一:調用NSObject分類中的類方法:

    [ScottStatusResult setupObjectClassInArray:^NSDictionary *{
         return @{ @"statuses" : @"ScottStatus",
          // 或者 @"statuses" : [ScottStatus class],
                   @"ads" : @"ScottAd"
          // 或者 @"ads" : [ScottAd class]
                 };
            }];
  • 方式二:在模型的.m文件中實現方法供回調:

    + (NSDictionary *)objectClassInArray
        {
            return @{
                     @"statuses" : @"ScottStatus",
                      // 或者 @"statuses" : [ScottStatus class],
                     @"ads" : @"ScottAd"
                     // 或者 @"ads" : [ScottAd class]
                     };
        }

原理上都差很少,都是經過代碼進行回調,這個主要實現方式二。
NSObject+ScottKeyValue分類中的ScottKeyValue協議中添加一個方法

+ (NSDictionary *)objectClassInArray;

NSObject+ScottKeyValue分類中的setKeyValues:方法中添加一種類型判斷

// 若是不是來自foundation框架的類而且不是基本數據類型 ,則遞歸,若是是基本數據類型,typeClass爲nil
       if (!type.isFromFoundation && typeClass) {
           value = [typeClass objectWithKeyValues:value];
       }else if ([self.class respondsToSelector:@selector(objectClassInArray)]){ // 看該類是否實現了objectClassInArray方法
           id objectClass;
           objectClass = [self.class objectClassInArray][property.name];
          // 若是是NSString類型
          if ([objectClass isKindOfClass:[NSString class]]) {
              objectClass = NSClassFromString(objectClass);
          }
          if (objectClass) {
              // 返回一個裝了模型的數組
              value = [objectClass objectArrayWithKeyValuesArray:value];
          }
      }

返回一個裝了模型的數組方法實現:

/**
     *  根據字典/JSON返回模型數組
     *
     *  @param keyValuesArray 字典/JSON數組
     *
     *  @return 模型數組
     */
    + (NSMutableArray *)objectArrayWithKeyValuesArray:(id)keyValuesArray
    {
        if ([self isClassFromFoundation:self])
            return keyValuesArray;
        keyValuesArray = [keyValuesArray JSONObject];
        NSMutableArray *modelArray = [NSMutableArray array];
        // 遍歷
        for (NSDictionary *keyValues in keyValuesArray) {
            id model;
            model = [self objectWithKeyValues:keyValues];
            if (model) {
                [modelArray addObject:model];
            }
        }    
        return modelArray;
    }

到這裏,字典數組轉模型就算是完成了,具體調用的過程看源碼文章結尾會給地址

key的替換

在實際開發中,服務器一般返回一個字段名id,或者descriptionJSON數據,而這兩個名字在OC中有特殊含義,在定義屬性的時候並不能使用這類名稱.這時屬性名與字典key再也不是直接對應的關係,須要加入一層轉換。
這個在前面用該屬性名做爲鍵去字典中尋找對應的值講到過,在次就再也不重複講解。

性能優化

將5個字典轉模型的例子同時運行,在NSObject+ScottProperty分類中的+ (NSArray *)properties方法中添加一句打印NSLog(@"%@調用了properties方法",[self class]);。另外,以前的例子都是有內存泄露的,這裏添加了free(properties);修復了這個問題。

+ (NSArray *)properties {
        NSLog(@"%@調用了properties方法",[self class]);
        NSMutableArray *propertiesArr = [NSMutableArray array];
        unsigned int outCount = 0;
        objc_property_t *properties = class_copyPropertyList(self, &outCount);
        for (int i=0; i<outCount; i++) {
            objc_property_t property = properties[i];
            ScottProperty *propertyObj = [ScottProperty propertyWithProperty:property];
            [propertiesArr addObject:propertyObj];
        }
        free(properties);
        return propertiesArr;
    }

運行程序,能夠看到控制檯輸出:
圖11
能夠看到,不少的類都不止一次調用了獲取屬性的方法,對於一個類來講,要獲取它的所有屬性,只要獲取一次就夠了.獲取到後將結果緩存起來,下次就沒必要進行沒必要要的計算。
下面進行優化:

// 設置一個全局字典用來將類的屬性都緩存起來
    static NSMutableDictionary *cachedProperties_;
    + (void)load
    {
        cachedProperties_ = [NSMutableDictionary dictionary];
    }

將方法改寫爲:

+ (NSArray *)properties {
        NSMutableArray *propertiesArr = cachedProperties_[NSStringFromClass(self)];
        if (!propertiesArr) {
            NSLog(@"%@調用了properties方法",[self class]);
            propertiesArr = [NSMutableArray array];
            unsigned int outCount = 0;
            objc_property_t *properties = class_copyPropertyList(self, &outCount);
            for (int i=0; i<outCount; i++) {
                objc_property_t property = properties[i];
                ScottProperty *propertyObj = [ScottProperty propertyWithProperty:property];
                [propertiesArr addObject:propertyObj];
                //        NSLog(@"name:%@--type:%@",propertyObj.name,propertyObj.type.typeClass);
                // 爲了之後方便使用,將C字符串轉化成OC對象
                //        char *name = property_getName(property);
                //        NSString *name = @(property_getName(property));
                //        char *att = property_getAttributes(property);
                //        NSString *att = @(property_getAttributes(property));
                //        NSUInteger loc = 1;
                //        NSUInteger len = [att rangeOfString:@","].location - loc;
                //        NSString *type = [att substringWithRange:NSMakeRange(loc, len)];
                //        NSLog(@"%@",type);
            }
            free(properties);
            cachedProperties_[NSStringFromClass(self)] = propertiesArr;
        }
        return propertiesArr;
    }

此時,控制檯輸出:
圖12
能夠看出每個類只通過一次就能夠獲取全部屬性。


除了緩存屬性外,提取類型編碼的過程也能夠進一步緩存優化性能。
在下面的方法中加上一句打印:

- (void)getTypeCode:(NSString *)code {
        NSLog(@"%@",code);
        ...
    }

能夠看到控制檯輸出:
圖13

能夠看到一些經常使用的類型例如NSString屢次調用了該方法。提取類型時,只要知道類名(在這裏也就是typeCode),一個ScottPropertyType就已經能夠肯定了。

重寫了- initWithTypeString:方法:

static NSMutableDictionary *cacheTypes_;
    + (void)load {
        cacheTypes_ = [NSMutableDictionary dictionary];
    }
    + (instancetype)propertiesWithAttributeString:(NSString *)att {
        return [[ScottPropertyType alloc] initWithTypeString:att];
    }
    - (instancetype)initWithTypeString:(NSString *)typeString {   
        if (self = [super init]) {
            NSUInteger loc = 1;
            NSUInteger len = [typeString rangeOfString:@","].location - loc;
            NSString *typeCode = [typeString substringWithRange:NSMakeRange(loc, len)];
            if (!cacheTypes_[typeCode]) {
                [self getTypeCode:typeCode];
                cacheTypes_[typeCode] = self;
            }
        }
        return self;
    }

輸出結果:
圖14

結束語

OK,到這裏,咱們的解讀也算是完成了,因爲是下班以後寫的,因此花費了4天的時間,終於把此篇文章寫完了,歡迎你們點評並討論。
最後代碼地址:--->戳這裏

參考資料

參考資料:跟着MJExtension實現簡單的字典轉模型

相關文章
相關標籤/搜索