你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> iOS開源庫源碼解析之Mantle

iOS開源庫源碼解析之Mantle

編輯:IOS開發綜合

前言

iOS開發中,不管是哪種設計模式,Model層都是不可或缺的。而Model層的第三方庫常用的庫有以下幾個

JSONModel Mantle MJExtension

JSON data到對象的轉換原理都差不多,一般的順序如下

根據Runtime,動態的獲取屬性的類型和屬性的名字,(如果需要,做一次Json的key的Mapping 創建對應的對象實例 根據KVC(NSKeyValueCoding協議)來為屬性設置值

Mantle就是這樣的一個庫,個人比較喜歡Mantle,而且在GithubStar也是提到的幾個庫中最多的。Mantle除了提供JSON和對象的相互轉化,繼承自MTLModel的對象還自動實現了

NSCopying NSCoding isEqual hash

等幾個工具方法。


本文的講解順序

首先會講解幾個Runtime的基礎知識,不理解這個,也就沒辦法掌握這幾個JSON到Model轉化的原理

介紹Runtime如何獲取某一個類的全部屬性的名字 介紹Runtime如何動態獲取屬性的類型

然後,會講解Mantle本身

類的組織架構關系 JSON到對象的處理流程(對象到JSON的過程類似) NSValueTransformer 如何自動實現NSCoding,NSCopying,hash等方法 異常處理 其他認為有用的,例如編譯選項等

本文會很長,希望讀者看完後能有些許收獲,如果發現有任何地方有問題,歡迎指正,我會及時修改。


利用Runtime動態獲取類的屬性

首先,寫兩個類

@interface Base : NSObject
@property (copy,nonatomic)NSString * baseProperty;
@end

@interface Demo : Base
@property (nonatomic,strong)NSDate * createAt;
@property (nonatomic,copy)NSString * name;
@property (nonatomic,assign)CGFloat count;
@end

然後, 寫一個方法來Log Property

-(void)logAllPropertys{
    uint count;
    objc_property_t * propertys = class_copyPropertyList(Demo.class,&count);
    @try {
        for (int i = 0; i < count ; i++) {
            objc_property_t  property = propertys[i];
            NSLog(@"%@",@(property_getName(property)));
        }
    }@finally {
        free(propertys);
    }
}

執行這個方法的Log

2016-05-26 22:51:48.996 LearnMantle[4670:165290] createAt
2016-05-26 22:51:49.001 LearnMantle[4670:165290] name
2016-05-26 22:51:49.001 LearnMantle[4670:165290] count

不難發現class_copyPropertyList僅僅是獲取了當前類的屬性列表,並沒有獲取基類的屬性對象。所以對上述方法進行修改

-(void)logAllPropertys{
    Class cls = Demo.class;
    while (![cls isEqual:NSObject.class]) {
        uint count;
        objc_property_t * propertys;
        @try {
            propertys = class_copyPropertyList(cls,&count);
            cls = cls.superclass;
            for (int i = 0; i < count ; i++) {
                objc_property_t  property = propertys[i];
                NSLog(@"%@",@(property_getName(property)));
            }
        }@finally {
            free(propertys);
        }
    }
}

這裡又個Tips:

class_copyPropertyList返回一個數組,這個數字必須要手動釋放,所以用Try-Catch-Finally包裹起來。後面會介紹,Mantle如何用更簡潔的方式來實現。


利用Runtime來獲取屬性的attributes

關鍵方法property_getAttributes,返回個一個C類型的字符串。
我們先聲明一個這樣的屬性

@property (nonatomic,readonly,copy)id name;

然後,打印出它的attributes信息

    NSLog(@"%@",@(property_getAttributes(class_getProperty(self.class,@"name".UTF8String))));

可以看到Log是

2016-05-28 10:09:10.476 LearnMantle[731:17207] T@,R,C,N,V_name

這裡的Attributes字符串是編碼後的字符串,分為三個部分

T@,T表示開頭,後面跟著屬性的類型,@表示id類型 VnameV表示中間部分的結束,後面跟ivar名字,自動合成呢的情況下前面加下劃線 中間R,C,N用逗號隔開,表示屬性的描述,R表示readonlyC表示CopyN表示Nonatomic

Mantle和ReactiveCocoa都是采用了extobjc這個OC的Runtime工具類將屬性的詳細信息提取到一個結構體裡的,原理都是一樣的。提取完成的結構體是mtl_propertyAttributes


Matnle的類的組織架構

按照文件的方式,

MTLJSONAdapter.h,定義了協議MTLJSONSerializing和適配器類MTLJSONAdapter,這兩個協議/類定義了接口來實現JSON-MTLModel的轉換。

MTLModel.h,定義了協議MTLModel和基類MTLModel,基類MTLModel實現了isEqual,NSCopyinghash幾個方法。

MTLModel+NSCoding.h,MTLModel的類別,讓其支持NSCoding協議

MTLValueTransformer.h,NSValueTransformer的子類,定義了將一個value轉變成另一個value的接口。例如,返回的一個2020-01-01T15:33:30字符串,利用轉換block轉換成NSDate

其它的都是工具類,提供工具方法,不全列出來了。


JSON->對象的處理過程

以下面代碼調用為例(為了看起來不那麼臃腫,省略不必要的代碼)

Demo * demo = [MTLJSONAdapter modelOfClass:[Demo class] fromJSONDictionary:json error:&error];

看看這個方法的具體實現,就知道分為兩個大的過程

+ (id)modelOfClass:(Class)modelClass fromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error {
    //1.根據modelClass初始化一個adapter
    MTLJSONAdapter *adapter = [[self alloc] initWithModelClass:modelClass];
    //2.adapter解析實際的JSON數據
    return [adapter modelFromJSONDictionary:JSONDictionary error:error];
}

現在看看整個第一大步,initWithModelClass,Mantle做了什麼,

1.1,斷言檢查,並保存modelClass

    NSParameterAssert(modelClass != nil);
    NSParameterAssert([modelClass conformsToProtocol:@protocol(MTLJSONSerializing)]);
    //...
    _modelClass = modelClass;

1.2,獲取所有的屬性名字,獲取MTLJSONSerialing中JSONKeyPathsByPropertyKey方法提供的屬性名字->JSON key的映射,並進行合法性檢查

    //屬性名->JSON key的映射
    JSONKeyPathsByPropertyKey = [modelClass JSONKeyPathsByPropertyKey];
    //所有的屬性集合
    NSSet *propertyKeys = [self.modelClass propertyKeys];
    //每一個屬性進行檢查
    for (NSString *mappedPropertyKey in _JSONKeyPathsByPropertyKey) {
        //檢查屬性名->JSON Key映射的屬性名是否合法
        if (![propertyKeys containsObject:mappedPropertyKey]) {
            NSAssert(NO, @"%@ is not a property of %@.", mappedPropertyKey, modelClass);
            return nil;
        }
        //獲取對應的JSON key
        id value = _JSONKeyPathsByPropertyKey[mappedPropertyKey];
        //如果是Array(支持JSON key是Array)
        if ([value isKindOfClass:NSArray.class]) {
            //Array中的每一個Key必須是String類型
            for (NSString *keyPath in value) {
                if ([keyPath isKindOfClass:NSString.class]) continue;

                NSAssert(NO, @"%@ must either map to a JSON key path or a JSON array of key paths, got: %@.", mappedPropertyKey, value);
                return nil;
            }
        } else if (![value isKindOfClass:NSString.class]) {
            //檢查JSON key是否時Array類型
            NSAssert(NO, @"%@ must either map to a JSON key path or a JSON array of key paths, got: %@.",mappedPropertyKey, value);
            return nil;
        }
    }

1.3 獲取所有的NSValueTransformer,來方便做值轉換(例如:服務器JSON返回的是2015-10-01T13:15:15,轉換成NSDate)

    _valueTransformersByPropertyKey = [self.class valueTransformersForModelClass:modelClass];

用過Mantle的都知道,mantle利用”屬性名+JSONTransformer”的方法名字來提供NSValueTransformer,
這裡Mantle用了一些Runtime稍微高級點的東西,所以這個方法我會詳細講解

+ (NSDictionary *)valueTransformersForModelClass:(Class)modelClass {
    //...
    for (NSString *key in [modelClass propertyKeys]) {//對每一個key檢查NSValueTransformer
        //根據屬性名字+JSONTransformer來合成一個Selector
        SEL selector = MTLSelectorWithKeyPattern(key, "JSONTransformer");
        if ([modelClass respondsToSelector:selector]) {//如果提供了Transformer方法
            //獲取IMP指針,也就是實際方法的執行體
            IMP imp = [modelClass methodForSelector:selector];
            //OC方法轉換為C方法的時候,前兩個參數是_cmd,和SEL,所以,這裡做一個強制轉化,方便下一行執行
            NSValueTransformer * (*function)(id, SEL) = (__typeof__(function))imp;
            //獲取transformer,保存到Dictionary
            NSValueTransformer *transformer = function(modelClass, selector);
            if (transformer != nil) result[key] = transformer;
            continue;
        }
        //檢查是否通過協議方法JSONTransformerForKey來提供NSValueTransformer
        if ([modelClass respondsToSelector:@selector(JSONTransformerForKey:)]) {
            //...
        }
        //把一個屬性的類型,關鍵字,屬性名字提取到一個結構體中
        objc_property_t property = class_getProperty(modelClass, key.UTF8String);
        if (property == NULL) continue;
        mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
        @onExit {
            free(attributes);
        };
        NSValueTransformer *transformer = nil;
        //如果某一個屬性是id類型
        if (*(attributes->type) == *(@encode(id))) {
            //獲得該屬性的實際類名
            Class propertyClass = attributes->objectClass;
            if (propertyClass != nil) {
                //獲取該類名型提供的NSValueTransformer,即類是否提供了keyJSONTransformer方法
                transformer = [self transformerForModelPropertiesOfClass:propertyClass];
            }
            //如果該類型也是一個MTLModel,並且實現了MTLJSONSerializing,獲取該對象的NSValueTransformer,也就是保證了在MTLModel的一個屬性也是一個MTLModel的時候能夠正常工作
            if (nil == transformer && [propertyClass conformsToProtocol:@protocol(MTLJSONSerializing)]) {
                transformer = [self dictionaryTransformerWithModelClass:propertyClass];
            }
            //如果仍然沒有獲取到transformer,驗證對於modalClass是否可轉換
            if (transformer == nil) transformer = [NSValueTransformer mtl_validatingTransformerForClass:propertyClass ?: NSObject.class];
        } else {
        //不是ID類型,則是值類型的transformer
            transformer = [self transformerForModelPropertiesOfObjCType:attributes->type] ?: [NSValueTransformer mtl_validatingTransformerForClass:NSValue.class];
        }

        if (transformer != nil) result[key] = transformer;
    }

    return result;
}

再看看第二大步,Adapter如何解析JSON
即這個方法

- (id)modelFromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error {
//...
}

2.1,檢查是否實現了聚類方式解析JSON,例如解析這樣的JSON

[
    {
        "key1":"value1",
        "key2":"value2"
    },
    {
        "key3":"value3",
        "key4":"value4"

    }
]

對應代碼塊

    if ([self.modelClass respondsToSelector:@selector(classForParsingJSONDictionary:)]) {
        //...
    }

2.2,對於每一個Property的名字,即propertyKey,獲取對應的JSON key。根據JSON key 來獲取對應的值,主要掉用mtl_valueForJSONKeyPath:success:error:

這個方法很簡單,比如對應json的keyPath是person.name.first
先分解成person,name,first,然後一層一層的獲取json[person][name][first],只不過Mantle在解析的時候,用了個for循環,來給用戶反饋,到底錯誤在哪裡。個人感覺用以下兩個KVC的方法更簡潔一點

//驗證是否可用KVC
- validateValue:forKeyPath:error:
//用KVC來獲取值
- valueForKeyPath:

2.3,對於2.2種,獲取到的值,利用1.3的NSValueTransformer進行轉換,這裡只知道NSValueTransformer能夠把一個值轉換成另一個值就行了,後面會詳細講解如何轉換的。


Tips:
這裡要提到的是,Mantle采用了條件編譯方式來處理異常,即debug模式下會拋出異常給開發者,但是release模式下,不會崩潰

#if DEBUG
    @throw ex;
#else
    //...           
#endif

 

2.4 根據以上三步得到的值字典,對每一個key利用KVC進行設置值,KVC設置值之前,調用

[obj validateValue:&validatedValue forKey:key error:error]

來驗證是否可以KVC


NSValueTransformer

官方文檔

NSValueTranformer是一個抽象的基類,利用Cocoa Bindings技術來進行值的相互轉換

既然是一個抽象基類,那麼使用的時候要繼承這個基類,然後實現必要的方法,從而才能進行相應的值轉換。

例如,實現一個簡單的NSDate<->NSString轉換的Transformer

@interface LHValueTransformer : NSValueTransformer

@end

@implementation LHValueTransformer

+(BOOL)allowsReverseTransformation{
    return YES;
}
+(Class)transformedValueClass{
    return [NSString class];
}
-(NSDateFormatter *)dateFormatter{
    NSDateFormatter * formatter = [[NSDateFormatter alloc] init];
    formatter.dateFormat = @"yyyy-MM-dd HH:mm:ss";
    return formatter;
}
-(id)transformedValue:(id)value{
    NSAssert([value isKindOfClass:[NSDate class]], @"Should a NSDate value");

    return [[self dateFormatter] stringFromDate:value];
}
-(id)reverseTransformedValue:(id)value{
    NSAssert([value isKindOfClass:[NSString class]], @"Should be a NSString value");
    return [[self dateFormatter] dateFromString:value];
}
@end

然後,這樣掉用

    NSValueTransformer * trans = [[LHValueTransformer alloc] init];

    NSDate * date = [NSDate date];
    NSString * str = [trans transformedValue:date];
    NSDate * date2 = [trans reverseTransformedValue:str];

MTLValueTransformer就是這樣的一個子類,只不過它提供了正反兩個轉換的block作為接口。


isEqual,NSCopying,hash

實現NSCopying和hash很簡單,就是基類根據Runtime動態的獲取所有的屬性,然後對應的進行操作就可以了


#pragma mark NSCopying

- (instancetype)copyWithZone:(NSZone *)zone {
    MTLModel *copy = [[self.class allocWithZone:zone] init];
    [copy setValuesForKeysWithDictionary:self.dictionaryValue];
    return copy;
}

#pragma mark NSObject

- (NSString *)description {
    NSDictionary *permanentProperties = [self dictionaryWithValuesForKeys:self.class.permanentPropertyKeys.allObjects];

    return [NSString stringWithFormat:@"<%@: %p> %@", self.class, self, permanentProperties];
}

- (NSUInteger)hash {
    NSUInteger value = 0;
    //每個value取hash值
    for (NSString *key in self.class.permanentPropertyKeys) {
        value ^= [[self valueForKey:key] hash];
    }

    return value;
}

- (BOOL)isEqual:(MTLModel *)model {
    if (self == model) return YES;
    if (![model isMemberOfClass:self.class]) return NO;

    for (NSString *key in self.class.permanentPropertyKeys) {
        id selfValue = [self valueForKey:key];
        id modelValue = [model valueForKey:key];
        //每一個value取isEqual
        BOOL valuesEqual = ((selfValue == nil && modelValue == nil) || [selfValue isEqual:modelValue]);
        if (!valuesEqual) return NO;
    }

    return YES;
}

NSCoding

NSCoding的支持有些復雜,源代碼MTLModel+NSCoding.m

對於initWithCoder:
1. 根據Runtime,獲取所有的屬性名字
2. 對於每一個屬性,檢查是否響應decodeWithCoder:modelVersion:,也就是說,支持屬性也是MTLModel對象,如果是,則調用decodeWithCoder:modelVersion:解析這個MTLModel
3. 如果不是MTLModel子類,則調用decodeObjectForKey來解析,這裡的key就是屬性的名字

encodeWithCoder類似,不做講解


異常處理

Mantle中,有一些

@try{}
@catch{}
@finally{}

並且在catch模塊中

#if DEBUG
    @throw ex;
#else
    //其它處理
#endif

這樣能夠方便調試錯誤,並且在運行時的時候不崩潰。

同時,你還能看到這樣的代碼

mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
@onExit {
    free(attributes);
};

這裡的@onExit是一個宏定義,保證代碼在在當前域返回(return,break,異常)始終能執行到。其實本質就是把代碼放到了finally裡


__attribute__

__attribute__機制能夠為方法,變量,類型增加額外的屬性。
增加的額外屬性,能夠讓編譯器進行額外的檢查,從而提供額外的提示
比如

@property (nonatomic, strong, readonly) id model __attribute__((unavailable("Replaced by -modelFromJSONDictionary:error:")));

+ (NSArray *)JSONArrayFromModels:(NSArray *)models __attribute__((deprecated("Replaced by +JSONArrayFromModels:error:"))) NS_SWIFT_UNAVAILABLE("Replaced by +JSONArrayFromModels:error:");

就分別提示model當前不可用unavailable,和JSONArrayFromModels方法被deprecated

後面有時間了,系統的整理下所有的__attribute__,今天很晚了,先這樣吧


後續

下一篇會寫React Native的博客,然後MBProgressHud或者AFN的源碼分析

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved