MJExtension原理深刻解析

MJExtension概述

MJExtension是是一個很是易用且功能強大的第三方Model和JSON相互轉化的商業化第三方庫,幫助開發者節省了從JSON或者Foundation object轉換成Model所需的時間,並且強大的拓展功能,知足了開發者的大部分數據模型化的需求。html

用MJ本身的話來講,第三方庫MJExtension就是git

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.github

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。app

基本原理

基本原理很是簡單,Fabric在這裏簡略介紹一下ide

第一步:獲取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)];
複製代碼

在這裏須要注意兩點:

一、掌握緩存的技巧能夠提升項目性能,減小代碼重複執行。

二、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祝你們狗年旺旺旺!

相關文章
相關標籤/搜索