iOS | MJExtension 介绍 & 源码解析

介绍:

MJExtension是一个转换速度快、使用简单方便的字典转模型框架, 由国内李明杰大神所著, 并且开源, 目前国内很多 iOS 项目都有使用该框架.
github地址:https://github.com/CoderMJLee/MJExtension

功能:

MJExtension是一套字典和模型之间互相转换的超轻量级框架,提供了以下功能:

  • JSON --> ModelCore Data Model
  • JSONString --> ModelCore Data Model
  • ModelCore Data Model --> JSON
  • JSON Array --> Model ArrayCore Data Model Array
  • JSONString --> Model ArrayCore Data Model Array
  • Model ArrayCore Data Model Array --> JSON Array
  • 只需要一行代码,就能实现模型的所有属性进行Coding(归档和解档)

安装:

使用CocoaPods

pod 'MJExtension'

Examples【示例】:

// 字典转模型
@interface User : NSObject
@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *icon;
@property (assign, nonatomic) unsigned int age;
@property (copy, nonatomic) NSString *height;
@property (strong, nonatomic) NSNumber *money;
@property (assign, nonatomic) BOOL sex;
@end

/***********************************************/

NSDictionary *dict = @{
    @"name" : @"Jack",
    @"icon" : @"lufy.png",
    @"age" : @20,
    @"height" : @"1.55",
    @"money" : @100.9,
    @"sex" : @1,
};

// JSON -> User
User *user = [User mj_objectWithKeyValues:dict];

源码分析:

MJExtension一个丰富的字典转模型库,提供了字典/JSON转模型,JSON转CoreData模型, 以及模型数据转字典,转JSON,转数组,以及模型数据的归档与解档操作.

本文主要分析 字典转模型的具体流程以及源码:

1. 入口方法:

+ (instancetype)mj_objectWithKeyValues:(id)keyValues: 作为最基本的入口,在调用了该方法之后, 内部会调用 + (instancetype)mj_objectWithKeyValues:(id)keyValues context:(NSManagedObjectContext *)context方法, 如果传了 context,则字典会转为 CoreData模型,如果不传入 context,则会进行普通的字典转模型操作,源码如下:

/**
 *  通过字典来创建一个模型
 *  @param keyValues 字典(可以是NSDictionary、NSData、NSString)
 *  @return 新建的对象
 */
+ (instancetype)mj_objectWithKeyValues:(id)keyValues{
    return [self mj_objectWithKeyValues:keyValues context:nil];
}

+ (instancetype)mj_objectWithKeyValues:(id)keyValues context:(NSManagedObjectContext *)context{
    // 获得JSON对象(将 JSON字符串或 NSData 转为 字典)
    keyValues = [keyValues mj_JSONObject];
    
    // 断言,如果不是字典类型,则直接返回 nil
    MJExtensionAssertError([keyValues isKindOfClass:[NSDictionary class]], nil, [self class], @"keyValues参数不是一个字典");
    
    // 判断是否为需要转为 CoreData 模型
    if ([self isSubclassOfClass:[NSManagedObject class]] && context) {
        NSString *entityName = [NSStringFromClass(self) componentsSeparatedByString:@"."].lastObject;
        return [[NSEntityDescription insertNewObjectForEntityForName:entityName inManagedObjectContext:context] mj_setKeyValues:keyValues context:context];
    }
    // 创建模型对象并进行赋值操作
    return [[[self alloc] init] mj_setKeyValues:keyValues];
}

上述代码可以看到: mj_JSONObject 方法主要用于将JSON字符串或NSData数据转为字典,MJExtensionAssertError断言则用于判断是否为字典对象,如果不是一个字典,则直接返回 nil, 接下来判断是否转CoreData 模型, 如果不是转CoreData 模型,则进行 字典转模型操作;

1.1 转JSON对象

每次字典转模型前, 都会通过mj_JSONObject方法,将JSON字符串或者NSData 数据转为字典.也可以过滤掉一些非JSON的错误数据, mj_JSONObject 源码如下:

// 将NSString字符串或者NSData转为字典对象
- (id)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;
}
1.2 错误处理断言:

MJExtensionAssertError如果数据不是字典类型或者是其他类型的,断言则会进行处理,直接返回 nil, MJExtensionAssertError定义如下:

/**
 * 断言-如果条件不为真,则直接返回 nil
 * @param condition   条件
 * @param returnValue 返回值
 */
#define MJExtensionAssertError(condition, returnValue, clazz, msg) \
[clazz setMj_error:nil]; \
if ((condition) == NO) { \
    MJExtensionBuildError(clazz, msg); \
    return returnValue;\
}


2. 字典转模型

通过mj_setKeyValues:方法可以进行字典转模型操作, 他会调用 mj_setKeyValues: context:方法, 在 mj_setKeyValues: context:方法内部,主要会进行以下操作

  1. 白名单以及黑名单属性过滤数据
    • 如果设置有白名单,则只有白名单数组中的属性名才允许进行字典和模型的转换,
    • 黑名单则相反,黑名单数组中的属性名将会被忽略, 不进行字典和模型的转换
  2. 获取当前对象所有的属性信息,包括父类的
    • 通过mj_enumerateProperties方法,来获取对象所有属性信息,其内部还会多次调用其他方法,用于查询,基本都是通过 block 回调方式进行传值的.
    • 通过+ (NSMutableArray *)mj_properties方法用于获取一个类的所有属性信息
    • 通过mj_enumerateClasses来遍历对象所属类以及其父类
  3. 取出每个属性的值(对应字典的 value值)
  4. 对属性的值进行复杂的操作
    • 判断属性是否为可变或者不可变,生成可变或者不可变的值
    • 如果属性是一个自定义对象,则会继续调用mj_objectWithKeyValues进行字典转模型操作
    • 如果属性是一个数组模型,则会调用mj_objectArrayWithKeyValuesArray进行字典数组转模型数组
    • 其他类型处理(NSURL, NSNumber, Bool)等类型的特殊处理...
  5. 通过KVC进行属性赋值操作.
2.1 核心代码

字典转模型的核心代码mj_setKeyValues:如下:


/**
   字典 -> 模型
 */
- (instancetype)mj_setKeyValues:(id)keyValues{
    return [self mj_setKeyValues:keyValues context:nil];
}

/**
    核心代码:
 */
- (instancetype)mj_setKeyValues:(id)keyValues context:(NSManagedObjectContext *)context{
    // 获得JSON对象---(将 JSON字符串或 NSData 转为 字典)
    keyValues = [keyValues mj_JSONObject];
    
     // 断言,如果不是字典类型,则直接返回 nil
    MJExtensionAssertError([keyValues isKindOfClass:[NSDictionary class]], self, [self class], @"keyValues参数不是一个字典");
    
    // 或者当前对象的类对象
    Class clazz = [self class];
    
    // 只有这个数组中的属性名才允许进行字典和模型的转换
    NSArray *allowedPropertyNames = [clazz mj_totalAllowedPropertyNames];
    
    // 这个数组中的属性名将会被忽略:不进行字典和模型的转换
    NSArray *ignoredPropertyNames = [clazz mj_totalIgnoredPropertyNames];
    
    // 通过封装的方法回调一个通过运行时编写的,用于返回属性列表的方法。(此 block 块会被多次回调,因为有多个属性)
    [clazz mj_enumerateProperties:^(MJProperty *property, BOOL *stop) {
        @try {
            // 0.检测是否被忽略
            if (allowedPropertyNames.count && ![allowedPropertyNames containsObject:property.name]) return;
            if ([ignoredPropertyNames containsObject:property.name]) return;
            
            // 1.取出属性值
            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;
            }
            
            // 值的过滤
            id newValue = [clazz mj_getNewValueFromObject:self oldValue:value property:property];
            if (newValue != value) { // 有过滤后的新值
                [property setValue:newValue forObject:self];
                return;
            }
            
            // 如果没有值,就直接返回
            if (!value || value == [NSNull null]) return;
            
            // 2.复杂处理
            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];
            }
            
            if (!type.isFromFoundation && propertyClass) { // 模型属性
                value = [propertyClass mj_objectWithKeyValues:value context:context];
            } else 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];
                }
            } else {
                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;
                            }
                        }
                    }
                } else if ([value isKindOfClass:[NSNumber class]] && propertyClass == [NSDecimalNumber class]){
                    // 过滤 NSDecimalNumber类型
                    if (![value isKindOfClass:[NSDecimalNumber class]]) {
                        value = [NSDecimalNumber decimalNumberWithDecimal:[((NSNumber *)value) decimalValue]];
                    }
                }
                
                // value和property类型不匹配
                if (propertyClass && ![value isKindOfClass:propertyClass]) {
                    value = nil;
                }
            }
            
            // 3.赋值
            [property setValue:value forObject:self];
        } @catch (NSException *exception) {
            MJExtensionBuildError([self class], exception.reason);
            MJExtensionLog(@"%@", exception);
        }
    }];
    
    // 转换完毕
    if ([self respondsToSelector:@selector(mj_didConvertToObjectWithKeyValues:)]) {
        [self mj_didConvertToObjectWithKeyValues:keyValues];
    }
....
    return self;
}


2.2 遍历所有的成员进行回调

mj_enumerateProperties方法用于查询对象所有的成员变量.然后通过enumeration进行逐一回调操作,在回调函数中进行赋值操作, 源码如下:

// 遍历所有的成员
+ (void)mj_enumerateProperties:(MJPropertiesEnumeration)enumeration{
    // 创建信号量
    MJExtensionSemaphoreCreate
    // 通过信号量进行加锁
    MJExtensionSemaphoreWait
    // 获得该对象成员变量属性列表
    NSArray *cachedProperties = [self mj_properties];
    // 信号量进行解锁
    MJExtensionSemaphoreSignal
    // 遍历成员变量
    BOOL stop = NO;
    for (MJProperty *property in cachedProperties) {
        // 回调给上一级函数进行操作
        enumeration(property, &stop);
        if (stop) break;
    }
}


2.3 获取对象属性列表信息(包括父类的成员变量)

mj_properties 方法用于获取获取对象属性列表信息包括父类的成员变量,将对象继承链上的所有属性都查询到,装进一个数组,返回给调用者,代码如下:

// 获取对象属性列表信息
+ (NSMutableArray *)mj_properties{
    // 优先查看是否有缓存
    NSMutableArray *cachedProperties = [self mj_propertyDictForKey:&MJCachedPropertiesKey][NSStringFromClass(self)];
    if (cachedProperties == nil) {
        if (cachedProperties == nil) {
  
            // 创建一个空的可变数组,保存成员变量信息
            cachedProperties = [NSMutableArray array];
            
            // 遍历所有的类(当前类以及父类)
            [self mj_enumerateClasses:^(__unsafe_unretained Class c, BOOL *stop) {
                
                // 1.获得所有的成员变量
                unsigned int outCount = 0;
                // 通过runtime 的 class_copyPropertyList获取所有成员变量,objc_property_t 是一个结构体,是成员变量的底层结构
                objc_property_t *properties = class_copyPropertyList(c, &outCount);
                
                // 2.遍历每一个成员变量
                for (unsigned int i = 0; i<outCount; i++) {
                    // 将 objc_property_t类型 包装成一个 MJProperty 对象
                    MJProperty *property = [MJProperty cachedPropertyWithProperty:properties[i]];
                    // 过滤掉Foundation框架类里面的属性
                    if ([MJFoundation isClassFromFoundation:property.srcClass]) continue;
                    // 过滤掉`hash`, `superclass`, `description`, `debugDescription`
                    if ([MJFoundation isFromNSObjectProtocolProperty:property.name]) continue;
                    
                    property.srcClass = c;
                    /**** 同一个成员属性 - 父类和子类的行为可能不一致(originKey、propertyKeys、objectClassInArray) ****/
                    /** 设置最原始的key */
                    [property setOriginKey:[self mj_propertyKey:property.name] forClass:self];
                    [property setObjectClassInArray:[self mj_propertyObjectClassInArray:property.name] forClass:self];
                    [cachedProperties addObject:property];
                }
                
                // 3.释放内存
                free(properties);
            }];
            
            [self mj_propertyDictForKey:&MJCachedPropertiesKey][NSStringFromClass(self)] = cachedProperties;
        }
    }
    return cachedProperties;
}


2.4 遍历所有的类(直到NSObject 为止)

mj_enumerateClasses方法会遍历当前类所属所有类, 从当前类开始,一级一级往上遍历, 直到遇到基类NSObject为止,代码如下:

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


3. 总结

MJExtension 字典转模型的大概流程总结:

通过 rumtime 获取对象的所有成员变量信息, 通过KVC进行赋值操作, 达到了将字典数据映射到了模型对象上;

不过 MJExtension内部做也了大量容错以及优化操作, 同时还提供了对象转字典,对象转JSON,以及归档等等操作.使得我们开发中,通过一句简单代码就可以做到复杂的操作;


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

推荐阅读更多精彩内容