MJExtension源码学习(一)

继续进行优秀开源框架的源码学习,这次打算学习一些常用的model解析的框架,比如YYModel,MJExtension,Mantle等。我自己用过YYModel和MJExtension,比较简单易用,看过别人用Mantle的代码,个人感觉稍微繁琐一些,所以这次就先学习MJExtension吧。

本次的学习我分为了两个过程:

  1. 初步了解MJExtension的原理,并通过第一版的代码学习基本的逻辑和思路。
  2. 阅读学习新版本的代码,加深对MJExtension的认识。

本文主要是记录第一个过程中的学习和心得。

MJExtension从最初到现在,也已经更新了几十个版本了。所以在开始之前,我们先查阅一些别的资料,从大概上来了解一下MJExtension的实现原理和一些学习的点。

参考文章

就拿最简单的json转model来说,我的个人观点,其实主要是运行时机制递归思想相结合来实现。通过运行时机制,我们可以获取到一个类的所有属性,然后通过遍历来对每一个属性进行赋值,如果该属性又是一个自定义的类,那就用到递归的思想这样一级级的解析下去,直到解析完成。数组也是一样的,只是多了一个对数组遍历的环节。

现在让我们具体来看MJExtension第一版本的代码(以下所提到MJExtension都是指的它的第一版,特殊情况会单独指出)

MJExtension中最主要的就是NSObject+MJKeyValue这个类,他通过分类的形式向我们提供了dict->model的方法,然后其中重要的方法- (instancetype)setKeyValues:(NSDictionary *)keyValues

赋值部分

下面是其中的代码:

- (instancetype)setKeyValues:(NSDictionary *)keyValues
{
    MJAssert2([keyValues isKindOfClass:[NSDictionary class]], self);
    
    [[self class] enumerateIvarsWithBlock:^(MJIvar *ivar, BOOL *stop) {
        // 1.取出属性值
        id value = keyValues ;
        for (NSString *key in ivar.keys) {
            value = value[key];
        }
        if (!value || value == [NSNull null]) return;
        
        // 2.如果是模型属性
        MJType *type = ivar.type;
        Class typeClass = type.typeClass;
        if (!type.isFromFoundation && typeClass) {
            value = [typeClass objectWithKeyValues:value];
        } else if (typeClass == [NSString class]) {
            if ([value isKindOfClass:[NSNumber class]]) {
                // NSNumber -> NSString
                value = [_numberFormatter stringFromNumber:value];
            } else if ([value isKindOfClass:[NSURL class]]) {
                // NSURL -> NSString
                value = [value absoluteString];
            }
        } else if ([value isKindOfClass:[NSString class]]) {
            if (typeClass == [NSNumber class]) {
                // NSString -> NSNumber
                value = [_numberFormatter numberFromString:value];
            } else if (typeClass == [NSURL class]) {
                // NSString -> NSURL
                value = [NSURL URLWithString:value];
            }
        } else if (ivar.objectClassInArray) {
            // 3.字典数组-->模型数组
            value = [ivar.objectClassInArray objectArrayWithKeyValuesArray:value];
        }
        
        // 4.赋值
        [ivar setValue:value forObject:self];
    }];
    
    // 转换完毕
    if ([self respondsToSelector:@selector(keyValuesDidFinishConvertingToObject)]) {
        [self keyValuesDidFinishConvertingToObject];
    }
    
    return self;
}

可以看的出这个方法里面的主要代码就是enumerateIvarsWithBlock 回调里面的代码,回调中返回了一个MJIvar的类,MJIvar其实就是对你的model类里面的每一个成员变量做的进一步的封装,封装后每一个成员变量对应对封装成一个MJIvar的实例。

MJIvar中的大多数字段都是起到了一个标识和记录的作用,比如说成员变量的名、属于哪个类等,其中还包含一个MJType类型的属性,其实也是做一些标识的作用,大家点进去看看就一目了然了。

做这一层的封装主要是为了之后的处理值时使用。

上述代码的中间部分大篇的if,else if的判断就是在做值的分类处理

MJType *type = ivar.type;
Class typeClass = type.typeClass;
if (!type.isFromFoundation && typeClass) {
   value = [typeClass objectWithKeyValues:value];
}

这第一个判断,就是用于如果model中的某个成员变量还是一个自定义类的情况,type中的isFromFoundation字段就是标识改成员变量的类是否是自定义的类,如果是自定义的类,把这个类存进type.typeClass下面。
value = [typeClass objectWithKeyValues:value];
这句代码也就是递归思想的提现,如果这个成员变量是一个自定义类的,那么该成员变量对应的值应该也是一个model,所以用这个二级的model类继续调用objectWithKeyValues方法,继续解析下去。

如果是数组,调用objectArrayWithKeyValuesArray这个方法,原理相同,只是多一层的遍历。

所有的解析,最后都调用了

// 4.赋值
[ivar setValue:value forObject:self];

这个是MJIvar中的方法

- (void)setValue:(id)value forObject:(id)object
{
    if (_type.KVCDisabled) return;
    [object setValue:value forKey:_propertyName];
}

这里_propertyName的也是MJIvar的一个字段,就是记录封装成MJIvar之前的这个成员变量的名字,然后使用
setValue:forKey为一个类的成员变量赋值。

获取类的成员变量,封装MJIvar

上面的是所有的成员变量已经封装成MJIvar之后,遍历所有的MJIvar并最终赋值的过程,还有一个方法也很重要,他实现了获取所有的成员变量并封装的这个过程的。下面看+ (void)enumerateIvarsWithBlock:(MJIvarsBlock)block这个方法

+ (void)enumerateIvarsWithBlock:(MJIvarsBlock)block
{
    static const char MJCachedIvarsKey;
    // 获得成员变量
    NSMutableArray *cachedIvars = objc_getAssociatedObject(self, &MJCachedIvarsKey);
    if (cachedIvars == nil) {
        cachedIvars = [NSMutableArray array];
        
        [self enumerateClassesWithBlock:^(__unsafe_unretained Class c, BOOL *stop) {
            // 1.获得所有的成员变量
            unsigned int outCount = 0;
            Ivar *ivars = class_copyIvarList(c, &outCount);
            
            // 2.遍历每一个成员变量
            for (unsigned int i = 0; i<outCount; i++) {
                MJIvar *ivar = [MJIvar cachedIvarWithIvar:ivars[i]];
                ivar.key = [self ivarKey:ivar.propertyName];
                // 如果有多级映射
                ivar.keys = [ivar.key componentsSeparatedByString:@"."];
                // 数组中的模型类
                ivar.objectClassInArray = [self ivarObjectClassInArray:ivar.propertyName];
                ivar.srcClass = c;
                [cachedIvars addObject:ivar];
            }
            
            // 3.释放内存
            free(ivars);
        }];
        objc_setAssociatedObject(self, &MJCachedIvarsKey, cachedIvars, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    // 遍历成员变量
    BOOL stop = NO;
    for (MJIvar *ivar in cachedIvars) {
        block(ivar, &stop);
        if (stop) break;
    }
}

大家可以看到,它主要的部分,又是一个遍历之后的回调, 我们那顺便贴出来这个遍历的代码

+ (void)enumerateClassesWithBlock:(MJClassesBlock)block
{
    // 1.没有block就直接返回
    if (block == nil) return;
    
    // 2.停止遍历的标记
    BOOL stop = NO;
    
    // 3.当前正在遍历的类
    Class c = self;
    
    // 4.开始遍历每一个类
    while (c && !stop) {
        // 4.1.执行操作
        block(c, &stop);
        
        // 4.2.获得父类
        c = class_getSuperclass(c);
        
        if ([MJFoundation isClassFromFoundation:c]) break;
    }
}

很容易看出来,这是为了处理那种父类也是自定义类的情况,那我们实际开发中来说,一般后台返回的数据都是有固定形式的,比如说status,message,code这种字段是每个接口都返回的,所以这些字段我一般都写在一个父类里面,然后这个循环就是遍历出父类,为从父类中继承的字段赋值。

说回上一个遍历方法,其实就是一个封装过程,从代码中可以看出来,使用了一些runtime
的api来获取了类的成员变量,然后通过循环对每个变量进行了封装。这一步的话,光这样干很难体验什么,大家可以跑一个简单的例子,然后debug跟一下,看看MJIvar中每个属性代表什么。

映射问题

有些情况可能要映射字段名,比如id属于关键字,可能公司要求model中不允许用id作为成员变量名, 所以要做映射处理,还有如果数组中如果包含别的model,这个组数中的model类名我们也应该告诉MJExtension。

MJExtension都抛出了方法,需要映射名称使用replacedKeyFromPropertyName,数组包含模型使用objectClassInArray,我们根据自己的需要的重写相应方法。

举个例子:

成员变量属于是数组的情况下, 这个数组里面包含model类型要存在ivar.objectClassInArray下面

// 数组中的模型类
ivar.objectClassInArray = [self ivarObjectClassInArray:ivar.propertyName];

这是ivarObjectClassInArray的实现

+ (Class)ivarObjectClassInArray:(NSString *)propertyName
{
    if ([self respondsToSelector:@selector(objectClassInArray)]) {
        return self.objectClassInArray[propertyName];
    } else {
        // 为了兼容以前的对象方法
        id tempObject = self.tempObject;
        if ([tempObject respondsToSelector:@selector(objectClassInArray)]) {
            id dict = [tempObject objectClassInArray];
            return dict[propertyName];
        }
        return nil;
    }
    return nil;
}

在赋值的时候会先判断是否respondsToSelector,然后根据情况赋值。

第一版的代码比较简单,我就简单的dict->model说了一下,model->dict大家可以自己再去看看,当然其他还有一些细节处理的东西, 大家也可以通过代码来进一步学习。

对MJExtension学习的第一步就先到这,慢慢我会继续看它的新的代码,学习他的一些优化和封装。

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

推荐阅读更多精彩内容

  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,242评论 8 265
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,065评论 1 32
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,493评论 18 399
  • 一直以为能够读懂源代码是件很牛的事情,但是每次都被动辄复杂的语法的架构吓跑,在偶然看到一个叫Draveness的大...
    VoyageCN阅读 931评论 1 2
  • 第一章听和讲 我想问题大都在于发音;你不知道正确的发音,或者虽然知道而你的耳却不行。 听:“听不懂”→正确的发音 ...
    milaji阅读 616评论 0 0