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中還會涉及到其它的一些技術點,在此不多做敘述。