我們經(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方法:

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這個字典:

- (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類,如下:

然后- (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這個方法是怎么操作的:

③ 值的過濾
// 值的過濾
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)化性能,下面講幾個比較重要的緩存,理解了這些緩存也有助于更深入的理解整個框架。
NSObject+MJProperty這個分類中保存著一個字典cachedPropertiesDict,這個字典的key是NSStringFromClass(class),值就是一個數(shù)組,這個數(shù)組里面存放著一個類的所有屬性。這樣當(dāng)我們下一次還要對同一個類進行模型賦值操作,就可以直接從這個字典里面取出這個類的一個包含所有屬性的數(shù)組了。
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)建。
在MJPropertyType中有一個types字典,這個字典是在單例中初始化的,types字典的key是code,value是MJPropertyType對象,每次有新的code,就添加到這個字典里面去,這樣的好處就是如果code一致,就可以直接從字典中取MJPropertyType。
每一個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源碼解讀