跟著MJExtension實現簡單的字典轉模型框架

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

最簡單的字典

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

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

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

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

最直接的方法是:

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

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

如果要寫一個框架自動幫我們轉模型出來,大致思路如下:

1.遍歷模型中的屬性,然后拿到屬性名作為鍵值去字典中尋找.

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

3.賦值


首先進行第一步:

遍歷模型中的屬性,然后拿到屬性名作為鍵值去字典中尋找.

方法偽代碼:

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

為了方便使用,創建一個叫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的替換與性能優化>

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容