MJExtension概述
MJExtension是是一個非常易用且功能強大的第三方Model和JSON相互轉化的商業化第三方庫,幫助開發者節省了從JSON或者Foundation object轉換成Model所需的時間,而且強大的擴充功能,滿足了開發者的大部分資料模型化的需求。
用MJ自己的話來說,第三方庫MJExtension就是
A fast, convenient and nonintrusive conversion between JSON and model. Your model class don't need to extend another base class. You don't need to modify any model file.
MJExtension專案原始碼請檢視github上的Demo
下面,Fabric就來為大家揭開MJExtension的神祕面紗。
你需要了解的知識:
這篇文章適合於iOS中級開發者,在開啟閱讀之前,你需要了解以下知識點:
- Objective-C runtime執行時機制,有興趣的請參閱runtime iOS官方文件介紹
- objc_property_t結構體的相關知識點,尤其是Declared property type encodings的相關知識點,具體請見Declared Properties iOS官方文件介紹
- 類的分類進行動態新增屬性的原理。
- block和delegate的基本使用方法。
- SEL指標的基本使用方法。
- C語言結構體中變數的儲存結構,定址方法。
However,無論你了不瞭解這些知識點,相信讀完整篇文章都會幫助你更加深入地瞭解Object-C這門語言,領略它獨特的魅力。
為了方便敘述,我把一些基本的Foundation的資料結構,例如NSDictionary, NSArray, NSSet等統一稱之為Foundation object。
基本原理
基本原理非常簡單,Fabric在這裡簡略介紹一下
第一步:獲取NSObject中的所有屬性
unsigned int propertyCount = 0;
///通過執行時獲取當前類的屬性
objc_property_t *propertys = class_copyPropertyList([self class], &propertyCount);
//把屬性放到陣列中
for (int i = 0; i < propertyCount; i ++) {
///取出第一個屬性
objc_property_t property = propertys[i];
//得到屬性對應的名稱
NSString *name = @(property_getName(property));
NSLog(@"name:%@", name);
}
複製程式碼
ps:雖然沒有看到runtime的完整原始碼,但是有些方法的內部構造我們還是可以猜測出來的,例如:
const char * _Nonnull property_getName(objc_property_t _Nonnull property) {
return property->name;
}
複製程式碼
這就是一個對於獲取結構體對應指標值的一個很簡單的包裝。
第二步:在Foundation object(陣列、字典等)以name 為key,尋找到對應的value值,然後將對應值填充入相應的Model當中
- (void)setValue:(id)value forObject:(id)object
{
if (self.type.KVCDisabled || value == nil) return;
[object setValue:value forKey:self.name];
}
複製程式碼
ps:這裡需要著重介紹一下,NSObject可以通過-[setValue:forKey:]的方式對相應的屬性進行賦值,瞭解這點對於瞭解MJExtension原理很有必要。
MJExtension的優勢
看了Fabric剛才的基本原理介紹,大家可能認為JSON轉化為model非常簡單嘛,核心程式碼也就幾句。但是,MJExtension作為商業化SDK,它的強大優勢在於它的相容性好,擴充性強。開發者可以替換key值的名稱,可以將陣列裡面的字典轉化為對應的model,可以忽略某些轉換的屬性,也可以定義所有需要轉換的屬性,還可以針對於一些舊值,轉換為新值,例如時間戳和時間的相互轉換。
MJExtension詳細原理
MJExtension內部結構
Fabric畫了一張原理圖,大致地將MJExtension的內部結構和類與類之間的相互關係描述了出來。
- MJExtensionConst類: 類中定義了一些字串常量,分別表示property的屬性型別,具體的型別字串是存放在MJProperty的屬性MJPropertyType中的code屬性下的。不同型別的屬性由不同的encode字串表示,讀者可以自己
@encode(int)
@encode(float)
@encode(NSString)
列印出一些常用型別屬性的encode值來加深理解。 - MJPropertyType類: 用於記錄MJProperty的一些相關的特性,例如要轉換的物件的型別,是否是Foundation object型別物件等等,主要包括:
+ (instancetype)cachedTypeWithCode:(NSString *)code
用於查詢快取的type型別。
BOOL idType
BOOL numberType
BOOL boolType
等一堆代表要轉換value物件具體型別的屬性。
Class typeClass
表明value物件的型別。
NSString *code
,用來寫入Property的encode值。
BOOL fromFoundation
,用來表示要轉換的 物件是否是NSDictionary``NSArray``NSSet
等基本的Foundation object型別,簡單來說就是如果要轉換的物件是NSObject的子類且不是NSManagedObject類就返回NO。
KVCDisabled
該物件是否能被監聽
- MJPropertyKey類: 負責將value寫入MJProperty,主要包括:
- (id)valueInObject:(id)object
用於將value值寫入MJProperty物件。MJPropertyKeyType type
用於表明當前的MJProperty中需要轉換的value是一個字典裡面的value還是在陣列裡面的value。NSString *name
用於表示當前的NSDictionary中value的key值或者NSArray中value的index。 - MJFoundation類: 判斷當前物件是不是Foundation object(NSDictionary, NSArray等)
- MJProperty類: MJExtension包裝屬性值的基本單位,每一個objc_property_t值都有一個MJProperty來進行包裝,這個類是整個MJExtension程式碼中最核心的一個類,對於該類的作用,Fabric會在下文為大家介紹。
- NSObject+MJProperty類: 為開發者預留了一些可以重寫的方法和block,開發者可以用這些方法可以將字典中的key替換為Model中對應的Property,可以指定NSArray中的字典對應的Model。
- NSObject+MJClass類: 設定JSON和Model互轉的黑白名單,設定歸檔的黑白名單。
- NSObject+MJKeyValue類: JSON和Model互轉的實現類。
- NSString+MJExtension類: 一些字串特殊處理的方法,包括大小寫互相轉換,駝峰命名法和下劃線命名法字串的相互轉換等。
- NSObject+MJCoding類: 重寫了
-[encodeObject:forKey:]
和-[decoderObject:forkey:]
兩個方法,使得物件可以直接進行歸檔操作。
MJExtension核心程式碼分析
MJExtension的設計非常巧妙,涉及的方法也非常多,有限的篇幅裡面很難說的細緻入微,所以Fabric決定帶大家一起探索一下MJExtension實現Model轉換的核心的方法: - (instancetype)mj_setKeyValues:(id)keyValues context:(NSManagedObjectContext *)context
。我們按照方法裡面的程式碼,由上之下,從內而外執行下去:
//將JSON轉換為Foundation object物件(NSDictionary, NSArray等)
keyValues = [keyValues mj_JSONObject];
複製程式碼
//設定黑名單和白名單
NSArray *allowedPropertyNames = [clazzmj_totalAllowedPropertyNames];
NSArray *ignoredPropertyNames = [clazz mj_totalIgnoredPropertyNames];
複製程式碼
//聲稱了所有的MJProperty屬性並進行遍歷輸出
[clazz mj_enumerateProperties:^(MJProperty *property, BOOL *stop) {
//遍歷所有的MJProperty物件,設定到Model的對應的屬性當中
}
複製程式碼
現在我們探究一下,每一個MJProperty物件是怎麼生成的,結合我上面的MJExtension內部結構圖,大家可能理解起來比較容易。
在NSObject+MJProperty
類中,+ (NSMutableArray *)properties
這個方法是專門負責生成所有的MJProperty物件的。
//首先從快取中讀取儲存的MJProperty陣列
NSMutableArray *cachedProperties = [self dictForKey:&MJCachedPropertiesKey][NSStringFromClass(self)];
複製程式碼
在這裡需要注意兩點:
1、掌握快取的技巧可以提高專案效能,減少程式碼重複執行。
2、Fabric認為這裡並不需要用字典來儲存MJproperty物件陣列,因為每一個Model都對應一個class,所以不存在兩個class公用一個cachedProperty的情況,因此直接用陣列來承接MJProperty屬性陣列就可以了。
繼續探究+ (NSMutableArray *)properties
方法,
如果沒有快取,就遍歷所有的非Foundation object基本型別的物件,取出objc_property_t陣列,包裝成MJProperty陣列。
unsigned int outCount = 0;
objc_property_t *properties = class_copyPropertyList(c, &outCount);
for (unsigned int i = 0; i<outCount; i++) {
//包裝properties
MJProperty *property = [MJProperty cachedPropertyWithProperty:properties[i]];
if ([MJFoundation isClassFromFoundation:property.srcClass]) continue;
property.srcClass = c;
[property setOriginKey:[self propertyKey:property.name] forClass:self];
[property setObjectClassInArray:[self propertyObjectClassInArray:property.name] forClass:self];
[cachedProperties addObject:property];
}
複製程式碼
Fabric認為
if ([MJFoundation isClassFromFoundation:property.srcClass]) continue;
property.srcClass = c;
複製程式碼
這兩行程式碼有些雞肋,因為srcClass只有兩種可能:Model型別或者nil,所以完全可以使用一個BOOL值來判斷srcClass是否是Foundation object型別,而不用一個Class *srcClass
屬性。
在這裡需要著重理解這幾行程式碼
[property setOriginKey:[self propertyKey:property.name] forClass:self];
[property setObjectClassInArray:[self propertyObjectClassInArray:property.name]
複製程式碼
第一個方法是把所有要替換的key值包裝成陣列儲存到NSMutableDictionary *propertyKeysDict
物件中;
第二個方法是把陣列中對應的想要轉換成的Model的Class型別儲存到NSMutableDictionary *objectClassInArrayDict
字典中。
理解了這兩個方法也就理解了+ (NSDictionary *)mj_replacedKeyFromPropertyName
和+ (void)mj_setupObjectClassInArray:(MJObjectClassInArray)objectClassInArray
兩個功能函式的實現原理了。
繼續看如何包裝objc_property_t property
,先從快取中讀取MJProperty。
//這裡需要注意property指標指向的記憶體地址每次都是不變的,所以可以這樣動態關聯
MJProperty *propertyObj = objc_getAssociatedObject(self, property);
複製程式碼
如果沒有快取就把property包裝成一個MJProperty,
- (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的name和attrs中的encode屬性通過擷取字串來獲得屬性的型別。 下面來看一下MJProperty的type屬性是如何設定的,
#pragma mark - 公共方法
- (void)setCode:(NSString *)code
{
_code = code;
MJExtensionAssertParamNotNil(code);
if ([code isEqualToString:MJPropertyTypeId]) {
_idType = YES;
} else if (code.length == 0) {
_KVCDisabled = YES;
} else if (code.length > 3 && [code hasPrefix:@"@\""]) {
// 去掉@"和",擷取中間的型別名稱
_code = [code substringWithRange:NSMakeRange(2, code.length - 3)];
_typeClass = NSClassFromString(_code);
_fromFoundation = [MJFoundation isClassFromFoundation:_typeClass];
_numberType = [_typeClass isSubclassOfClass:[NSNumber class]];
} else if ([code isEqualToString:MJPropertyTypeSEL] ||
[code isEqualToString:MJPropertyTypeIvar] ||
[code isEqualToString:MJPropertyTypeMethod]) {
_KVCDisabled = YES;
}
// 是否為數字型別
NSString *lowerCode = _code.lowercaseString;
NSArray *numberTypes = @[MJPropertyTypeInt, MJPropertyTypeShort, MJPropertyTypeBOOL1, MJPropertyTypeBOOL2, MJPropertyTypeFloat, MJPropertyTypeDouble, MJPropertyTypeLong, MJPropertyTypeLongLong, MJPropertyTypeChar];
if ([numberTypes containsObject:lowerCode]) {
_numberType = YES;
if ([lowerCode isEqualToString:MJPropertyTypeBOOL1]
|| [lowerCode isEqualToString:MJPropertyTypeBOOL2]) {
_boolType = YES;
}
}
}
複製程式碼
以上程式碼判斷了property屬性的具體型別,參考@encode()函式和MJExtensionConst方法,大家應該能夠理解以上程式碼。
到這裡,MJProperty的包裝就基本說完了。
緊接著,取出keyValues中對應的值,
// 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;
}
複製程式碼
處理value值,
// 值的過濾
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;
複製程式碼
最後是根據MJProperty的type中的typeClass,將不可擴充套件的集合轉換成可變集合,方便後續的操作。對特定的值進行處理,如果是模型屬性就繼續進行遞迴轉化,
if (!type.isFromFoundation && propertyClass) { // 模型屬性
value = [propertyClass mj_objectWithKeyValues:value context:context];
}
複製程式碼
如果是陣列,就遍歷陣列,如果陣列中還是陣列就繼續遞迴,如果不是呼叫+[mj_objectWithKeyValues:context]轉化成對應的Model元素放入陣列,
for (NSDictionary *keyValues in keyValuesArray) {
if ([keyValues isKindOfClass:[NSArray class]]){
[modelArray addObject:[self mj_objectArrayWithKeyValuesArray:keyValues context:context]];
} else {
id model = [self mj_objectWithKeyValues:keyValues context:context];
if (model) [modelArray addObject:model];
}
}
複製程式碼
對於其他的propertyClass型別,Fabric在這裡不做贅述,大家可以自己研究,並不複雜。
最後一步,將經過處理的value值代入Model當中,
// 3.賦值
[property setValue:value forObject:self];
複製程式碼
在模型轉換完成之後,大家還可以重寫- (void)mj_keyValuesDidFinishConvertingToObject
這個函式進行後續操作。
說到這裡,Fabric基本上把MJExtension的Foundation object(或者JSON)轉換成Model的原理細緻的說了一遍。因為篇幅有限,本人能力有限,很多東西沒有說清楚,感興趣的同學和朋友們可以加我的微信:justlikeitRobert進行詳細探討。
謝謝大家的耐心閱讀,Fabric祝大家狗年旺旺旺!