Whew Mantle

What is Mantle

Mantle是一个用于简化Model层的第三方库

Mantle effet

  • 不想为ModelJSON互转写一大堆
  • 不想为Model支持archiveunarchive写一堆
  • 不想为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的哈希算法从综合性考虑来说的确已经是不错的选择了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,567评论 18 399
  • 在经过一次没有准备的面试后,发现自己虽然写了两年的android代码,基础知识却忘的差不多了。这是程序员的大忌,没...
    猿来如痴阅读 2,827评论 3 10
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,174评论 11 349
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,431评论 25 707
  • 网上有一张这样的图,用表情来表示周一到周五的心情,周一是困死。周三是倦怠,周五却是眉开眼笑。想来大家都是这...
    走失的风阅读 292评论 0 1