你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 源碼篇:Mantle

源碼篇:Mantle

編輯:IOS開發基礎

Mantle是一個用於簡化Cocoa或Cocoa Touch程序中model層的第三方庫。通常我們的應該中都會定義大量的model來表示各種數據結構,而這些model的初始化和編碼解碼都需要寫大量的代碼。而Mantle的優點在於能夠大大地簡化這些代碼。

Mantle源碼中最主要的內容包括:

  • MTLModel類:通常是作為我們的Model的基類,該類提供了一些默認的行為來處理對象的初始化和歸檔操作,同時可以獲取到對象所有屬性的鍵值集合。

  • MTLJSONAdapter類:用於在MTLModel對象和JSON字典之間進行相互轉換,相當於是一個適配器。

  • MTLJSONSerializing協議:需要與JSON字典進行相互轉換的MTLModel的子類都需要實現該協議,以方便MTLJSONApadter對象進行轉換。

在此就以這三者作為我們的分析點。

基類MTLModel

MTLModel是一個抽象類,它主要提供了一些默認的行為來處理對象的初始化和歸檔操作。

初始化

MTLModel默認的初始化方法-init並沒有做什麼事情,只是調用了下[super init]。而同時,它提供了一個另一個初始化方法:

- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error;

其中參數dictionaryValue是一個字典,它包含了用於初始化對象的key-value對。我們來看下它的具體實現:

- (instancetype)initWithDictionary:(NSDictionary *)dictionary error:(NSError **)error {
    ...
    for (NSString *key in dictionary) {
        // 1. 將value標記為__autoreleasing,這是因為在MTLValidateAndSetValue函數中,
        //    可以會返回一個新的對象存在在該變量中
        __autoreleasing id value = [dictionary objectForKey:key];
        // 2. value如果為NSNull.null,會在使用前將其轉換為nil
        if ([value isEqual:NSNull.null]) value = nil;
        // 3. MTLValidateAndSetValue函數利用KVC機制來驗證value的值對於key是否有效,
        //    如果無效,則使用使用默認值來設置key的值。
        //    這裡同樣使用了對象的KVC特性來將value值賦值給model對應於key的屬性。
        //    有關MTLValidateAndSetValue的實現可參考源碼,在此不做詳細說明。
        BOOL success = MTLValidateAndSetValue(self, key, value, YES, error);
        if (!success) return nil;
    }
    ...
}

子類可以重寫該方法,以在設置完對象的屬性後做進一步的處理或初始化工作,不過需要記住的是:應該通過super來調用父類的實現。

獲取屬性的鍵(key)、值(value)

MTLModel類提供了一個類方法+propertyKeys,該方法返回所有@property聲明的屬性所對應的名稱字符串的一個集合,但不包括只讀屬性和MTLModel自身的屬性。在這個類方法會去遍歷model的所有屬性,如果屬性是非只讀且其ivar值不為NULL,則獲取到表示屬性名的字符串,並將其放入到集合中,其實現如下:

+ (NSSet *)propertyKeys {
    // 1. 如果對象中已有緩存的屬性名的集合,則直接返回緩存。該緩存是放在一個關聯對象中。
    NSSet *cachedKeys = objc_getAssociatedObject(self, MTLModelCachedPropertyKeysKey);
    if (cachedKeys != nil) return cachedKeys;
    NSMutableSet *keys = [NSMutableSet set];
    // 2. 遍歷對象所有的屬性
    //    enumeratePropertiesUsingBlock方法會沿著superclass鏈一直向上遍歷到MTLModel,
    //    查找當前model所對應類的繼承體系中所有的屬性(不包括MTLModel),並對該屬性執行block中的操作。
    //    有關enumeratePropertiesUsingBlock的實現可參考源碼,在此不做詳細說明。
    [self enumeratePropertiesUsingBlock:^(objc_property_t property, BOOL *stop) {
        mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
        @onExit {
            free(attributes);
        };
        // 3. 過濾只讀屬性和ivar為NULL的屬性
        if (attributes->readonly && attributes->ivar == NULL) return;
        // 4. 獲取屬性名字符串,並存儲到集合中
        NSString *key = @(property_getName(property));
        [keys addObject:key];
    }];
    // 5. 將集合緩存到關聯對象中。
    objc_setAssociatedObject(self, MTLModelCachedPropertyKeysKey, keys, OBJC_ASSOCIATION_COPY);
    return keys;
}

有了上面這個類方法,要想獲取到對象中所有屬性及其對應的值就方法了。為此MTLModel提供了一個只讀屬性dictionaryValue來取一個包含當前model所有屬性及其值的字典。如果屬性值為nil,則會用NSNull來代替。另外該屬性不會為nil。

@property (nonatomic, copy, readonly) NSDictionary *dictionaryValue;
// 實現
- (NSDictionary *)dictionaryValue {
    return [self dictionaryWithValuesForKeys:self.class.propertyKeys.allObjects];
}

合並對象

合並對象是指將兩個MTLModel對象按照自定義的方法將其對應的屬性值進行合並。為此,在MTLModel定義了以下方法:

- (void)mergeValueForKey:(NSString *)key fromModel:(MTLModel *)model;

該方法將當前對象指定的key屬性的值與model參數對應的屬性值按照指定的規則來進行合並,這種規則由我們自定義的-mergeFromModel:方法來確定。如果我們的子類中實現了-mergeFromModel:方法,則會調用它;如果沒有找到,且model不為nil,則會用model的屬性的值來替代當前對象的屬性的值。具體實現如下:

- (void)mergeValueForKey:(NSString *)key fromModel:(MTLModel *)model {
    NSParameterAssert(key != nil);
    // 1. 根據傳入的key拼接"mergeFromModel:"字符串,並從該字符串中獲取到對應的selector
    //    如果當前對象沒有實現-mergeFromModel:方法,且model不為nil,則用model的屬性值
    //    替代當前對象的屬性值
    //
    //    MTLSelectorWithCapitalizedKeyPattern函數以C語言的方式來拼接方法字符串,具體實現請
    //    參數源碼,在此不詳細說明
    SEL selector = MTLSelectorWithCapitalizedKeyPattern("merge", key, "FromModel:");
    if (![self respondsToSelector:selector]) {
        if (model != nil) {
            [self setValue:[model valueForKey:key] forKey:key];
        }
        return;
    }
    // 2. 通過NSInvocation方式來調用對應的-mergeFromModel:方法。
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]];
    invocation.target = self;
    invocation.selector = selector;
    [invocation setArgument:&model atIndex:2];
    [invocation invoke];
}

此外,MTLModel還提供了另一個方法來合並兩個對象所有的屬性值,即:

- (void)mergeValuesForKeysFromModel:(MTLModel *)model;

需要注意的是model必須是當前對象所屬類或其子類。

歸檔對象(Archive)

Mantle將對MTLModel的編碼解碼處理都放在了MTLModel的NSCoding分類中進行處理了,該分類及相關的定義都放在MTLModel+NSCoding文件中。

對於不同的屬性,在編碼解碼過程中可能需要區別對待,為此Mentle定義了枚舉MTLModelEncodingBehavior來確定一個MTLModel屬性被編碼到一個歸檔中的行為。其定義如下:

typedef enum : NSUInteger {
    MTLModelEncodingBehaviorExcluded = 0,           // 屬性絕不應該被編碼
    MTLModelEncodingBehaviorUnconditional,          // 屬性總是應該被編碼
    MTLModelEncodingBehaviorConditional,            // 對象只有在其它地方被無條件編碼時才應該被編碼。這只適用於對象屬性
} MTLModelEncodingBehavior;

具體每個屬性的歸檔行為我們可以在+encodingBehaviorsByPropertyKey類方法中設置。MTLModel類為我們提供了一個默認實現,如下:

+ (NSDictionary *)encodingBehaviorsByPropertyKey {
    // 1. 獲取所有屬性鍵值
    NSSet *propertyKeys = self.propertyKeys;
    NSMutableDictionary *behaviors = [[NSMutableDictionary alloc] initWithCapacity:propertyKeys.count];
    // 2. 對每一個屬性進行處理
    for (NSString *key in propertyKeys) {
        objc_property_t property = class_getProperty(self, key.UTF8String);
        NSAssert(property != NULL, @"Could not find property \"%@\" on %@", key, self);
        mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
        @onExit {
            free(attributes);
        };
        // 3. 當屬性為weak時,默認設置為MTLModelEncodingBehaviorConditional,否則默認為MTLModelEncodingBehaviorUnconditional,設置完後,將其封裝在NSNumber中並放入字典中。
        MTLModelEncodingBehavior behavior = (attributes->weak ? MTLModelEncodingBehaviorConditional : MTLModelEncodingBehaviorUnconditional);
        behaviors[key] = @(behavior);
    }
    return behaviors;
}

任何不在該返回字典中的屬性都不會被歸檔。子類可以根據自己的需要來指定各屬性的歸檔行為。但在實際時應該通過super來調用父類的實現。

而為了從歸檔中解碼指定的屬性,Mantle提供了以下方法:

- (id)decodeValueForKey:(NSString *)key withCoder:(NSCoder *)coder modelVersion:(NSUInteger)modelVersion;

默認情況下,該方法會查找當前對象中類似於-decodeWithCoder:modelVersion:的方法,如果找到便會調用相應方法,並按照自定義的方式來處理屬性的解碼。如果我們沒有實現自定義的方法或者coder不需要安全編碼,則會對指定的key調用-[NSCoder decodeObjectForKey:]方法。其具體實現如下:

- (id)decodeValueForKey:(NSString *)key withCoder:(NSCoder *)coder modelVersion:(NSUInteger)modelVersion {
    ...
    SEL selector = MTLSelectorWithCapitalizedKeyPattern("decode", key, "WithCoder:modelVersion:");
    // 1. 如果自定義了-decodeWithCoder:modelVersion:方法,則通過NSInvocation來調用方法
    if ([self respondsToSelector:selector]) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]];
        invocation.target = self;
        invocation.selector = selector;
        [invocation setArgument:&coder atIndex:2];
        [invocation setArgument:&modelVersion atIndex:3];
        [invocation invoke];
        __unsafe_unretained id result = nil;
        [invocation getReturnValue:&result];
        return result;
    }
    @try {
        // 2. 如果沒有找到自定義的-decodeWithCoder:modelVersion:方法,
        //    則走以下流程。
        //
        // coderRequiresSecureCoding方法的具體實現請參數源碼
        if (coderRequiresSecureCoding(coder)) {
            // 3. 如果coder要求安全編碼,則會從需要安全編碼的字典中取出屬性所對象的類型,然後根據指定
            //    類型來對屬性進行解碼操作。
            //    為此,MTLModel提供了類方法allowedSecureCodingClassesByPropertyKey,來獲取
            //    類的對象包含的所有需要安全編碼的屬性及其對應的類的字典。該方法首先會查看是否已有
            //    緩存的字典,如果沒有則遍歷類的所有屬性。首先過濾掉那些不需要編碼的屬性,
            //    然後遍歷剩下的屬性,如果是非對象類型或類類型,則其對應的類型設定為NSValue,
            //    如果是這兩者,則對應的類型即為相應類型。
            //    該方法的具體實現請參考源代碼。
            NSArray *allowedClasses = self.class.allowedSecureCodingClassesByPropertyKey[key];
            NSAssert(allowedClasses != nil, @"No allowed classes specified for securely decoding key \"%@\" on %@", key, self.class);
            return [coder decodeObjectOfClasses:[NSSet setWithArray:allowedClasses] forKey:key];
        } else {
            // 4. 不需要安全編碼
            return [coder decodeObjectForKey:key];
        }
    } @catch (NSException *exception) {
        ...
    }
}

當然,所有的編碼解碼工作還得需要我們實現-initWithCoder:和-encodeWithCoder:兩個方法來完成。我們在定義MTLModel的子類時,可以根據自己的需要來對特定的屬性進行處理,不過最好調用super的實現來執行父類的操作。MTLModel對這兩個方法的實現請參考源碼,在此不多作說明。

適配器MTLJSONApadter

為了便於在MTLModel對象和JSON字典之間進行相互轉換,Mantle提供了類MTLJSONApadter,作為這兩者之間的一個適配器。

MTLJSONSerializing協議

Mantle定義了一個協議MTLJSONSerializing,那些需要與JSON字典進行相互轉換的MTLModel的子類都需要實現該協議,以方便MTLJSONApadter對象進行轉換。這個協議中定義了三個方法,具體如下:

@protocol MTLJSONSerializing
@required
+ (NSDictionary *)JSONKeyPathsByPropertyKey;
@optional
+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key;
+ (Class)classForParsingJSONDictionary:(NSDictionary *)JSONDictionary;
@end

這三個方法都是類方法。其中+JSONKeyPathsByPropertyKey是必須實現的,它返回的字典指定了如何將對象的屬性映射到JSON中不同的key path(字符串值或NSNull)中。任何不在此字典中的屬性被認為是與JSON中使用的key值相匹配。而映射到NSNull的屬性在JSON序列化過程中將不進行處理。

+JSONTransformerForKey:方法指定了如何將一個JSON值轉換為指定的屬性值。反過來,轉換器也用於將屬性值轉換成JSON值。如果轉換器實現了+JSONTransformer方法,則MTLJSONAdapter會使用這個具體的方法,而不使用+JSONTransformerForKey:方法。另外,如果不需要執行自定義的轉換,則返回nil。

重寫+classForParsingJSONDictionary:方法可以將當前Model解析為一個不同的類對象。這對象類簇是非常有用的,其中抽象基類將被傳遞給-[MTLJSONAdapter initWithJSONDictionary:modelClass:]方法,而實例化的則是子類。

如果我們希望MTLModel的一個子類能使用MTLJSONApadter來進行轉換,則需要實現這個協議,並實現相應的方法。

初始化

MTLJSONApadter對象有一個只讀屬性,該屬性即為適配器需要處理的MTLModel對象,其聲明如下:

@property (nonatomic, strong, readonly) MTLModel
*model;

可見該對象必須是實現了MTLJSONSerializing協議的MTLModel對象。該屬性是只讀的,因此它只能通過初始化方法來初始化。

MTLJSONApadter對象不能通過-init來初始化,這個方法會直接斷言。而是需要通過類提供的兩個初始化方法來初始化,如下:

- (id)initWithJSONDictionary:(NSDictionary *)JSONDictionary modelClass:(Class)modelClass error:(NSError **)error;
- (id)initWithModel:(MTLModel*)model;

其中-(id)initWithJSONDictionary:modelClass:error:是使用一個字典和需要轉換的類來進行初始化。字典JSONDictionary表示一個JSON數據,這個字典需要符合NSJSONSerialization返回的格式。如果該參數為空,則方法返回nil,且返回帶有MTLJSONAdapterErrorInvalidJSONDictionary碼的error對象。該方法的具體實現如下:

- (id)initWithJSONDictionary:(NSDictionary *)JSONDictionary modelClass:(Class)modelClass error:(NSError **)error {
    ...
    if (JSONDictionary == nil || ![JSONDictionary isKindOfClass:NSDictionary.class]) {
        ...
        return nil;
    }
    if ([modelClass respondsToSelector:@selector(classForParsingJSONDictionary:)]) {
        modelClass = [modelClass classForParsingJSONDictionary:JSONDictionary];
        if (modelClass == nil) {
            ...
            return nil;
        }
        ...
    }
    ...
    _modelClass = modelClass;
    _JSONKeyPathsByPropertyKey = [[modelClass JSONKeyPathsByPropertyKey] copy];
    NSMutableDictionary *dictionaryValue = [[NSMutableDictionary alloc] initWithCapacity:JSONDictionary.count];
    NSSet *propertyKeys = [self.modelClass propertyKeys];
    // 1. 檢驗model的+JSONKeyPathsByPropertyKey中字典key-value對的有效性
    for (NSString *mappedPropertyKey in self.JSONKeyPathsByPropertyKey) {
        // 2. 如果model對象的屬性不包含+JSONKeyPathsByPropertyKey返回的字典中的某個屬性鍵值
        //    則返回nil。即+JSONKeyPathsByPropertyKey中指定的屬性鍵值必須是model對象所包含
        //    的屬性。
        if (![propertyKeys containsObject:mappedPropertyKey]) {
            ...
            return nil;
        }
        id value = self.JSONKeyPathsByPropertyKey[mappedPropertyKey];
        // 3. 如果屬性不是映射到一個JSON關鍵路徑或者是NSNull,也返回nil。
        if (![value isKindOfClass:NSString.class] && value != NSNull.null) {
            ...
            return nil;
        }
    }
    for (NSString *propertyKey in propertyKeys) {
        NSString *JSONKeyPath = [self JSONKeyPathForPropertyKey:propertyKey];
        if (JSONKeyPath == nil) continue;
        id value;
        @try {
            value = [JSONDictionary valueForKeyPath:JSONKeyPath];
        } @catch (NSException *ex) {
            ...
            return nil;
        }
        if (value == nil) continue;
        @try {
            // 4. 獲取一個轉換器,
            //    如上所述,+JSONTransformerForKey:會先去查看是否有+JSONTransformer方法,
            //    如果有則會使用這個具體的方法,如果沒有,則調用相應的+JSONTransformerForKey:方法
            //    該方法具體實現請參考源碼
            NSValueTransformer *transformer = [self JSONTransformerForKey:propertyKey];
            if (transformer != nil) {
                // 5. 獲取轉換器轉換生的值
                if ([value isEqual:NSNull.null]) value = nil;
                value = [transformer transformedValue:value] ?: NSNull.null;
            }
            dictionaryValue[propertyKey] = value;
        } @catch (NSException *ex) {
            ...
            return nil;
        }
    }
    // 6. 初始化_model
    _model = [self.modelClass modelWithDictionary:dictionaryValue error:error];
    if (_model == nil) return nil;
    return self;
}

另外,MTLJSONApadter還提供了幾個類方法來創建一個MTLJSONApadter對象,如下:

+ (id)modelOfClass:(Class)modelClass fromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error;
+ (NSArray *)modelsOfClass:(Class)modelClass fromJSONArray:(NSArray *)JSONArray error:(NSError **)error;
+ (NSDictionary *)JSONDictionaryFromModel:(MTLModel*)model;

具體實現可參考源碼。

從對象中獲取JSON數據

從MTLModel對象中獲取JSON數據是上述初始化過程中的一個逆過程。該過程由-JSONDictionary方法來實現,具體如下:

- (NSDictionary *)JSONDictionary {
    NSDictionary *dictionaryValue = self.model.dictionaryValue;
    NSMutableDictionary *JSONDictionary = [[NSMutableDictionary alloc] initWithCapacity:dictionaryValue.count];
    [dictionaryValue enumerateKeysAndObjectsUsingBlock:^(NSString *propertyKey, id value, BOOL *stop) {
        NSString *JSONKeyPath = [self JSONKeyPathForPropertyKey:propertyKey];
        if (JSONKeyPath == nil) return;
        // 1. 獲取屬性的值
        NSValueTransformer *transformer = [self JSONTransformerForKey:propertyKey];
        if ([transformer.class allowsReverseTransformation]) {
            if ([value isEqual:NSNull.null]) value = nil;
            value = [transformer reverseTransformedValue:value] ?: NSNull.null;
        }
        NSArray *keyPathComponents = [JSONKeyPath componentsSeparatedByString:@"."];
        // 2. 對於嵌套屬性值的設置,會先從keypath中獲取每一層屬性,
        //    如果當前層級的obj中沒有該屬性,則為其設置一個空字典;然後再進入下一層級,依此類推
        //    最後設置如下形式的字典: @{@"nested": @{@"name": @"foo"}}
        id obj = JSONDictionary;
        for (NSString *component in keyPathComponents) {
            if ([obj valueForKey:component] == nil) {
                [obj setValue:[NSMutableDictionary dictionary] forKey:component];
            }
            obj = [obj valueForKey:component];
        }
        [JSONDictionary setValue:value forKeyPath:JSONKeyPath];
    }];
    return JSONDictionary;
}

從上可以看出,該方法實際上最終獲得的是一個字典。而獲得字典後,再將其序列化為JSON串就容易了。

MTLJSONApadter也提供了一個簡便的方法,來從一個model中獲取一個JSON字典,其定義如下:

+ (NSDictionary *)JSONDictionaryFromModel:(MTLModel
*)model;

MTLManagedObjectAdapter

為了適應Core Data,Mantle專門定義了MTLManagedObjectAdapter類。該類用作MTLModel對象與NSManagedObject對象之前的轉換。具體的我們在此不詳細描述。

技術點總結

Mantle的功能主要是進行對象間數據的轉換:即如何在一個MTLModel和一個JSON字典中進行數據的轉換。因此,所使用的技術大都是Cocoa Foundation提供的功能。除了對於Core Data的處理之外,主要用到的技術的有如下幾條:

KVC的應用:這主要體現在對MTLModel子類的屬性賦值中,通過KVC機制來驗證值的有效性並為屬性賦值。

NSValueTransform:這主要用於對JSON值轉換為屬性值的處理,我們可以自定義轉換器來滿足我們自己的轉換需求。

NSInvocation:這主要用於統一處理針對特定key值的一些方法的調用。比如-mergeFromModel:這一類方法。

Run time函數的使用:這主要用於對從一個字符串中獲取到方法對應的字符串,然後通過sel_registerName函數來注冊一個selector。

當然在Mantle中還會涉及到其它的一些技術點,在此不多做敘述。

參考

Mantle工程

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