MJExtension原理深入解析

Fabric發表於2018-02-26

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的內部結構和類與類之間的相互關係描述了出來。

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祝大家狗年旺旺旺!

相關文章