跟着MJExtension實現簡單的字典轉模型框架 - 簡書

演示代碼地址:https://github.com/codeWillwillCode/LearnMJExtensioncss

最簡單的字典

首先,從最簡單的字典開始.html

NSDictionary *dict = @{                           @"name" : @"Jack",                           @"icon" : @"lufy.png",                           @"age" : @"20",                           @"height" : @1.55,                           @"money" : @"100.9",                           @"sex" : @(SexFemale),                           @"gay" : @"1"
                        }

目標是拿到字典裏的值(value)User模型進行賦值.模型的屬性名對應字典的鍵(key).ios

typedef enum {
    SexMale,
    SexFemale
} Sex;@interface User : NSObject/** 名稱 */@property (copy, nonatomic) NSString *name;/** 頭像 */@property (copy, nonatomic) NSString *icon;/** 年齡 */@property (assign, nonatomic) unsigned int age;/** 身高 */@property (copy, nonatomic) NSString *height;/** 財富 */@property (strong, nonatomic) NSNumber *money;/** 性別 */@property (assign, nonatomic) Sex sex;/** 同性戀 */@property (assign, nonatomic, getter=isGay) BOOL gay;@end

最直接的方法是:git

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

假如屬性數量一多,人工手寫大量樣板代碼將耗費大量時間和精力,毫無心義.github

若是要寫一個框架自動幫咱們轉模型出來,大體思路以下:json

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

2.找到後根據模型的屬性類型轉成正確的類型數組

3.賦值緩存


首先進行第一步:ruby

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

方法僞代碼:

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

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

@interface NSObject (Property)+ (NSArray *)properties;@end

假設咱們看不見一個類的.h.m,有什麼辦法能夠獲取它全部的實例變量呢?答案是經過運行時機制.當在實現+ (NSArray *)properties方法時,須要導入運行時庫.而後使用庫中的API提供的函數獲得一個類的方法列表.

注:在舊版本的MJExtension中,獲取成員變量是經過class_copyIvarList來獲取的類的全部實例變量,根據MJ源碼中的說明:"在 swift 中,因爲語法結構的變化,使用 Ivar 很是不穩定,常常會崩潰!",因此改用了獲取成員屬性的方法.

另外,不論是獲取成員屬性仍是實例變量,都不能獲取到父類的列表.(本人忽略了對父類成員屬性的獲取,後期更新中會更新這一失誤).

// Any instance variables declared by superclasses are not included.objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)

返回的是叫objc_property_t的一個結構體指針,而且經過傳入值引用可以獲得屬性的個數.

#import "NSObject+Property.h"#import <objc/runtime.h>@implementation NSObject (Property)+ (NSArray *)properties{    NSArray *propertiesArray = [NSMutableArray array];      // 1.得到全部的屬性
    unsigned int outCount = 0;
    objc_property_t *properties = class_copyPropertyList(self, &outCount);      // .....
    return propertiesArray;
}@end

來到這裏已經獲取到了屬性列表,那麼objc_property_t指向的結構體內部是怎樣的呢.經過搜尋<objc/runtime.h>頭文件並看不到objc_property_t的定義的.但好在runtime開源,咱們搜尋到了相關的定義.

typedef struct property_t *objc_property_t;struct property_t {    const char *name;    const char *attributes;
};

因爲知道告終構體的內部構造,就能夠獲取內部的成員變量.例如如下方法:

typedef struct property_t {    const char *name;    const char *attributes;
} *propertyStruct;@implementation NSObject (Property)+ (NSArray *)properties{    NSArray *propertiesArray = [NSMutableArray array];    // 1.得到全部的屬性
    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];        NSLog(@"name:%s---attributes:%s",((propertyStruct)property)->name,((propertyStruct)property)->attributes);
    }    return propertiesArray;
}@end

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

NSArray *propertyArray = [User properties];

獲得控制檯輸出:


從輸出中能夠看到該結構體的name成員表示成員屬性的名字,attributes表示成員屬性中的一些特性(如是什麼類,原子性仍是非原子性,是strong仍是weak仍是copy,生成的成員變量名等信息)...

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

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

實際上,框架提供的運行時庫已經給咱們提供獲取屬性名屬性特性的函數了.經過下面方式也能打印出相同結果.

NSLog(@"name:%s---attributes:%s",property_getName(property),
                                   property_getAttributes(property));

runtime源碼中能夠看到這兩個函數的內部是這樣實現的:

const char *property_getName(objc_property_t prop){    return prop->name;
}const char *property_getAttributes(objc_property_t prop){    return prop->attributes;
}

再回顧前面說的思路,這時會更清晰:

1.拿到模型的屬性名(注意屬性名和成員變量名的區別),和對應的數據類型.

2.用該屬性名做爲鍵去字典中尋找對應的值.

3.拿到值後將值轉換爲屬性對應的數據類型.

4.賦值.

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

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

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


該部分源碼請看項目實例代碼中的<打印類型>


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

/**
 *  包裝一種類型
 */@interface MJPropertyType : 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來表示類型,而基本數據類型只能用布爾來標識.

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

@interface MJProperty : NSObject/** 成員屬性的名字 */@property (nonatomic, readonly) NSString *name;/** 成員屬性的類型 */@property (nonatomic, readonly) MJPropertyType *type;@end

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

 for (int i = 0; i < outCount; i++) {        objc_property_t property = properties[i];

        MJProperty *propertyObj = [MJProperty propertyWithProperty:property];
 }

propertyWithProperty:方法的實現以下:

+ (instancetype)propertyWithProperty:(objc_property_t)property{    return  [[MJProperty alloc] initWithProperty:property];
}


- (instancetype)initWithProperty:(objc_property_t)property{    if (self = [super init]) {
        _name = @(property_getName(property));
        _type = [MJPropertyType propertyTypeWithAttributeString:@(property_getAttributes(property))];;
    }    return self;
}

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

+ (instancetype)propertyTypeWithAttributeString:(NSString *)string{    return [[MJPropertyType alloc] initWithTypeString:string];
}

- (instancetype)initWithTypeString:(NSString *)string
{    if (self = [super init])
    {        NSUInteger loc = 1;        NSUInteger len = [string rangeOfString:@","].location - loc;        NSString *type = [string substringWithRange:NSMakeRange(loc, len)];        NSLog(@"%@",type);
    }    return self;
}

重構完成以後,結構顯得更加清晰.更有利於接下來的工做.下面繼續完成type的提取.

該部分源碼請看項目實例代碼中的<重構>



上面獲取到的這些類型,是類型編碼,在蘋果文檔中告訴了咱們編碼對應的類型:


根據這個對應關係的圖表,咱們將經常使用的幾個編碼定義成常量字符串或者宏表示它所對應的類型,便於編碼和閱讀:

/**
 *  成員變量類型(屬性類型)
 */NSString *const MJPropertyTypeInt = @"i";NSString *const MJPropertyTypeShort = @"s";NSString *const MJPropertyTypeFloat = @"f";NSString *const MJPropertyTypeDouble = @"d";NSString *const MJPropertyTypeLong = @"q";NSString *const MJPropertyTypeChar = @"c";NSString *const MJPropertyTypeBOOL1 = @"c";NSString *const MJPropertyTypeBOOL2 = @"b";NSString *const MJPropertyTypePointer = @"*";NSString *const MJPropertyTypeIvar = @"^{objc_ivar=}";NSString *const MJPropertyTypeMethod = @"^{objc_method=}";NSString *const MJPropertyTypeBlock = @"@?";NSString *const MJPropertyTypeClass = @"#";NSString *const MJPropertyTypeSEL = @":";NSString *const MJPropertyTypeId = @"@";

設置完後,就能夠進行提取類型了.

- (instancetype)initWithTypeString:(NSString *)string
{    if (self = [super init])
    {        NSUInteger loc = 1;        NSUInteger len = [string rangeOfString:@","].location - loc;        NSString *typeCode = [string substringWithRange:NSMakeRange(loc, len)];
        [self getTypeCode:typeCode];        NSLog(@"%@",typeCode);
    }    return self;
}

- (void)getTypeCode:(NSString *)code
{    if ([code isEqualToString:MJPropertyTypeId]) {
        _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 = @[MJPropertyTypeInt, MJPropertyTypeShort, MJPropertyTypeBOOL1, MJPropertyTypeBOOL2, MJPropertyTypeFloat, MJPropertyTypeDouble, MJPropertyTypeLong, MJPropertyTypeChar];    if ([numberTypes containsObject:lowerCode]) {
        _numberType = YES;        if ([lowerCode isEqualToString:MJPropertyTypeBOOL1]
            || [lowerCode isEqualToString:MJPropertyTypeBOOL2]) {
            _boolType = YES;
        }
    }
}

至此,一個MJProperty的骨架就大體搭好了.

該部分源碼請看項目實例代碼中的<MJProperty的構建>



13F73F26-1195-43BC-BC98-FF2641B7DA58.png

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

@implementation NSObject (keyValue2object)+ (instancetype)objectWithKeyValues:(id)keyValues{    if (!keyValues) return nil;    return [[[self alloc] init] setKeyValues:keyValues];
}

- (instancetype)setKeyValues:(id)keyValues{    NSArray *propertiesArray = [self.class properties];    for (MJProperty *property in propertiesArray) {
        MJPropertyType *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;
}@end

打印結果:


而後進行下一步----2.用該屬性名做爲鍵去字典中尋找對應的值.

 id value = [keyValues valueForKey:property.name]; if (!value) continue;

接下來是第三步:3.拿到值後將值的類型轉換爲屬性對應的數據類型.

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

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

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

if (type.isNumberType){
            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:@"true"] ) {
                        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];

最簡單的字典轉模型大體完成了,固然,還有不少細節沒有完善,但細節老是隨着需求的不斷變化而不斷增長的.

該部分源碼請看項目實例代碼中的<簡單的字典轉模型>

JSON字符串 -> 模型

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

/**
 *  JSON字符串 -> 模型
 */
void keyValues2object1(){
    // 1.定義一個JSON字符串
    NSString *jsonString = @"{\"name\":\"Jack\", \"icon\":\"lufy.png\", \"age\":20}";

    // 2.將JSON字符串轉爲User模型
    User *user = [User objectWithKeyValues:jsonString];

    // 3.打印User模型的屬性
    NSLog(@"name=%@, icon=%@, age=%d", user.name, user.icon, user.age);}

這時程序會崩潰,由於沒有對程序原來只對字典類型做處理:

// 若是是字符串,到這行就崩了id value = [keyValues valueForKey:property.name];

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

- (instancetype)setKeyValues:(id)keyValues{
    keyValues = [keyValues JSONObject];
......
}

該方法的具體實現以下,若是是NSString,就要先轉成NSData再進行序列化.

- (id)JSONObject{    id foundationObj;    if ([self isKindOfClass:[NSString class]]) {
        foundationObj = [NSJSONSerialization JSONObjectWithData:[(NSString *)self dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil];
    }else if ([self isKindOfClass:[NSData class]]){
        foundationObj = [NSJSONSerialization JSONObjectWithData:(NSData *)self options:kNilOptions error:nil];
    }    return foundationObj?:self;
}

該部分源碼請看項目實例代碼中的<JSON轉模型>

複雜的字典 -> 模型

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

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

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

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

在提取類型的方法中添加這樣一條:

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]]);          // 判斷是不是模型類
        _fromFoundation = [NSObject isClassFromFoundation:_typeClass];
  }

怎麼判斷是否來自Foundation框架呢? 下圖展現了Foundation框架(NSObject部分)下的類結構.


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

static NSSet *foundationClasses_;

+ (NSSet *)foundationClasses
{    if (foundationClasses_ == nil) {

        foundationClasses_ = [NSSet setWithObjects:
                              [NSURL class],
                              [NSDate class],
                              [NSValue class],
                              [NSData class],
                              [NSArray class],
                              [NSDictionary class],
                              [NSString class],
                              [NSAttributedString class], nil];
    }    return foundationClasses_;
}

具體isClassFromFoundation的邏輯由類方法實現,在上面的集合中遍歷.因爲幾乎全部類都是繼承自NSObject,因此NSObject不能寫入上面的集合當中,須要額外判斷:

+ (BOOL)isClassFromFoundation:(Class)c{    if (c == [NSObject class]) return YES;
    __block BOOL result = NO;
    [[self foundationClasses] enumerateObjectsUsingBlock:^(Class foundationClass, BOOL *stop) {        if ([c isSubclassOfClass:foundationClass]) {
            result = YES;
            *stop = YES;
        }
    }];    return result;
}

獲得結果後,須要在setKeyValues:keyValues這一核心方法中添加是否爲模型類的判斷:

// 若是不是來自foundation框架的類而且不是基本數據類型 ,則遞歸
 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.小碼哥ad01.com"
                                       },
                                   @{                                       @"image" : @"ad02.png",                                       @"url" : @"http://www.小碼哥ad02.com"
                                       }
                                   ],                           @"totalNumber" : @"2014",                           @"previousCursor" : @"13476589",                           @"nextCursor" : @"13476599"
                           };

上面定義了一個字典,模型StatusResult有兩個數組屬性.

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

對於一個數組來講,你必需要告訴方法裏面裝的是什麼模型,才能將字典中值爲數組的成員轉成模型.

MJExtension中,提供了兩種方式進行處理.

方式一,調用NSObject分類中得類方法:

[StatusResult setupObjectClassInArray:^NSDictionary *{
        return @{
                 @"statuses" : @"Status",
                 // 或者 @"statuses" : [Status class],
                 @"ads" : @"Ad"
                 // 或者 @"ads" : [Ad class]
                 };
    }];

方式二,在模型的.m文件中實現方法供回調:

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

原理上都差很少,都是經過代碼進行回調,這個主要實現方式二.

在分類中聲明一個protocol提供接口供模型類調用.

@protocol MJKeyValue <NSObject>+ (NSDictionary *) objectClassInArray;@end

在轉換的代碼中設置添加設置數組模型的方法:

if (!type.isFromFoundation && typeClass) {
    value = [typeClass objectWithKeyValues:value];
}// 看該類是否實現了objectClassInArray方法else if ([self.class respondsToSelector:@selector(objectClassInArray)]){
    id objectClass;    // 若是是class類型,例如@"statuses" : [Status class]
      objectClass = [self.class objectClassInArray][property.name];    // 若是是NSString類型,例如@"statuses" : @"Status"
    if ([objectClass isKindOfClass:[NSString class]]) {
        objectClass = NSClassFromString(objectClass);
    }    // 若是有值
    if (objectClass) {        // 返回一個裝了模型的數組
        value = [objectClass objectArrayWithKeyValuesArray:value];
    }

}

這時返回的值固然是個裝滿模型的數組模型.思路也很簡單,對數組裏的每個成員都進行字典轉模型的方法.若是其中的成員不是自定義模型類,那麼直接返回.

+ (NSMutableArray *)objectArrayWithKeyValuesArray:(id)keyValuesArray{    if ([self isClassFromFoundation:self])        return keyValuesArray;    // 若是是json字符串,轉成字典
    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的替換

@interface IDAndDescription : NSObject@property (nonatomic, copy) NSString *ID;@property (nonatomic, copy) NSString *Description;@end

實際開發中,服務器一般返回一個字段名爲id,或者descriptionJSON數據,而這兩個名字在OC中有特殊含義,如上所示,在定義屬性的時候並不能使用這類名稱.這時屬性名與字典key再也不是直接對應的關係,須要加入一層轉換.

源碼中key的替換也有幾種方式選擇,這裏實現replacedKeyFromPropertyName這一方式.

過程是在要替換key的模型類中實現replacedKeyFromPropertyName方法,返回一個原始key和改名的key對應的字典.replacedKeyFromPropertyNameprotocol中聲明.

實際上,也就是建立了一個方法來獲取屬性名與字典key的對應關係.

在模型類中實現接口中的方法告知對應關係.

@implementation IDAndDescription+ (NSDictionary *)replacedKeyFromPropertyName{    return @{             @"ID" : @"id",             @"Description" : @"description"
             };
}@end

該方法從字典中需找要替換的key,參數是property的名字.若是字典中找不到對應的屬性名,則不須要進行轉換.

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

在獲取值(value)的時候,要將key替換成對應的key.

id value = [keyValues valueForKey:[self.class propertyKey:property.name]];if (!value) continue;

轉換完成.


性能優化

將5個字典轉模型的例子同時進行運行,在+ properties方法中添加一句打印.另外以前的例子都是有內存泄露的,這裏添加了free(properties)修復了這個問題.

+ (NSArray *)properties{
    NSLog(@"%@調用了properties方法",[self class]);
    NSMutableArray *propertiesArray = [NSMutableArray array];    // 1.得到全部的屬性
    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];

        MJProperty *propertyObj = [MJProperty propertyWithProperty:property];

        [propertiesArray addObject:propertyObj];
    }    free(properties);    return propertiesArray;
}

輸出臺輸出以下:


能夠看到,不少的類都不止一次調用了獲取屬性的方法,對於一個類來講,要獲取它的所有屬性,只要獲取一次就夠了.獲取到後將結果緩存起來,下次就沒必要進行沒必要要的計算.

注意:因爲我寫文章時手上的這份源碼相對較早,緩存屬性列表是經過一個全局字典來緩存的,而在最新版本的MJExtension中,已經換成了關聯對象來實現.因爲實現思路大體都是同樣,而且效果相同,因此這裏並不糾結用哪一種方式.

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

將方法改寫爲:

+ (NSArray *)properties
{    NSMutableArray *cachedProperties = cachedProperties_[NSStringFromClass(self)];    if (!cachedProperties) {        NSLog(@"%@調用了properties方法",[self class]);

        cachedProperties = [NSMutableArray array];        // 1.得到全部的屬性
        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];

            MJProperty *propertyObj = [MJProperty propertyWithProperty:property];

            [cachedProperties addObject:propertyObj];
        }

        free(properties);

        cachedProperties_[NSStringFromClass(self)] = cachedProperties;
    }    return cachedProperties;
}

此時控制檯輸出:


能夠看每一個類只通過一次獲取所有屬性.


除了緩存屬性外,提取類型編碼的過程也能夠進一步緩存優化性能.

在下面的方法中加上一句打印:

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

控制檯輸出:


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

重寫了- initWithTypeString:方法:

static NSMutableDictionary *cachedTypes_;
+ (void)load
{
    cachedTypes_ = [NSMutableDictionary dictionary];
}

+ (instancetype)propertyTypeWithAttributeString:(NSString *)string{    return [[MJPropertyType alloc] initWithTypeString:string];
}

- (instancetype)initWithTypeString:(NSString *)string
{    NSUInteger loc = 1;    NSUInteger len = [string rangeOfString:@","].location - loc;    NSString *typeCode = [string substringWithRange:NSMakeRange(loc, len)];    if (!cachedTypes_[typeCode])
    {        NSLog(@"%@",typeCode);        self = [super init];
        [self getTypeCode:typeCode];
        cachedTypes_[typeCode] = self;
    }    return self;
}

輸出結果:


該部分源碼請看項目實例代碼中的<key的替換與性能優化>

相關文章
相關標籤/搜索