字典轉模型

背景介紹

在iOS開發中,也不知道是誰先起頭的,喜歡用Object-C的動態特性。一個比較普遍的應用是JSON解析之后的Dictionary轉自定義的Model,將類class轉換為struct,取出其中的成員屬性數組,然后用一個循環,跟網絡收到的dictionary進行對照,省去了model.property = dictionary[key]這些據說是沒有技術含量的體力活。感覺上是有點高大上了。
為此,gitHub上還出現了像Mantle這樣比較重的第三方庫,當然功能會很多,比如property和key的名字不必相同,可以復雜一點array套dictionary再套array,同時還實現NSCoding和NSCopying協議,方便使用序列化,和直接=號copy
這樣真的好嗎?其實這些都是Object-C可以和C和C++混編這種方便性給慣出來的毛病。在Object-C中混上C的語句,直接調用iOS底層的API就是技術高的體現?可以說是也可以說不是。
在Object-C中混入C,直接調用最底層的runtime,運用了Object-C語言的本質,貌似應該是“高深”的技術,同時也體現了技術人員為實現產品人那些奇葩要求而進行的不懈努力,從這個角度講,應該是積極正面的。
從另一個角度講,這是非常壞的習慣。程序在實現功能之后,最重要的一條是“可讀性”,這一條怎么強調都不過分。僅僅為了程序員“偷懶”,或者炫耀所謂的“技術性”,將Object-C和C混編,搞得不倫不類,是非常愚蠢可笑的做法,這跟“郭美美炫富”的效果沒什么兩樣。從語言的美感,靈活性,效率等角度來說,至今還沒有哪一個語言能超越C和C++。那么為什么其他的語言,比如Object-C能很好的發展呢?就是為了限制靈活性,獲得良好的“可讀性”。在舒服地用著Object-C的同時,嵌入底層的c代碼,這跟程序語言發展的大趨勢是相反的,這個除了“偷懶”和“炫技”以外,基本沒什么其他好處。這類人,在普通開發者來看,是“高手”;但是在真正的高手看來,只能是“半桶水”而已。
說了這么多,只是為了表明自己的觀點,iOS開發只是應用開發,專心用UIKit,WebKit,HeathKit,HomeKit....各種上層API實現具體業務就好了,runtime什么的就讓它在底層默默發揮作用好了。如果是為了體現所謂的“技術含量”,那么去做C和C++相關的開發,或者iOS中的“越獄”開發,將會是更好的選擇。

當前現狀

所謂形勢比人強,現在不用點runtime都不好意思說自己是iOS開發的,從“軟件工程”的角度講,將這些惡心的代碼限制在一定范圍之內,比如在某個framework中,也算是一個既能堅持自己想法,又不顯得過于落伍的折中方案。
當前的工程選擇的是AutoCoding這個第三方庫,以源文件方式放在shared_library文件夾中。這個庫只是實現了NSCoding和NSCopying協議,字典轉模型功能是通過NSObject的category來實現的,相當于手寫。這個庫在github上star也只有800多,跟Mantle(9581)、JSONModel(5361)、MJExtention(5519)、YYModel(1846)、FastEasyMapping(419)等相比感覺沒什么優勢,不知道當初選擇的理由是什么。
YYModel的作者寫一篇測試的軟文來講這個問題,感覺還是不錯的。基本上我也認同的他的觀點。Mantle、JSONModel用基類的方式,侵入性太強,MJExtention源文件稍微多了一點。YYModel文件少,各方面表現都不錯,跟手寫也差不了多少。所以最終要用的話,就選YYModel,以源文件的方式集成在自己的工程中。

考慮的方面

功能角度

  1. 字典轉模型

  2. 實現NSCoding協議,方便做序列化(緩存)

  3. 實現NSCopying協議,能用=,用copy方法

兼容性角度

  1. 類型嵌套

  2. 組數嵌套

  3. 映射,也就是模型的屬性名和字典的key名稱不一致

  4. 容錯,屬性的類型和字典value的類型不一致

  5. 速度

  6. 侵入性,是否需要繼承指定的基類

其他考慮

  1. framework是一個隔離帶,這個功能在framework外面實現還是在framework里面實現

  2. 這部分工作放framework外面由業務人員自己實現還是放在framework內部有框架人員實現?

  3. 如果在framework內部實現,如何處理內外部類型傳遞的問題。在Object-C中,class類型可以傳遞,但是class的實現無法在framework內部替外部代勞。

  4. 是否提供基類?

討論后的方案

  • 將網絡返回的response或者error原樣傳遞出去,讓framework外部的調用者有機會自己處理

  • 提供類型參數,能字典轉模型的就轉了吧

  • 提供自定義的基類作為接口,實現NSCoding和NSCopying協議,方便外部使用

  • 手寫實現這些功能比較麻煩,暫時不采用

  • 第三方庫采用YYModel,這樣就可以不對framework外部暴露第三方庫頭文件。Mantle雖然很強,對于速度的劣勢也可以忍受,但是必須繼承基類的使用方式在framework的場景下實在不合適。

現有代碼

現有代碼基本上是手寫實現了這些功能,跟第三方庫相比還有差距,但是也有足夠的學習意義。

NSCoding, NSCopying協議實現

#import <Foundation/Foundation.h>

@interface BaseDataModel : NSObject<NSCoding, NSCopying>

@end
#import "BaseDataModel.h"
#import <objc/runtime.h>

@implementation BaseDataModel

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    unsigned int outCount, i;
    objc_property_t *properties = class_copyPropertyList([self class], &outCount);
    for (i = 0; i < outCount; i++) {
        objc_property_t property = properties[i];
        NSString *key = [NSString stringWithFormat:@"%s",property_getName(property)];
        id value = [self valueForKey:key];
        if(!value)continue;
        [aCoder encodeObject:value forKey:[NSString stringWithFormat:@"%@",key]];
    }
    free (properties);
}

- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super init];
    if(self)
    {
        unsigned int outCount, i;
        objc_property_t *properties = class_copyPropertyList([self class], &outCount);
        for (i = 0; i < outCount; i++) {
            objc_property_t property = properties[i];
            NSString *key = [NSString stringWithFormat:@"%s",property_getName(property)];
            id value = [aDecoder decodeObjectForKey:key];
            if (!value)continue;
            [self setValue:value forKey:key];
        }
        free (properties);
    }
    return self;
}

- (id)copyWithZone:(NSZone *)zone
{
    id copyObject = [[[self class] allocWithZone:zone] init];
    unsigned int outCount, i;
    objc_property_t *properties = class_copyPropertyList([self class], &outCount);
    for (i = 0; i < outCount; i++) {
        objc_property_t property = properties[i];
        NSString *key = [NSString stringWithFormat:@"%s",property_getName(property)];
        id value = [self valueForKey:key];
        if (!value)continue;
        [copyObject setValue:value forKey:key];
    }
    free (properties);
    return copyObject;
}

@end

字典轉模型

這部分相對比較復雜,不一定能完全看懂,能了解核心原理就可以了。

+ (id)ac_objectsWithArray:(id)array objectClass:(__unsafe_unretained Class)clazz
{
    NSMutableArray * objects = [NSMutableArray array];
    
    for ( NSDictionary * obj in array )
    {
        if ( [obj isKindOfClass:[NSDictionary class]] )
        {
            id convertedObj = [clazz ac_objectWithDictionary:obj];
            if ( convertedObj ) {
                [objects addObject:convertedObj];
            }
        }
        else
        {
            [objects addObject:obj];
        }
    }
    
    return [objects copy];
}

+ (instancetype)ac_objectWithDictionary:(NSDictionary *)dictionary
{
    id object = [[self alloc] init];
    
    NSDictionary * properties = [object codableProperties];
    
    for ( __unsafe_unretained NSString *property in properties )
    {
        id value = dictionary[property];
        Class clazz = properties[property][@"class"];
        Class subClazz = properties[property][@"subclass"];
        
        if ( value )
        {
            id convertedValue = value;
            
            if ( [value isKindOfClass:[NSArray class]] )
            {
                if ( subClazz != NSNull.null ) {
                    convertedValue = [NSObject ac_objectsWithArray:value objectClass:subClazz];
                }
                // TODO: handle else
            }
            else if ( [value isKindOfClass:[NSDictionary class]] )
            {
                convertedValue = [clazz ac_objectWithDictionary:value];
            }
            
            if ( convertedValue && ![convertedValue isKindOfClass:[NSNull class]] )
            {
                if ( [self conformsToProtocol:@protocol(AutoModelCoding)] )
                {
                    convertedValue = [(id<AutoModelCoding>)self processedValueForKey:property originValue:value convertedValue:convertedValue class:clazz subClass:subClazz];
                }
                
                [object setValue:convertedValue forKey:property];
                
                if ( ![convertedValue isKindOfClass:clazz] )
                {
                    //                    @"Expected '%@' to be a %@, but was actually a %@"
                    NSLog( @"The type of '%@' in <%@> is <%@>, but not compatible with expected <%@>, please see detail in the <AutoModelCoding> protocol.", property, [self class], [value class], clazz );
                }
            }
        }
    }
    
    return object;
}

- (NSDictionary *)codableProperties
{
    __autoreleasing NSDictionary *codableProperties = objc_getAssociatedObject([self class], _cmd);
    if (!codableProperties)
    {
        codableProperties = [NSMutableDictionary dictionary];
        Class subclass = [self class];
        while (subclass != [NSObject class])
        {
            [(NSMutableDictionary *)codableProperties addEntriesFromDictionary:[subclass codableProperties]];
            subclass = [subclass superclass];
        }
        codableProperties = [NSDictionary dictionaryWithDictionary:codableProperties];
        
        //make the association atomically so that we don't need to bother with an @synchronize
        objc_setAssociatedObject([self class], _cmd, codableProperties, OBJC_ASSOCIATION_RETAIN);
    }

    return codableProperties;
}
@protocol AutoModelCoding <NSObject>
+ (id)processedValueForKey:(NSString *)key
               originValue:(id)originValue
            convertedValue:(id)convertedValue
                     class:(__unsafe_unretained Class)clazz
                  subClass:(__unsafe_unretained Class)subClazz;
@end

參考文檔

Mantle–國外程序員最常用的iOS模型&字典轉換框架
iOS JSON 模型轉換庫評測
Mantle
YYModel
MJExtension
AutoCoding

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

推薦閱讀更多精彩內容