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工程