iOS-三方庫-MJExtension源碼分析

我們經(jīng)常需要從網(wǎng)絡(luò)上拉取json數(shù)據(jù),然后將json數(shù)據(jù)轉(zhuǎn)化為自己的模型數(shù)據(jù),將json數(shù)據(jù)轉(zhuǎn)化為我們自己的模型數(shù)據(jù)經(jīng)常使用的框架有YYModel和MJExtension,所以現(xiàn)在也是打算花一些時間看一下MJExtension的源碼,并且寫一篇博客記錄一下,因為不記錄下來的話感覺很容易忘,學(xué)習(xí)效果不佳。

一. MJExtension的使用

1. 最簡單的使用

模型:

//User.h
@interface User : NSObject

@property (nonatomic, copy)NSString *name;
@property (nonatomic, copy)NSString *icon;
@property (nonatomic, assign)unsigned int age;
@property (nonatomic, copy)NSString *height;
@property (nonatomic, strong)NSNumber *money;

@end

字典轉(zhuǎn)模型:

//ViewController.m
NSDictionary *dict = @{
                           @"name" : @"Jack",
                           @"icon" : @"lufy.png",
                           @"age" : @20,
                           @"height" : @"1.55",
                           @"money" : @100.9
                           };

// JSON -> User
User *user = [User mj_objectWithKeyValues:dict];
    
NSLog(@"name=%@, icon=%@, age=%u, height=%@, money=%@", user.name, user.icon, user.age, user.height, user.money);

打印結(jié)果:

name=Jack, icon=lufy.png, age=20, height=1.55, money=100.9

通過一句簡單的代碼,就把字典數(shù)據(jù)轉(zhuǎn)化為了模型數(shù)據(jù),非常方便簡潔。

2. 復(fù)雜一點的使用

很多時候json轉(zhuǎn)模型都不是這樣簡單,有時候會出現(xiàn)模型中嵌套模型或者模型中的屬性名和json數(shù)據(jù)中的key不一致的情況。

下面看一下一個Student類的模型:

//Student.h
@interface Student : NSObject

@property (nonatomic, copy)NSString *ID;
@property (nonatomic, copy)NSString *desc;
@property (nonatomic, copy)NSString *nowName;
@property (nonatomic, copy)NSString *oldName;
@property (nonatomic, copy)NSString *nameChangedTime;
@property (nonatomic, strong)Bag *bag;

@end

我們看到Student模型中嵌套著Bag這個模型:

//Bag.h
@interface Bag : NSObject

@property (nonatomic, copy)NSString *name;
@property ( nonatomic, assign)double *price;

@end

然后我們再看一下json數(shù)據(jù):

NSDictionary *dict = @{
                           @"id" : @"20",
                           @"description" : @"kids",
                           @"name" : @{
                                   @"newName" : @"lufy",
                                   @"oldName" : @"kitty",
                                   @"info" : @[
                                           @"test-data",
                                           @{
                                               @"nameChangedTime" : @"2013-08"
                                               }
                                           ]
                                   },
                           @"other" : @{
                                   @"bag" : @{
                                           @"name" : @"a red bag",
                                           @"price" : @100.7
                                           }
                                   }
                           };

可以看到字典數(shù)據(jù)中是id,而模型中是ID,同樣也有desc和description,模型中有newName和oldName這些屬性,而字典中這些屬性在name字段下面,而且nameChangedTime層級也不一樣,bag屬性也是一樣的道理,那么怎么辦呢?

我們只需要讓模型遵守MJKeyValue協(xié)議,實現(xiàn)+ (NSDictionary *)mj_replacedKeyFromPropertyName;協(xié)議方法,如下:

//Student.m
/**
 *  將屬性名換為其他key去字典中取值
 *
 *  @return 字典中的key是屬性名,value是從字典中取值用的key
 */
+ (NSDictionary *)mj_replacedKeyFromPropertyName
{
    return @{
             @"ID" : @"id",
             @"desc" : @"description",
             @"oldName" : @"name.oldName",
             @"nowName" : @"name.newName",
             @"nameChangedTime" : @"name.info[1].nameChangedTime",
             @"bag" : @"other.bag"
             };
}

這個方法的作用就是在給模型賦值的時候,把右邊字段的值賦給模型中左邊字段的屬性。

轉(zhuǎn)化一下試試:

// JSON -> Student
Student *stu = [Student mj_objectWithKeyValues:dict];

// Printing
NSLog(@"ID=%@, desc=%@, oldName=%@, nowName=%@, nameChangedTime=%@",
      stu.ID, stu.desc, stu.oldName, stu.nowName, stu.nameChangedTime);
// ID=20, desc=kids, oldName=kitty, nowName=lufy, nameChangedTime=2013-08

NSLog(@"bagName=%@, bagPrice=%d", stu.bag.name, stu.bag.price);
// bagName=a red bag, bagPrice=100.700000

這里需要注意一個地方就是模型中的nameChangedTime這個屬性,在字典中去取值的時候是取name.info[1].nameChangedTime這個字段的值,這個在后面我們講核心源碼的時候會用到。后面講源碼也會以上面這個為例子來講,這樣比較好理解。

二. MJExtension核心類簡介

1. MJFoundation

//判斷一個類是否是foundation類及其子類
+ (BOOL)isClassFromFoundation:(Class)c;
//判斷屬性是否是協(xié)議里面定義的
+ (BOOL)isFromNSObjectProtocolProperty:(NSString *)propertyName;

2. MJProperty

這個類非常重要,這個類是對我們類中屬性的再封裝。

首先會通過runtime的方法去遍歷類中的屬性:

unsigned int count;
objc_property_t *propertyList = class_copyPropertyList([Student class], &count);
for (int i = 0; i < count; i++) {
    objc_property_t property = propertyList[i];
    const char *propertyName = property_getName(property);
    const char *attris = property_getAttributes(property);
    NSLog(@"%s %s", propertyName, attris);
}

free(propertyList);

打印結(jié)果:

ID T@"NSString",C,N,V_ID
desc T@"NSString",C,N,V_desc
nowName T@"NSString",C,N,V_nowName
oldName T@"NSString",C,N,V_oldName
nameChangedTime T@"NSString",C,N,V_nameChangedTime
bag T@"Bag",&,N,V_bag

通過char類型的attris字符串我們可以看到,它中間有一個串是表示它是屬于哪一個類的,比如NSString,Bag。

通過遍歷類的屬性,我們得到了objc_property_t類型的屬性對象,然后使用這個objc_property_t對象來創(chuàng)建一個對應(yīng)的MJProperty對象,我們看看MJ大神是怎么做的:

#pragma mark - 緩存
+ (instancetype)cachedPropertyWithProperty:(objc_property_t)property
{
    //這里的self是MJProperty類對象
    MJProperty *propertyObj = objc_getAssociatedObject(self, property);
    if (propertyObj == nil) {
        propertyObj = [[self alloc] init];
        propertyObj.property = property;
        //給MJProperty類對象添加關(guān)聯(lián)對象,使用property作為key,關(guān)聯(lián)propertyObj實例對象
        objc_setAssociatedObject(self, property, propertyObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return propertyObj;
}

首先MJ大神通過objc_property_t對象這個key去緩存中取,如果緩存中取不到,那么就根據(jù)objc_property_t來創(chuàng)建一個MJProperty對象,并且把這個MJProperty對象通過property這個key與MJProperty類對象關(guān)聯(lián)起來。那么下次如果再從緩存中取同一個objc_property_t對應(yīng)的MJProperty對象就能取到了,就不用再創(chuàng)建了。這也是MJ大神使用緩存的一個地方。

上面代碼塊中propertyObj.property = property;這行代碼觸發(fā)了MJProperty對象的setter方法:

設(shè)置屬性.png

MJProperty有一個type屬性,這個屬性是MJPropertyType類的,就是表示MJProperty對象的property屬性是屬于什么類型的,具體可看源碼。

另外每一個MJProperty對象還持有著兩個字典,一個是propertyKeysDict,一個是objectClassInArrayDict。

  • propertyKeysDict
    這個字典的key是NSStringFromClass(class),值是一個數(shù)組,比如在復(fù)雜一點的應(yīng)用中,給模型中的nameChangedTime這個屬性賦值的時候,在字典中去取值的時候要對應(yīng)name.info[1].nameChangedTime這個字段的值。那么就要把name,info,1,nameChangedTim,這個四個字段分別封裝為一個MJPropertyKey,加入一個數(shù)組中,作為value。這個數(shù)組在最終取值的時候會用到。

  • objectClassInArrayDict
    這個字典的key也是NSStringFromClass(class),值是一個類對象,表示如果這個MJProperty對象的類型是數(shù)組,并且數(shù)組中的元素類型是模型,那么這個字典的value就是模型的類對象。

上面兩個字典的解釋如果沒看懂也沒關(guān)系,接著往下看,看完核心源碼分析就懂了。

3. MJPropertyKey

上面說過,給模型中的nameChangedTime這個屬性賦值的時候,在字典中取值的時候要對應(yīng)name.info[1].nameChangedTime這個字段的值,那么就要把name,info,1,nameCHangedTime這四個字段分別封裝成一個MJPropertyKey。

它有兩個屬性,一個屬性是name,也就是name、info、1這種,還有一個屬性就是type,它是自定義的MJPropertyKeyType類型的枚舉值,這個枚舉值有兩種類型,即MJPropertyKeyTypeDictionary和MJPropertyKeyTypeArray,像name、info這種就屬于MJPropertyKeyTypeDictionary類型的,1就屬于MJPropertyKeyTypeArray類型的。這個也是在取值的時候用的,類型是MJPropertyKeyTypeDictionary就是從字典中取值,類型是MJPropertyKeyTypeArray就是從數(shù)組中取值。

4. MJPropertyType

MJProperty類有一個屬性是type,這個屬性是MJPropertyType類的,這個type屬性就是表征這個MJProperty對象它的property屬性屬于什么類,NSString類或者NSNumber類等等。MJProperty對象的type是通過截取property的attributes得到code然后初始化為MJPropertyType對象得到的:

_type = [MJPropertyType cachedTypeWithCode:code];

三. 核心源碼分析

我們就從復(fù)雜一點的使用這個例子去看一下MJExtension的核心源碼。

沿著+ (instancetype)mj_objectWithKeyValues:(id)keyValues;這個方法一直往下查找就能找到其核心代碼:

/**
 核心代碼:
 */
- (instancetype)mj_setKeyValues:(id)keyValues context:(NSManagedObjectContext *)context
{
    // 獲得JSON對象
    keyValues = [keyValues mj_JSONObject];
    
    MJExtensionAssertError([keyValues isKindOfClass:[NSDictionary class]], self, [self class], @"keyValues參數(shù)不是一個字典");
    
    //類對象
    Class clazz = [self class];
    //白名單
    NSArray *allowedPropertyNames = [clazz mj_totalAllowedPropertyNames];
    //黑名單
    NSArray *ignoredPropertyNames = [clazz mj_totalIgnoredPropertyNames];
    
    //通過封裝的方法回調(diào)一個通過運行時編寫的,用于返回屬性列表的方法。
    [clazz mj_enumerateProperties:^(MJProperty *property, BOOL *stop) {
        @try {
            // 0.檢測是否被忽略
            if (allowedPropertyNames.count && ![allowedPropertyNames containsObject:property.name]) return;
            if ([ignoredPropertyNames containsObject:property.name]) return;
            
            // 1.取出屬性值
            id value;
            NSArray *propertyKeyses = [property propertyKeysForClass:clazz];
            for (NSArray *propertyKeys in propertyKeyses) {
                value = keyValues;
                for (MJPropertyKey *propertyKey in propertyKeys) {
                    value = [propertyKey valueInObject:value];
                }
                if (value) break;
            }
            
            // 值的過濾
            id newValue = [clazz mj_getNewValueFromObject:self oldValue:value property:property];
            if (newValue != value) { // 有過濾后的新值
                [property setValue:newValue forObject:self];
                return;
            }
            
            // 如果沒有值,就直接返回
            if (!value || value == [NSNull null]) return;
            
            // 2.復(fù)雜處理
            MJPropertyType *type = property.type;
            Class propertyClass = type.typeClass;
            Class objectClass = [property objectClassInArrayForClass:[self class]];
            
            // 不可變 -> 可變處理
            if (propertyClass == [NSMutableArray class] && [value isKindOfClass:[NSArray class]]) {
                value = [NSMutableArray arrayWithArray:value];
            } else if (propertyClass == [NSMutableDictionary class] && [value isKindOfClass:[NSDictionary class]]) {
                value = [NSMutableDictionary dictionaryWithDictionary:value];
            } else if (propertyClass == [NSMutableString class] && [value isKindOfClass:[NSString class]]) {
                value = [NSMutableString stringWithString:value];
            } else if (propertyClass == [NSMutableData class] && [value isKindOfClass:[NSData class]]) {
                value = [NSMutableData dataWithData:value];
            }
            
            if (!type.isFromFoundation && propertyClass) { // 模型屬性
                value = [propertyClass mj_objectWithKeyValues:value context:context];
            } else if (objectClass) {
                if (objectClass == [NSURL class] && [value isKindOfClass:[NSArray class]]) {
                    // string array -> url array
                    NSMutableArray *urlArray = [NSMutableArray array];
                    for (NSString *string in value) {
                        if (![string isKindOfClass:[NSString class]]) continue;
                        [urlArray addObject:string.mj_url];
                    }
                    value = urlArray;
                } else { // 字典數(shù)組-->模型數(shù)組
                    value = [objectClass mj_objectArrayWithKeyValuesArray:value context:context];
                }
            } else {
                if (propertyClass == [NSString class]) {
                    if ([value isKindOfClass:[NSNumber class]]) {
                        // NSNumber -> NSString
                        value = [value description];
                    } else if ([value isKindOfClass:[NSURL class]]) {
                        // NSURL -> NSString
                        value = [value absoluteString];
                    }
                } else if ([value isKindOfClass:[NSString class]]) {
                    if (propertyClass == [NSURL class]) {
                        // NSString -> NSURL
                        // 字符串轉(zhuǎn)碼
                        value = [value mj_url];
                    } else if (type.isNumberType) {
                        NSString *oldValue = value;
                        
                        // NSString -> NSNumber
                        if (type.typeClass == [NSDecimalNumber class]) {
                            value = [NSDecimalNumber decimalNumberWithString:oldValue];
                        } else {
                            NSDecimalNumber *decimalValue = [NSDecimalNumber decimalNumberWithString:oldValue];
                            value = decimalValue == [NSDecimalNumber notANumber] ? @(0) : @(decimalValue.doubleValue);
                        }
                        
                        // 如果是BOOL
                        if (type.isBoolType) {
                            // 字符串轉(zhuǎn)BOOL(字符串沒有charValue方法)
                            // 系統(tǒng)會調(diào)用字符串的charValue轉(zhuǎn)為BOOL類型
                            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 ([value isKindOfClass:[NSNumber class]] && propertyClass == [NSDecimalNumber class]){
                    // 過濾 NSDecimalNumber類型
                    if (![value isKindOfClass:[NSDecimalNumber class]]) {
                        value = [NSDecimalNumber decimalNumberWithDecimal:[((NSNumber *)value) decimalValue]];
                    }
                }
                
                // value和property類型不匹配
                if (propertyClass && ![value isKindOfClass:propertyClass]) {
                    value = nil;
                }
            }
            
            // 3.賦值
            [property setValue:value forObject:self];
        } @catch (NSException *exception) {
            MJExtensionBuildError([self class], exception.reason);
            MJExtensionLog(@"%@", exception);
        }
    }];
    
    // 轉(zhuǎn)換完畢
    if ([self respondsToSelector:@selector(mj_didConvertToObjectWithKeyValues:)]) {
        [self mj_didConvertToObjectWithKeyValues:keyValues];
    }
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Wdeprecated-declarations"
    if ([self respondsToSelector:@selector(mj_keyValuesDidFinishConvertingToObject)]) {
        [self mj_keyValuesDidFinishConvertingToObject];
    }
    if ([self respondsToSelector:@selector(mj_keyValuesDidFinishConvertingToObject:)]) {
        [self mj_keyValuesDidFinishConvertingToObject:keyValues];
    }
#pragma clang diagnostic pop
    return self;
}

這一部分代碼很長,我們一點一點來看:

1. 將json數(shù)據(jù)轉(zhuǎn)化為foundation類型

// 獲得JSON對象
keyValues = [keyValues mj_JSONObject];

MJExtensionAssertError([keyValues isKindOfClass:[NSDictionary class]], self, [self class], @"keyValues參數(shù)不是一個字典");

mj_JSONObject方法:

- (id)mj_JSONObject
{
    if ([self isKindOfClass:[NSString class]]) {
        return [NSJSONSerialization JSONObjectWithData:[((NSString *)self) dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil];
    } else if ([self isKindOfClass:[NSData class]]) {
        return [NSJSONSerialization JSONObjectWithData:(NSData *)self options:kNilOptions error:nil];
    }
    
    return self.mj_keyValues;
}

2. 獲取白名單和黑名單

//類對象
Class clazz = [self class];
//白名單
NSArray *allowedPropertyNames = [clazz mj_totalAllowedPropertyNames];
//黑名單
NSArray *ignoredPropertyNames = [clazz mj_totalIgnoredPropertyNames];

allowedPropertyNames是允許進行字典和模型轉(zhuǎn)換的屬性名數(shù)組,ignoredPropertyNames是不允許進行字典和模型轉(zhuǎn)換額屬性名數(shù)組,要求自己的模型類遵守MJKeyValue協(xié)議,實現(xiàn)協(xié)議的如下方法:

/**
 *  只有這個數(shù)組中的屬性名才允許進行字典和模型的轉(zhuǎn)換
 */
+ (NSArray *)mj_allowedPropertyNames;

/**
 *  這個數(shù)組中的屬性名將會被忽略:不進行字典和模型的轉(zhuǎn)換
 */
+ (NSArray *)mj_ignoredPropertyNames;

然后這倆數(shù)組就是在上面協(xié)議方法中獲取的。

3. 遍歷整個類的屬性

//通過封裝的方法回調(diào)一個通過運行時編寫的,用于返回屬性列表的方法
+ (void)mj_enumerateProperties:(MJPropertiesEnumeration)enumeration
{
    // 獲得成員變量
    MJExtensionSemaphoreCreate
    MJExtensionSemaphoreWait
    NSArray *cachedProperties = [self mj_properties];
    MJExtensionSemaphoreSignal
    // 遍歷成員變量
    BOOL stop = NO;
    for (MJProperty *property in cachedProperties) {
        //回調(diào)block
        enumeration(property, &stop);
        if (stop) break;
    }
}

上面代碼,先獲取類中所有的屬性,然后使用for循環(huán)遍歷每一個屬性,然后執(zhí)行回調(diào)block。

先看一下+ (NSMutableArray *)properties;方法,代碼如下:

+ (NSMutableArray *)mj_properties
{
    //先從緩存中獲取成員變量
    NSMutableArray *cachedProperties = [self mj_propertyDictForKey:&MJCachedPropertiesKey][NSStringFromClass(self)];
    if (cachedProperties == nil) {
    
        //緩存中沒有就遍歷類中的每一個成員變量
        if (cachedProperties == nil) {
            cachedProperties = [NSMutableArray array];
            
            [self mj_enumerateClasses:^(__unsafe_unretained Class c, BOOL *stop) {
                // 1.獲得所有的成員變量
                unsigned int outCount = 0;
                //遍歷類中的屬性
                objc_property_t *properties = class_copyPropertyList(c, &outCount);
                
                // 2.遍歷每一個成員變量
                for (unsigned int i = 0; i<outCount; i++) {
                    //遍歷,然后根據(jù)OC的屬性創(chuàng)建MJProperty
                    //給MJProperty類對象添加關(guān)聯(lián)對象,objc_propert作為key,MJProperty實例對象作為關(guān)聯(lián)的對象
                    MJProperty *property = [MJProperty cachedPropertyWithProperty:properties[i]];
                    // 過濾掉Foundation框架類里面的屬性
                    if ([MJFoundation isClassFromFoundation:property.srcClass]) continue;
                    // 過濾掉`hash`, `superclass`, `description`, `debugDescription`
                    if ([MJFoundation isFromNSObjectProtocolProperty:property.name]) continue;
                    
                    property.srcClass = c;
                    //很重要的兩個方法
                    [property setOriginKey:[self mj_propertyKey:property.name] forClass:self];
                    [property setObjectClassInArray:[self mj_propertyObjectClassInArray:property.name] forClass:self];
                    [cachedProperties addObject:property];
                }
                
                // 3.釋放內(nèi)存
                free(properties);
            }];
            
            //將獲取的cachedProperties緩存下來
            [self mj_propertyDictForKey:&MJCachedPropertiesKey][NSStringFromClass(self)] = cachedProperties;
        }
    }
    
    return cachedProperties;
}

上面代碼,先從緩存中獲取屬性列表,如果緩存中沒有,就遍歷類中的每一個屬性,拿到屬性列表之后再存到緩存。核心代碼就是獲取類中的所有成員變量,然后包裝成MJProperty對象,如下:

[self mj_enumerateClasses:^(__unsafe_unretained Class c, BOOL *stop) {
    // 1.獲得所有的成員變量
    unsigned int outCount = 0;
    //遍歷類中的屬性
    objc_property_t *properties = class_copyPropertyList(c, &outCount);
    
    // 2.遍歷每一個成員變量
    for (unsigned int i = 0; i<outCount; i++) {
        //遍歷,然后根據(jù)OC的屬性創(chuàng)建MJProperty
        //給MJProperty類對象添加關(guān)聯(lián)對象,objc_propert作為key,MJProperty實例對象作為關(guān)聯(lián)的對象
        MJProperty *property = [MJProperty cachedPropertyWithProperty:properties[i]];
        // 過濾掉Foundation框架類里面的屬性
        if ([MJFoundation isClassFromFoundation:property.srcClass]) continue;
        // 過濾掉`hash`, `superclass`, `description`, `debugDescription`
        if ([MJFoundation isFromNSObjectProtocolProperty:property.name]) continue;
        
        property.srcClass = c;
        //很重要的兩個方法
        [property setOriginKey:[self mj_propertyKey:property.name] forClass:self];
        [property setObjectClassInArray:[self mj_propertyObjectClassInArray:property.name] forClass:self];
        [cachedProperties addObject:property];
    }
    
    // 3.釋放內(nèi)存
    free(properties);
}];

首先通過+ (void)mj_enumerateClasses:(MJClassesEnumeration)enumeration這個方法去遍歷當(dāng)前模型類及其父類,當(dāng)追溯到Foundation類型的類時就停止遍歷,方法實現(xiàn)如下:

+ (void)mj_enumerateClasses:(MJClassesEnumeration)enumeration
{
    // 1.沒有block就直接返回
    if (enumeration == nil) return;
    
    // 2.停止遍歷的標(biāo)記
    BOOL stop = NO;
    
    // 3.當(dāng)前正在遍歷的類
    Class c = self;
    
    // 4.開始遍歷每一個類
    while (c && !stop) {
        // 4.1.執(zhí)行操作
        enumeration(c, &stop);
        
        // 4.2.獲得父類
        c = class_getSuperclass(c);
        
        if ([MJFoundation isClassFromFoundation:c]) break;
    }
}

比如有一個Person類,其有兩個屬性name和sex,有一個Student類是繼承自Person類的,這個Student類自己有一個school屬性。那么當(dāng)我們使用runtime的方法讀取Student類的屬性列表時,只能讀取到一個自己聲明的屬性school。但是實際上name和sex也是它的屬性,所以這個時候就要遍歷其父類,拿到所有的屬性。

當(dāng)我們拿到模型類的objc_property_t類型的屬性時,就通過+ (instancetype)cachedPropertyWithProperty:(objc_property_t)property方法將其封裝成MJProperty對象:

+ (instancetype)cachedPropertyWithProperty:(objc_property_t)property
{
    //這里的self是MJProperty類對象
    MJProperty *propertyObj = objc_getAssociatedObject(self, property);
    if (propertyObj == nil) {
        propertyObj = [[self alloc] init];
        propertyObj.property = property;
        //給MJProperty類對象添加關(guān)聯(lián)對象,使用property作為key,關(guān)聯(lián)propertyObj實例對象
        objc_setAssociatedObject(self, property, propertyObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return propertyObj;
}

如上,這個方法先嘗試從關(guān)聯(lián)屬性中通過property對象這個key來取出MJProperty對象,如果取不到就創(chuàng)建一個MJProperty對象,并通過property這個key將其與MJProperty的類對象關(guān)聯(lián)起來,這樣下次就可以直接通過關(guān)聯(lián)屬性來得到MJProperty的值了。然后再通過propertyObj.property = property;這行代碼觸發(fā)set方法,在set方法里面為MJProperty對象的name屬性和type屬性賦值,其中type屬性就是和MJProperty對象關(guān)聯(lián)的property屬于什么類,是NSNumber類還是BOOL類等等:

- (void)setProperty:(objc_property_t)property
{
    _property = property;
    
    MJExtensionAssertParamNotNil(property);
    
    // 1.屬性名
    _name = @(property_getName(property));
    
    // 2.成員類型
    NSString *attrs = @(property_getAttributes(property));
    NSUInteger dotLoc = [attrs rangeOfString:@","].location;
    NSString *code = nil;
    NSUInteger loc = 1;
    if (dotLoc == NSNotFound) { // 沒有,
        code = [attrs substringFromIndex:loc];
    } else {
        code = [attrs substringWithRange:NSMakeRange(loc, dotLoc - loc)];
    }
    _type = [MJPropertyType cachedTypeWithCode:code];
}

下面兩行代碼非常重要

[property setOriginKey:[self mj_propertyKey:property.name] forClass:self];
[property setObjectClassInArray:[self mj_propertyObjectClassInArray:property.name] forClass:self];

對于第一行代碼

+ (id)propertyKey:(NSString *)propertyName這個方法是獲取模型的屬性名在字典中對應(yīng)的key,什么意思呢?還是拿第二個例子來說,它有一個nameChangedTime屬性,由于我們在模型類中實現(xiàn)了+ (NSDictionary *)mj_replacedKeyFromPropertyName這個方法,且這個方法中與nameChangedTime相對應(yīng)的是name.info[1].nameChangedTime,所以+ (id)propertyKey:(NSString *)propertyName返回的就是name.info[1].nameChangedTime這個字符串。

對于- (void)setOriginKey:(id)originKey forClass:(Class)c方法,這個方法會把name.info[1].nameChangedTime這個字符串拆解成一段一段,并封裝成一個個MJPropertyKey對象,組成數(shù)組,賦值給MJProperty的propertyKeysDict這個字典:

setOriginKey forClass.png

- (void)setPorpertyKeys:(NSArray *)propertyKeys forClass:(Class)c方法的實現(xiàn)如下:

/** 對應(yīng)著字典中的多級key */
- (void)setPorpertyKeys:(NSArray *)propertyKeys forClass:(Class)c
{
    if (propertyKeys.count == 0) return;
    NSString *key = NSStringFromClass(c);
    if (!key) return;
    
    MJ_LOCK(self.propertyKeysLock);
    self.propertyKeysDict[key] = propertyKeys;
    MJ_UNLOCK(self.propertyKeysLock);
}

對于第二行代碼

如果模型中有數(shù)組類型的屬性,并且數(shù)組中的元素也是模型類,那么就需要模型類遵守MJKeyValue協(xié)議,實現(xiàn)+ (NSDictionary *)mj_objectClassInArray;協(xié)議方法,就像這樣:模型類中有一個數(shù)組類型的屬性statuses,數(shù)組中的元素類型是模型,模型類是Status;另一個數(shù)組類型的屬性是ads,數(shù)組中的元素類型是模型,模型類是Ad,如下:

+ (NSDictionary *)mj_objectClassInArray
{
    return @{
             @"statuses" : @"Status",
             @"ads" : @"Ad"
             };
}

這時如果在+ (Class)propertyObjectClassInArray:(NSString *)propertyName方法中傳入statuses屬性,那么返回的就是Status類,如下:

propertyObjectClassInArray.png

然后- (void)setObjectClassInArray:(Class)objectClass forClass:(Class)c方法將這個Status類對象賦值給MJProperty對象的objectClassInArrayDict字典。

最后再通過+ (NSMutableDictionary *)mj_propertyDictForKey:(const void *)key方法將獲取的cachedProperties緩存下來,如下:

//將獲取的cachedProperties緩存下來
[self mj_propertyDictForKey:&MJCachedPropertiesKey][NSStringFromClass(self)] = cachedProperties;

方法實現(xiàn)如下:

+ (NSMutableDictionary *)mj_propertyDictForKey:(const void *)key
{
    //靜態(tài)變量
    static NSMutableDictionary *replacedKeyFromPropertyNameDict;
    static NSMutableDictionary *replacedKeyFromPropertyName121Dict;
    static NSMutableDictionary *newValueFromOldValueDict;
    static NSMutableDictionary *objectClassInArrayDict;
    static NSMutableDictionary *cachedPropertiesDict;
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        replacedKeyFromPropertyNameDict = [NSMutableDictionary dictionary];
        replacedKeyFromPropertyName121Dict = [NSMutableDictionary dictionary];
        newValueFromOldValueDict = [NSMutableDictionary dictionary];
        objectClassInArrayDict = [NSMutableDictionary dictionary];
        cachedPropertiesDict = [NSMutableDictionary dictionary];
    });
    
    if (key == &MJReplacedKeyFromPropertyNameKey) return replacedKeyFromPropertyNameDict;
    if (key == &MJReplacedKeyFromPropertyName121Key) return replacedKeyFromPropertyName121Dict;
    if (key == &MJNewValueFromOldValueKey) return newValueFromOldValueDict;
    if (key == &MJObjectClassInArrayKey) return objectClassInArrayDict;
    if (key == &MJCachedPropertiesKey) return cachedPropertiesDict;
    return nil;
}

上面static的靜態(tài)字典,保證程序運行中,字典會一直存在內(nèi)存中,從而達到緩存的目的。

到這里遍歷類的所有屬性就結(jié)束了,這樣獲得了整個類的所有屬性,每個屬性被封裝成了一個MJProperty對象,MJProperty對象有一個property屬性,還有type屬性來表征這個屬性屬于什么類。此外MJProperty對象還保存著兩個字典propertyKeysDict和objectClassInArrayDict,這兩個字典的key都是NSStringFromClass(c),前者的value是一個數(shù)組,這個數(shù)組里面的元素是MJPropertyKey類型的,主要是用來取值用的,后者的value是一個類對象,如果屬性是一個數(shù)組類型的屬性,且數(shù)組元素是模型類型,那么這個值就是模型的類對象。

4. 對模型進行賦值

對模型進行賦值是在+ (void)mj_enumerateProperties:(MJPropertiesEnumeration)enumeration方法的回調(diào)block里一個一個進行的,我們一行一行的看回調(diào)block。

① 檢測是否被忽略

如果這個屬性不在屬性白名單里或者在屬性黑名單里,那么就返回,不對屬性賦值:

// 0.檢測是否被忽略
if (allowedPropertyNames.count && ![allowedPropertyNames containsObject:property.name]) return;
if ([ignoredPropertyNames containsObject:property.name]) return;

② 取出屬性值

從每個屬性的propertyKeysDict字典中取出propertyKeys數(shù)組,根據(jù)propertyKeys數(shù)組來取值:

// 1.取出屬性值
id value;
NSArray *propertyKeyses = [property propertyKeysForClass:clazz];
for (NSArray *propertyKeys in propertyKeyses) {
    value = keyValues;
    for (MJPropertyKey *propertyKey in propertyKeys) {
        value = [propertyKey valueInObject:value];
    }
    if (value) break;
}

我們看一下- (id)valueInObject:(id)object這個方法是怎么操作的:

valueInObject.png

③ 值的過濾

// 值的過濾
id newValue = [clazz mj_getNewValueFromObject:self oldValue:value property:property];
if (newValue != value) { // 有過濾后的新值
    [property setValue:newValue forObject:self];
    return;
}

// 如果沒有值,就直接返回
if (!value || value == [NSNull null]) return;

想要明白上面的代碼就要明白MJKeyValue協(xié)議的如下方法:

/**
 *  舊值換新值,用于過濾字典中的值
 *
 *  @param oldValue 舊值
 *
 *  @return 新值
 */
- (id)mj_newValueFromOldValue:(id)oldValue property:(MJProperty *)property;

MJExtension官方的實例如下:NSString -> NSDate, nil -> @""【過濾字典的值(比如字符串日期處理為NSDate、字符串nil處理為@"")】

// Book
#import "MJExtension.h"

@implementation Book
- (id)mj_newValueFromOldValue:(id)oldValue property:(MJProperty *)property
{
    if ([property.name isEqualToString:@"publisher"]) {
        if (oldValue == nil) return @"";
    } else if (property.type.typeClass == [NSDate class]) {
        NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
        fmt.dateFormat = @"yyyy-MM-dd";
        return [fmt dateFromString:oldValue];
    }

    return oldValue;
}
@end

// NSDictionary
NSDictionary *dict = @{
                       @"name" : @"5分鐘突破iOS開發(fā)",
                       @"publishedTime" : @"2011-09-10"
                       };
// NSDictionary -> Book
Book *book = [Book mj_objectWithKeyValues:dict];

// printing
NSLog(@"name=%@, publisher=%@, publishedTime=%@", book.name, book.publisher, book.publishedTime);

比如,我們需要將服務(wù)端返回的字符串日期處理為NSDate、字符串nil處理為@"",就按照上面的邏輯處理就好了?,F(xiàn)在你應(yīng)該就明白值的過濾是做什么的了。

④ 不可變 -> 可變處理

如果屬性的類型是可變的類型,而取出的value是不可變的類型,那么就要把不可變類型變換為可變的類型:

// 2.復(fù)雜處理
MJPropertyType *type = property.type;
Class propertyClass = type.typeClass;
Class objectClass = [property objectClassInArrayForClass:[self class]];

// 不可變 -> 可變處理
if (propertyClass == [NSMutableArray class] && [value isKindOfClass:[NSArray class]]) {
    value = [NSMutableArray arrayWithArray:value];
} else if (propertyClass == [NSMutableDictionary class] && [value isKindOfClass:[NSDictionary class]]) {
    value = [NSMutableDictionary dictionaryWithDictionary:value];
} else if (propertyClass == [NSMutableString class] && [value isKindOfClass:[NSString class]]) {
    value = [NSMutableString stringWithString:value];
} else if (propertyClass == [NSMutableData class] && [value isKindOfClass:[NSData class]]) {
    value = [NSMutableData dataWithData:value];
}

⑤ 復(fù)雜處理

上面就是完成了對屬性的第一步賦值,但是這還不夠,如果這個屬性是模型類型,那么還要對這個模型再進行一次字典轉(zhuǎn)模型操作。如果這個屬性是數(shù)組類型且數(shù)組元素是模型類型,那么還要進行字典數(shù)組轉(zhuǎn)模型數(shù)組的操作?;蛘邔傩允荖SURL類型,value是NSString類型,這樣也要進行一下轉(zhuǎn)換:

if (!type.isFromFoundation && propertyClass) { // 模型屬性
    //由于屬性是模型類型,所以繼續(xù)進行字典轉(zhuǎn)模型操作
    value = [propertyClass mj_objectWithKeyValues:value context:context];
} else if (objectClass) {
    if (objectClass == [NSURL class] && [value isKindOfClass:[NSArray class]]) {
        // string array -> url array
        NSMutableArray *urlArray = [NSMutableArray array];
        for (NSString *string in value) {
            if (![string isKindOfClass:[NSString class]]) continue;
            [urlArray addObject:string.mj_url];
        }
        value = urlArray;
    } else { // 字典數(shù)組-->模型數(shù)組
        //屬性是數(shù)組類型且數(shù)組元素是模型,則進行字典數(shù)組轉(zhuǎn)模型數(shù)組的操作
        value = [objectClass mj_objectArrayWithKeyValuesArray:value context:context];
    }
} else {
    if (propertyClass == [NSString class]) {
        if ([value isKindOfClass:[NSNumber class]]) {
            // NSNumber 轉(zhuǎn) NSString
            value = [value description];
        } else if ([value isKindOfClass:[NSURL class]]) {
            // NSURL -> NSString
            value = [value absoluteString];
        }
    } else if ([value isKindOfClass:[NSString class]]) {
        if (propertyClass == [NSURL class]) {
            // NSString -> NSURL
            // 字符串轉(zhuǎn)碼
            value = [value mj_url];
        } else if (type.isNumberType) {
            NSString *oldValue = value;
            
            // NSString -> NSNumber
            if (type.typeClass == [NSDecimalNumber class]) {
                value = [NSDecimalNumber decimalNumberWithString:oldValue];
            } else {
                NSDecimalNumber *decimalValue = [NSDecimalNumber decimalNumberWithString:oldValue];
                value = decimalValue == [NSDecimalNumber notANumber] ? @(0) : @(decimalValue.doubleValue);
            }
            
            // 如果是BOOL
            if (type.isBoolType) {
                // 字符串轉(zhuǎn)BOOL(字符串沒有charValue方法)
                // 系統(tǒng)會調(diào)用字符串的charValue轉(zhuǎn)為BOOL類型
                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 ([value isKindOfClass:[NSNumber class]] && propertyClass == [NSDecimalNumber class]){
        // 過濾 NSDecimalNumber類型
        if (![value isKindOfClass:[NSDecimalNumber class]]) {
            value = [NSDecimalNumber decimalNumberWithDecimal:[((NSNumber *)value) decimalValue]];
        }
    }
    
    // value和property類型不匹配
    if (propertyClass && ![value isKindOfClass:propertyClass]) {
        value = nil;
    }
}

⑥ 賦值

// 3.賦值
[property setValue:value forObject:self];

使用KVC賦值,如下:

/**
 *  設(shè)置成員變量的值
 */
- (void)setValue:(id)value forObject:(id)object
{
    if (self.type.KVCDisabled || value == nil) return;
    [object setValue:value forKey:self.name];
}

⑦ 轉(zhuǎn)換完畢

轉(zhuǎn)換完畢,回調(diào)MJKeyValue協(xié)議方法,返回self,如下:

    // 轉(zhuǎn)換完畢
    if ([self respondsToSelector:@selector(mj_didConvertToObjectWithKeyValues:)]) {
        [self mj_didConvertToObjectWithKeyValues:keyValues];
    }
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Wdeprecated-declarations"
    if ([self respondsToSelector:@selector(mj_keyValuesDidFinishConvertingToObject)]) {
        [self mj_keyValuesDidFinishConvertingToObject];
    }
    if ([self respondsToSelector:@selector(mj_keyValuesDidFinishConvertingToObject:)]) {
        [self mj_keyValuesDidFinishConvertingToObject:keyValues];
    }
#pragma clang diagnostic pop
    return self;

這里,整個模型賦值的過程也就完成了。

四. MJExtension中的一些緩存操作

MJExtension中進行了大量的緩存操作來優(yōu)化性能,下面講幾個比較重要的緩存,理解了這些緩存也有助于更深入的理解整個框架。

  1. NSObject+MJProperty這個分類中保存著一個字典cachedPropertiesDict,這個字典的key是NSStringFromClass(class),值就是一個數(shù)組,這個數(shù)組里面存放著一個類的所有屬性。這樣當(dāng)我們下一次還要對同一個類進行模型賦值操作,就可以直接從這個字典里面取出這個類的一個包含所有屬性的數(shù)組了。

  2. MJProperty這個類中,通過runtime的動態(tài)關(guān)聯(lián)屬性的方法,關(guān)聯(lián)每一個objc_property_t,注意是與類對象相關(guān)聯(lián)。value是MJProperty對象:

+ (instancetype)cachedPropertyWithProperty:(objc_property_t)property
{
    //這里的self是MJProperty類對象
    MJProperty *propertyObj = objc_getAssociatedObject(self, property);
    if (propertyObj == nil) {
        propertyObj = [[self alloc] init];
        propertyObj.property = property;
        //給MJProperty類對象添加關(guān)聯(lián)對象,使用property作為key,關(guān)聯(lián)propertyObj實例對象
        objc_setAssociatedObject(self, property, propertyObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return propertyObj;
}

想象一種情況,Teacher和Student都繼承自Person,所以Teacher和Student都有Person的屬性,當(dāng)我們先給Teacher模型賦值的時候,Person類的每一個屬性已經(jīng)調(diào)用了上面的代碼塊封裝成了MJProperty對象,并與MJProperty類對象相關(guān)聯(lián)。那么當(dāng)我們再給Student模型賦值的時候,也會遍歷Person類的屬性,但是這個時候通過MJProperty *propertyObj = objc_getAssociatedObject(self, property);已經(jīng)能得到MJProperty對象了,不用去創(chuàng)建。

  1. 在MJPropertyType中有一個types字典,這個字典是在單例中初始化的,types字典的key是code,value是MJPropertyType對象,每次有新的code,就添加到這個字典里面去,這樣的好處就是如果code一致,就可以直接從字典中取MJPropertyType。

  2. 每一個MJProperty對象都有一個propertyKeysDict字典,這個字典的key是NSStringFromClass(class),值是一個數(shù)組,比如一個MJProperty的名字是name.info[1].text,那么這個數(shù)組就會包括4個MJPropertyKey對象,分別表示name,info,1,text,這些key是在取值的時候用的。那么問題來了,為什么要設(shè)計字典來存儲呢 ,直接用一個數(shù)組來存儲不就好了嗎?
    其實這個問題和2相似,因為我們在第二次遍歷Person類中的屬性的時候不用去創(chuàng)建一個MJProperty對象,直接通過關(guān)聯(lián)屬性去取值就好了,但是Student模型和Teacher模型它們的propertyKeys是有可能不一樣的,所以這里需要一個key來加以區(qū)分。

筆記地址:MJExtension筆記
原文地址:MJExtension源碼解讀

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容