字典转模型

背景介绍

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

推荐阅读更多精彩内容