What is Mantle
Mantle是一個用于簡化Model層的第三方庫
Mantle effet
- 不想為Model和JSON互轉寫一大堆
- 不想為Model支持archive和unarchive寫一堆
- 不想為Model支持Copy寫一大堆
- 不想看到Model里又包含了一大堆沒必要的Model
- 不想為Model支持Merge煞費苦心
- 不想為Model重寫description
Have a try
1.模擬一個群資料的Model
@interface JCGroupProfile : MTLModel <MTLJSONSerializing>
@property (nonatomic, copy, readonly) NSString *gid;
@property (nonatomic, copy, readonly) NSString *ownerID;
@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, copy, readonly) NSString *sign;
@property (nonatomic, strong, readonly) NSArray *photos;
@property (nonatomic, strong, readonly) NSDate *createDate;
@property (nonatomic, assign, readonly) NSUInteger *level;
@property (nonatomic, assign, readonly) NSUInteger *memberCount;
@property (nonatomic, assign, readonly) NSUInteger *memberMaxCount;
@property (nonatomic, assign, readonly) BOOL isVip;
@end
2.實現(xiàn)MTLJSONSerializing協(xié)議來描述Model和JSON的Key值映射關系。如果某個字段的值存在在JSON的二級節(jié)點下,可以通過keypath的方式設置。比如示例代碼中的@"ownerID" : @"owner_info.userid"
+ (NSDictionary *)JSONKeyPathsByPropertyKey
{
return @{
@"gid" : @"gid",
@"ownerID" : @"owner_info.userid",
@"name" : @"name",
@"sign" : @"sign",
@"photos" : @"photos",
@"createDate" : @"create_time",
@"level" : @"level",
@"memberCount" : @"member_count",
@"memberMaxCount" : @"member_max_count",
@"isVip" : @"is_vip",
};
}
3.如果屬性中有諸如NSDate這種非常規(guī)類型或自定義類型時,需要做類型轉換。
+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key
{
if ([key isEqualToString:@"createDate"]) {
return [MTLValueTransformer transformerUsingForwardBlock:^id(id value, BOOL *success, NSError *__autoreleasing *error) {
if (success && value && [value isKindOfClass:[NSNumber class]]) {
return [NSDate dateWithTimeIntervalSince1970:[(NSNumber *)value longValue]];
}
return nil;
} reverseBlock:^id(id value, BOOL *success, NSError *__autoreleasing *error) {
if (success && value && [value isKindOfClass:[NSDate class]]) {
return @([(NSDate *)value timeIntervalSince1970]);
}
return nil;
}];
}
return nil;
}
4.實際使用
- (void)hanldeResponseDic:(NSDictionary *)jsonDic
{
// JSON Convert To Model
JCGroupProfile *groupProfile = [MTLJSONAdapter modelOfClass:JCGroupProfile.class fromJSONDictionary:jsonDic error:nil];
// support copy
JCGroupProfile *groupProfileCopy = [groupProfile copy];
// support merge
[groupProfile mergeValuesForKeysFromModel:groupProfileCopy];
// support compare two Model Obj
BOOL isEqual = [groupProfile isEqual:groupProfileCopy];
// support archive
NSString *documentPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *filePath = [documentPath stringByAppendingPathComponent:@"file.archiver"];
[NSKeyedArchiver archiveRootObject:groupProfile toFile:filePath];
// support description
NSLog(@"groupProfile = %@", groupProfile);
// Model Convert To JSON
NSDictionary *jsonDicFromModel = [MTLJSONAdapter JSONDictionaryFromModel:groupProfile error:nil];
}
5.嘗試應對子類場景
@interface JCVipGroupProfile : JCGroupProfile
@property (nonatomic, assign, readonly) NSUInteger vipLevel;
@property (nonatomic, copy, readonly) NSString *activity;
@end
子類同樣應該重新實現(xiàn)MTLJSONSerializing協(xié)議中的鍵值映射方法和特殊類型轉換方法,并且一般的做法是:調用super并且補充self
+ (NSDictionary *)JSONKeyPathsByPropertyKey
{
NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithDictionary:[super JSONKeyPathsByPropertyKey]];
[dic setValuesForKeysWithDictionary:@{
@"vipLevel" : @"vip_level",
@"activity" : @"activity",
}];
return dic;
}
+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key
{
if ([key isEqualToString:@"aPropertyKey"]) {
// 補充子類里需要轉換類型的屬性
}
return [super JSONTransformerForKey:key];
}
6.嘗試應對類簇場景。先看看類簇的定義:
類簇是Foundation框架中廣泛使用的設計模式。類簇將一些私有的、具體的子類組合在一個公共的、抽象的超類下面,以這種方法來組織類可以簡化一個面向對象框架的公開架構,而又不減少功能的豐富性。
其實類簇簡單點理解就是抽象工廠。
假設有一個群資料基類GroupProfile,子類有GameGroupProfile、VipGroupProfile、SuperVipGroupProfile、ShopGroupProfile等,每個子類群資料都有自己擴展的字段,且這四種子類群類型都是互斥的。(不可能既是游戲群,同時又是會員群)
假設服務器返回一段群資料的JSON,在做JSON轉Model的時候,我們能通過在基類中實現(xiàn)MTLJSONSerializing協(xié)議里的一個方法,來快速決定JSON解析時應該轉換成具體哪個類型的子類對象。
+ (Class)classForParsingJSONDictionary:(NSDictionary *)JSONDictionary
{
if ([JSONDictionary[@"is_vip"] boolValue]) {
return [JCVipGroupProfile class];
}
if ([JSONDictionary[@"is_supervip"] boolValue]) {
return [JCSuperVipGroupProfile class];
}
if ([JSONDictionary[@"is_game"] boolValue]) {
return [JCGameGroupProfile class];
}
if ([JSONDictionary[@"is_shot"] boolValue]) {
return [JCShopGroupProfile class];
}
return self;
}
此時解析JSON可直接傳遞GroupProfile類型給MTLJSONAdapter作為解析的目標類型。
7.空標量異常處理
{
"level": null
}
假設服務器API返回了上面這個字段,Mantle會怎樣做解析轉換?
先來看看Mantle的源代碼片段:
__autoreleasing id value = [dictionary objectForKey:key];
if ([value isEqual:NSNull.null]) value = nil;
BOOL success = MTLValidateAndSetValue(self, key, value, YES, error);
可以看出,Mantle內部將null值轉換為了nil。
Mantle是基于KVC給property賦值的。如果property是一個諸如NSNumber的引用類型,運行ok;如果property是一個諸如Int的基本數(shù)據(jù)類型,將拋出異常NSInvalidArgumentException
NSObject的非正式協(xié)議NSKeyValueCoding提供了一個解決方法:
/*
Given that an invocation of -setValue:forKey: would be unable to set the keyed value because the type of the parameter of the corresponding accessor method is an NSNumber scalar type or NSValue structure type but the value is nil, set the keyed value using some other mechanism.
The default implementation of this method raises an NSInvalidArgumentException.
You can override it to map nil values to something meaningful in the context of your application.
*/
- (void)setNilValueForKey:(NSString *)key;
由于大多數(shù)情況基本數(shù)據(jù)類型屬性的缺省值都為0,所以可以直接在Mantle源碼里找到MTLModel.m,override方法:
- (void)setNilValueForKey:(NSString *)key
{
[self setValue:@0 forKey:key];
}
這樣,我們自定義的所有MTLModel子類,都能避免空標量異常。
如果有需要將缺省值置為諸如-1的情況時,可以在MTLModel子類中override方法:
- (void)setNilValueForKey:(NSString *)key
{
if ([key isEqualToString:@"level"]) {
self.level = -1;
} else {
[super setNilValueForKey:key];
}
}
Mantle Source Code Analysis
isEqual And Hash
MTLModel類的源碼中override了-hash
和-isEqual:
兩個方法,讓所有它的子類都很好地支持了判斷對象相等以及計算hash值。
- (NSUInteger)hash {
NSUInteger value = 0;
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];
BOOL valuesEqual = ((selfValue == nil && modelValue == nil) || [selfValue isEqual:modelValue]);
if (!valuesEqual) return NO;
}
return YES;
}
判斷兩個對象是否相等的思路:遍歷類對象中的相關屬性,依次檢測它們是否相等,如果有一個不相等,就返回 NO;否則,返回 YES。
哈希碰撞
我們知道,如果兩個對象是相等的,那么哈希值必定相等;但是哈系值相等時,兩個對象不一定相等。出現(xiàn)這種現(xiàn)象稱之為哈希碰撞。
換個說法,在一個哈希表中,如果一個key哈希后對應地址中已經存放了值,這種情況就是哈希碰撞。
為什么要減少哈希碰撞
Objective-C對于一個需要進行 hash 運算的容器,很重要的一點就是避免哈希碰撞。哈希碰撞會出現(xiàn)兩個或多個 key 映射到哈希表中同一存儲位置。此時,哈希表會保持舊value的存儲位置,并將新的value放置在離碰撞位置最近的可用存儲位置。一旦哈希表里出現(xiàn)過碰撞,并且存儲數(shù)據(jù)越來越多時,再次碰撞的可能性就會越來越大,尋找碰撞發(fā)生時可存儲空間的耗時也將變大。
哈希算法
從Mantle源碼上可以看出,MTLModel的哈希算法是將所有屬性的hash值通過^運算進行了哈希值合并。
現(xiàn)在看下這樣的情形:
如果群資料Model類只有群名稱和群簽名屬性,上面的哈希算法等價于:
- (NSUInteger)hash {
return [_name hash] ^ [_sign hash];
}
那么有可能出現(xiàn):groupA和groupB不相等,但是哈希值相等。
/*
* [@"游戲" hash] ^ [@"團購" hash]
* 等價于
* [@"團隊" hash] ^ [@"游戲" hash]
*
* 簡單的按位異或算法操作是對稱性的,造成了不同屬性之間差異性的丟失
*/
groupA.name = @"游戲";
groupA.sign = @"團購";
groupB.name = @"團購";
groupB.sign = @"游戲";
盡管哈希碰撞不可避免,此算法的效率也非常高,而且實際運行中也基本很少會出現(xiàn)哈希碰撞,但是依然可以做個優(yōu)化方面的討論。
現(xiàn)在嘗試用一種非對稱性的哈希算法作為MTLModel的哈希函數(shù)
#define NSUINTEGER_BIT (CHAR_BIT * sizeof(NSUInteger))
#define NSUINTEGER_RORATE(value, shift) ((((NSUInteger)value) << shift) | (((NSUInteger)value) >> (NSUINTEGER_BIT - shift)))
- (NSUInteger)hash {
NSUInteger value = 0;
NSUInteger mark = 0;
for (NSString *key in self.class.permanentPropertyKeys) {
if (mark % 2 == 0) {
value ^= NSUINTEGER_RORATE([[self valueForKey:key] hash], NSUINTEGER_BIT/2);
} else {
value ^= [[self valueForKey:key] hash];
}
++mark;
}
// 溢出處理
if (value > NSUIntegerMax) {
while (value > NSUIntegerMax) {
value -= NSUIntegerMax;
}
} else if (value < 0) {
while (value < 0) {
value += NSUIntegerMax;
}
}
return value;
}
這個算法表面上看上去沒太大問題,但是其實是有一個坑:快速枚舉的對象permanentPropertyKeys是一個NSSet實例,由于NSSet的遍歷是無序的,且這里的哈希值計算是非對稱性的,這會導致?lián)碛邢嗤氐亩鄠€NSSet對象遍歷計算出來的哈希值很可能會不同。換句話說,將會出現(xiàn)兩個相同對象的哈希值不相等。
重新優(yōu)化后的哈希算法:
- (NSUInteger)hash {
NSMutableArray *keysArray = [NSMutableArray array];
for (NSString *key in self.class.permanentPropertyKeys) {
[keysArray addObject:key];
}
NSArray *sortedKeysArray = [keysArray sortedArrayUsingComparator:^NSComparisonResult(NSString *obj1, NSString *obj2) {
return [obj1 compare:obj2];
}];
NSUInteger value = 0;
for (NSString *key in sortedKeysArray) {
if ([sortedKeysArray indexOfObject:key] % 2 == 0) {
value ^= NSUINTEGER_RORATE([[self valueForKey:key] hash], NSUINTEGER_BIT/2);
} else {
value ^= [[self valueForKey:key] hash];
}
}
// 溢出處理
if (value > NSUIntegerMax) {
while (value > NSUIntegerMax) {
value -= NSUIntegerMax;
}
} else if (value < 0) {
while (value < 0) {
value += NSUIntegerMax;
}
}
return value;
}
此時的哈希算法實現(xiàn)了我們的目的,但效率要比MTLModel原有的哈希算法要差。
作為Model層,解析和計算一定要高效,MTLModel的哈希算法從綜合性考慮來說的確已經是不錯的選擇了。