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.实现MTLJSONSerializing协议来描述Model和JSON的Key值映射关系。如果某个字段的值存在在JSON的二级节点下,可以通过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这种非常规类型或自定义类型时,需要做类型转换。
+ (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
子类同样应该重新实现MTLJSONSerializing协议中的键值映射方法和特殊类型转换方法,并且一般的做法是:调用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的时候,我们能通过在基类中实现MTLJSONSerializing协议里的一个方法,来快速决定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的基本数据类型,将抛出异常NSInvalidArgumentException
NSObject的非正式协议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;
由于大多数情况基本数据类型属性的缺省值都为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。
哈希碰撞
我们知道,如果两个对象是相等的,那么哈希值必定相等;但是哈系值相等时,两个对象不一定相等。出现这种现象称之为哈希碰撞。
换个说法,在一个哈希表中,如果一个key哈希后对应地址中已经存放了值,这种情况就是哈希碰撞。
为什么要减少哈希碰撞
Objective-C对于一个需要进行 hash 运算的容器,很重要的一点就是避免哈希碰撞。哈希碰撞会出现两个或多个 key 映射到哈希表中同一存储位置。此时,哈希表会保持旧value的存储位置,并将新的value放置在离碰撞位置最近的可用存储位置。一旦哈希表里出现过碰撞,并且存储数据越来越多时,再次碰撞的可能性就会越来越大,寻找碰撞发生时可存储空间的耗时也将变大。
哈希算法
从Mantle源码上可以看出,MTLModel的哈希算法是将所有属性的hash值通过^运算进行了哈希值合并。
现在看下这样的情形:
如果群资料Model类只有群名称和群签名属性,上面的哈希算法等价于:
- (NSUInteger)hash {
return [_name hash] ^ [_sign hash];
}
那么有可能出现:groupA和groupB不相等,但是哈希值相等。
/*
* [@"游戏" hash] ^ [@"团购" hash]
* 等价于
* [@"团队" hash] ^ [@"游戏" hash]
*
* 简单的按位异或算法操作是对称性的,造成了不同属性之间差异性的丢失
*/
groupA.name = @"游戏";
groupA.sign = @"团购";
groupB.name = @"团购";
groupB.sign = @"游戏";
尽管哈希碰撞不可避免,此算法的效率也非常高,而且实际运行中也基本很少会出现哈希碰撞,但是依然可以做个优化方面的讨论。
现在尝试用一种非对称性的哈希算法作为MTLModel的哈希函数
#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的遍历是无序的,且这里的哈希值计算是非对称性的,这会导致拥有相同元素的多个NSSet对象遍历计算出来的哈希值很可能会不同。换句话说,将会出现两个相同对象的哈希值不相等。
重新优化后的哈希算法:
- (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;
}
此时的哈希算法实现了我们的目的,但效率要比MTLModel原有的哈希算法要差。
作为Model层,解析和计算一定要高效,MTLModel的哈希算法从综合性考虑来说的确已经是不错的选择了。