MJExtension框架源码分析

百家 作者:iOS开发 2017-07-23 14:59:26

点击上方“iOS开发”,选择“置顶公众号”

关键时刻,第一时间送达!

iOS开发中经常会用到数据和模型的互相转换,大致有两种转换方式:1.手动写转换的代码,2.利用开源库进行转换。常用的开源库有:JSONModel、Mantle、MJExtension、YYModel等等,本文主要介绍一下MJExtension的底层实现,看一看小码哥如何设计这个轻量级的数模转换框架。


本着面向应用的角度,我觉得还是从一个字典转模型的例子入手,来详细介绍一下MJExtension的转换过程。


待转换的字典对象:


    NSDictionary *dict = @{

        @"name": @"kelvin",

        @"age": @18,

        @"married": @"false",

        @"computer": {

            @"type": @"AUSU",

            @"date": @"2012-01"

        },

        @"skills": @[

            @{

                @"type": @"C language"

                @"degree": @"proficient"

            },

            @{

                @"type": @"Python language",

                @"degree": @"grasp"

            }

        ],

    };


转换流程


1.实现转换的代码


MJExtension的转换十分方便,下面一行代码就可以搞定,当然在转换之前,要设置字典中数组对应的对象模型,因为在转换过程中,需要根据模型类来创建模型数组。


    // Person.m

    + (NSDictionary*)mj_objectClassInArray

    {

        return @{@"skills": @"Skill"};

    }


    // 使用过程的转换代码

    Person *p = [Person mj_objectWithKeyValues:dict];


2.具体的转换过程


在不考虑CoreData的情况下,我们直接来看转换的核心代码的方法,主要就是mj_setKeyValues:context:方法来完成字典转模型的工作


/**

 核心代码:

 */

- (instancetype)mj_setKeyValues:(id)keyValues context:(NSManagedObjectContext *)context


把传进来的数据转换为字典


可以看到,传进来的数据还可以是NSString 和 NSData类型,最后都被转换为JSON格式的数据


    // NSObject+MJKeyValue.m

    // 获得JSON对象

    keyValues = [keyValues mj_JSONObject];

    

--->


    if ([self isKindOfClass:[NSString class]]) {

        return [NSJSONSerialization JSONObjectWithData:[((NSString *)self) dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil];

    } else if ([self isKindOfClass:[NSData class]]) {

        return [NSJSONSerialization JSONObjectWithData:(NSData *)self options:kNilOptions error:nil];

    }

    

    // 返回字典

    return self.mj_keyValues;


获取转换的白名单和黑名单


首先检查名单是不是存在,如果存在,就直接返回;不存在,则根据方法mj_totalAllowedPropertyNames来获取名单,然后返回


    // NSObject+MJKeyValue.m

    Class clazz = [self class];

    NSArray *allowedPropertyNames = [clazz mj_totalAllowedPropertyNames];

    NSArray *ignoredPropertyNames = [clazz mj_totalIgnoredPropertyNames];


--->

    // NSObject+MJClass.m

    + (NSMutableArray *)mj_totalObjectsWithSelector:(SEL)selector key:(const char *)key

    {

        NSMutableArray *array = [self dictForKey:key][NSStringFromClass(self)];

        if (array) return array;

        

        // 创建、存储

        [self dictForKey:key][NSStringFromClass(self)] = array = [NSMutableArray array];

        

        if ([self respondsToSelector:selector]) {

    #pragma clang diagnostic push

    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"

            NSArray *subArray = [self performSelector:selector];

    #pragma clang diagnostic pop

            if (subArray) {

                [array addObjectsFromArray:subArray];

            }

        }

        

        [self mj_enumerateAllClasses:^(__unsafe_unretained Class c, BOOL *stop) {

            NSArray *subArray = objc_getAssociatedObject(c, key);

            [array addObjectsFromArray:subArray];

        }];

        return array;

    }


根据上述的名单,来判断属性是否需要被忽略


    // 检测是否被忽略

    if (allowedPropertyNames.count && ![allowedPropertyNames containsObject:property.name]) return;

    if ([ignoredPropertyNames containsObject:property.name]) return;


遍历成员变量


+ (void)mj_enumerateProperties:(MJPropertiesEnumeration)enumeration遍历所有的属性。属性会先从缓存cachedProperties中取,如果存在,直接返回;如果不存在,就遍历类来获取属性列表,创建MJProperty对象,并设置它的name(属性名)、type(数据类型)、srcClass(来源类)等等;同时,还会设置propertyKeysDict(类对应的字典中的所有key) 和 objectClassInArrayDict(类对应的字典数组中的模型类)。


    // NSObject+MJProperty.m

    // 获得成员变量

    NSArray *cachedProperties = [self properties];

    

    // 遍历成员变量

    BOOL stop = NO;

    for (MJProperty *property in cachedProperties) {

        enumeration(property, &stop);

        if (stop) break;

    }

    

---->


    + (NSMutableArray *)properties

    {

        NSMutableArray *cachedProperties = [self dictForKey:&MJCachedPropertiesKey][NSStringFromClass(self)];

        

        if (cachedProperties == nil) {

            cachedProperties = [NSMutableArray array];

            

            [self mj_enumerateClasses:^(__unsafe_unretained Class c, BOOL *stop) {

                // 1.获得所有的成员变量

                unsigned int outCount = 0;

                objc_property_t *properties = class_copyPropertyList(c, &outCount);

                

                // 2.遍历每一个成员变量

                for (unsigned int i = 0; i

                    MJProperty *property = [MJProperty cachedPropertyWithProperty:properties[i]];

                    

                    NSLog(@"%@ - %@", property.name, property.srcClass);

                    // 过滤掉Foundation框架类里面的属性

                    if ([MJFoundation isClassFromFoundation:property.srcClass]) continue;

                    property.srcClass = c;

                    [property setOriginKey:[self propertyKey:property.name] forClass:self];

                    [property setObjectClassInArray:[self propertyObjectClassInArray:property.name] forClass:self];

                    [cachedProperties addObject:property];

                }

                

                // 3.释放内存

                free(properties);

            }];

            

            [self dictForKey:&MJCachedPropertiesKey][NSStringFromClass(self)] = cachedProperties;

        }

        

        return cachedProperties;

    }    


在上述设置模型属性时,会调用mj_replacedKeyFromPropertyName查看是否需要替换为新的键值。通常,特殊字符如:id、description不能作为对象属性名,所以如果确实需要该属性名时,可以用ID、desc等代替,然后就需要实现该方法做替换,使对象的ID属性名对应字典中的id键。


    [property setOriginKey:[self propertyKey:property.name] forClass:self];

    

---->

    

    // NSObject+MJProperty.m

    + (id)propertyKey:(NSString *)propertyName

    {

        ...

        // 查看有没有需要替换的key

        if ((!key || [key isEqual:propertyName]) && [self respondsToSelector:@selector(mj_replacedKeyFromPropertyName)]) {

            key = [self mj_replacedKeyFromPropertyName][propertyName];

        }

        ...

    }


取出字典中的属性值


根据变量名获取字典中对应的值


    // 取出属性值

    id value;

    NSArray *propertyKeyses = [property propertyKeysForClass:clazz];

    for (NSArray *propertyKeys in propertyKeyses) {

        value = keyValues;

        for (MJPropertyKey *propertyKey in propertyKeys) {

            value = [propertyKey valueInObject:value];

        }

        if (value) break;

    }


过滤值


如果用户需要对取得的字典中的值进行特殊处理,则需要实现mj_newValueFromOldValue:property:方法,并返回对应key的新值。如修改日期格式等等


    // NSObject+MJKeyValue.m

    // 值的过滤

    id newValue = [clazz mj_getNewValueFromObject:self oldValue:value property:property];

    if (newValue != value) { // 有过滤后的新值

        [property setValue:newValue forObject:self];

        return;

    }

    

---->


    // NSObject+MJProperty.m

    // 如果有实现方法

    if ([object respondsToSelector:@selector(mj_newValueFromOldValue:property:)]) {

        return [object mj_newValueFromOldValue:oldValue property:property];

    }

    // 兼容旧版本

    if ([self respondsToSelector:@selector(newValueFromOldValue:property:)]) {

        return [self performSelector:@selector(newValueFromOldValue:property:)  withObject:oldValue  withObject:property];

    }

    

    // 查看静态设置

    __block id newValue = oldValue;

    [self mj_enumerateAllClasses:^(__unsafe_unretained Class c, BOOL *stop) {

        MJNewValueFromOldValue block = objc_getAssociatedObject(c, &MJNewValueFromOldValueKey);

        if (block) {

            newValue = block(object, oldValue, property);

            *stop = YES;

        }

    }];

    return newValue;    


获取变量的类型


根据property属性获取属性的类型(基本数据类型则为nil)和数组中的成员对象类型(如果属性不是数组,则为空对象类型)


    // 复杂处理

    MJPropertyType *type = property.type;

    Class propertyClass = type.typeClass;

    Class objectClass = [property objectClassInArrayForClass:[self class]];


转换为可变类型


如果对象的属性是可变类型,但是从字典中取出的值都是不可变类型,那么需要把取出的值转换为可变类型


    // 不可变 -> 可变处理

    if (propertyClass == [NSMutableArray class] && [value isKindOfClass:[NSArray class]]) {

        value = [NSMutableArray arrayWithArray:value];

    } else if (propertyClass == [NSMutableDictionary class] && [value isKindOfClass:[NSDictionary class]]) {

        value = [NSMutableDictionary dictionaryWithDictionary:value];

    } else if (propertyClass == [NSMutableString class] && [value isKindOfClass:[NSString class]]) {

        value = [NSMutableString stringWithString:value];

    } else if (propertyClass == [NSMutableData class] && [value isKindOfClass:[NSData class]]) {

        value = [NSMutableData dataWithData:value];

    }


模型属性


这里用到了是不是Foundation的判断,如果属性是自定义属性,就递归调用mj_objectWithKeyValues再次进行赋值


    if (!type.isFromFoundation && propertyClass) { // 模型属性

        value = [propertyClass mj_objectWithKeyValues:value context:context];

    }


数组属性


如果是NSURL的对象类型,就获取它的url字符串,然后组合成数组;如果是其它对象类型,就调用mj_objectArrayWithKeyValuesArray::转换为模型数组


    if (objectClass) {

        if (objectClass == [NSURL class] && [value isKindOfClass:[NSArray class]]) {

            // string array -> url array

            NSMutableArray *urlArray = [NSMutableArray array];

            for (NSString *string in value) {

                if (![string isKindOfClass:[NSString class]]) continue;

                [urlArray addObject:string.mj_url];

            }

            value = urlArray;

        } else { // 字典数组-->模型数组

            value = [objectClass mj_objectArrayWithKeyValuesArray:value context:context];

        }

    }


处理模型属性和数组属性之外的其它属性


如果属性的类型和字典中对应的值的类型不匹配,就进行转换,如:NSNumber -> NSString,NSURL -> NSString,NSString -> NSURL, NSString -> NSURL。另外,针对属性是布尔类型,而值是字符串:yes/true、no/false(忽略大小写情况),则转换为YES、NO的包装类型。最后,如果属性和值的类型依然不匹配,那么就把值设为空,给后面KVC使用


    if (propertyClass == [NSString class]) {

        if ([value isKindOfClass:[NSNumber class]]) {

            // NSNumber -> NSString

            value = [value description];

        } else if ([value isKindOfClass:[NSURL class]]) {

            // NSURL -> NSString

            value = [value absoluteString];

        }

    } else if ([value isKindOfClass:[NSString class]]) {

        if (propertyClass == [NSURL class]) {

            // NSString -> NSURL

            // 字符串转码

            value = [value mj_url];

        } else if (type.isNumberType) {

            NSString *oldValue = value;

            

            // NSString -> NSNumber

            if (type.typeClass == [NSDecimalNumber class]) {

                value = [NSDecimalNumber decimalNumberWithString:oldValue];

            } else {

                value = [numberFormatter_ numberFromString:oldValue];

            }

            

            // 如果是BOOL

            if (type.isBoolType) {

                // 字符串转BOOL(字符串没有charValue方法)

                // 系统会调用字符串的charValue转为BOOL类型

                NSString *lower = [oldValue lowercaseString];

                if ([lower isEqualToString:@"yes"] || [lower isEqualToString:@"true"]) {

                    value = @YES;

                } else if ([lower isEqualToString:@"no"] || [lower isEqualToString:@"false"]) {

                    value = @NO;

                }

            }

        }

    }

    

    // value和property类型不匹配

    if (propertyClass && ![value isKindOfClass:propertyClass]) {

        value = nil;

    }


赋值


  • 利用KVC赋值


    [property setValue:value forObject:self];


  • 归档


MJExtensionCodingImplementation这个宏表明对象支持归档,通过分类支持NSCoding协议,实现归档和解档的方法,我们可以直接把对象归档和解档,十分方便


  • 宏定义函数


这个宏可以构建错误日志并输出,方便调试。同时,对于宏替换不是十分了解的时候,可以使用预处理命令,把文件的宏全部替换后,就可以十分方便的看到宏被替换后的代码模样:


# clang -E 源文件 [-o 目标文件]


    // 判断参数是不是字典

    MJExtensionAssertError([keyValues isKindOfClass:[NSDictionary class]], self, [self class], @"keyValues参数不是一个字典");


---->

    // MJExtensionConst.h

    #define MJExtensionAssertError(condition, returnValue, clazz, msg)

    [clazz setMj_error:nil];

    if ((condition) == NO) {

        MJExtensionBuildError(clazz, msg);

        return returnValue;

    }


MJExtension的分析到此结束,有不足之处,欢迎斧正。


参考资料


MJExtension源码解析

MJExtension源码阅读

跟着MJExtension实现简单的字典转模型框架


  • 址:https://mp.weixin.qq.com/s/kNJq2U1jrNCJ-k0h1KkySA

  • iOS开发整理发布,转载请联系作者授权

↙点击“阅读原文”,加入 

『程序员大咖』

关注公众号:拾黑(shiheibook)了解更多

[广告]赞助链接:

四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接