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.實現(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的哈希算法從綜合性考慮來說的確已經是不錯的選擇了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容

  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,717評論 18 399
  • 在經過一次沒有準備的面試后,發(fā)現(xiàn)自己雖然寫了兩年的android代碼,基礎知識卻忘的差不多了。這是程序員的大忌,沒...
    猿來如癡閱讀 2,863評論 3 10
  • 從三月份找實習到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時芥藍閱讀 42,319評論 11 349
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,643評論 25 708
  • 網(wǎng)上有一張這樣的圖,用表情來表示周一到周五的心情,周一是困死。周三是倦怠,周五卻是眉開眼笑。想來大家都是這...
    走失的風閱讀 300評論 0 1