MJExtension底层实现原理简单剖析

最简单的字典

首先,从最简单的字典开始.

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

目标是拿到字典里的值(value)User模型进行赋值.模型的属性名对应字典的键(key).

typedef enum {
    SexMale,
    SexFemale
} Sex;

@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) Sex sex;
/** 同性恋 */
@property (assign, nonatomic, getter=isGay) BOOL gay;

@end

最直接的方法是:

 User *user = [[User alloc] init];
 user.name = dict[@"name"];
 user.icon = dict[@"icon"];
 ....

假如属性数量一多,人工手写大量样板代码将耗费大量时间和精力,毫无意义.

如果要写一个框架自动帮我们转模型出来,大致思路如下:

1.遍历模型中的属性,然后拿到属性名作为键值去字典中寻找.

2.找到后根据模型的属性类型转成正确的类型

3.赋值


首先进行第一步:

遍历模型中的属性,然后拿到属性名作为键值去字典中寻找.

方法伪代码:

[模型类 遍历属性的方法];

为了方便使用,创建一个叫NSObject+Property的分类.写一个获取所有属性的方法.

@interface NSObject (Property)
+ (NSArray *)properties;
@end

假设我们看不见一个类的.h.m,有什么办法可以获取它所有的实例变量呢?答案是通过运行时机制.当在实现+ (NSArray *)properties方法时,需要导入运行时库.然后使用库中的API提供的函数得到一个类的方法列表.

注:在旧版本的MJExtension中,获取成员变量是通过class_copyIvarList来获取的类的所有实例变量,根据MJ源码中的说明:"在 swift 中,由于语法结构的变化,使用 Ivar 非常不稳定,经常会崩溃!",所以改用了获取成员属性的方法.

另外,不管是获取成员属性还是实例变量,都不能获取到父类的列表.(本人忽略了对父类成员属性的获取,后期更新中会更新这一失误).

// Any instance variables declared by superclasses are not included.
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)

返回的是叫objc_property_t的一个结构体指针,并且通过传入值引用能够得到属性的个数.

#import "NSObject+Property.h"
#import <objc/runtime.h>

@implementation NSObject (Property)
+ (NSArray *)properties{
    NSArray *propertiesArray = [NSMutableArray array];
      // 1.获得所有的属性
    unsigned int outCount = 0;
    objc_property_t *properties = class_copyPropertyList(self, &outCount);
      // .....
    return propertiesArray;
}
@end

来到这里已经获取到了属性列表,那么objc_property_t指向的结构体内部是怎样的呢.通过搜寻<objc/runtime.h>头文件并看不到objc_property_t的定义的.但好在runtime开源,我们搜寻到了相关的定义.

typedef struct property_t *objc_property_t;

struct property_t {
    const char *name;
    const char *attributes;
};

由于知道了结构体的内部构造,就可以获取内部的成员变量.例如以下方法:

typedef struct property_t {
    const char *name;
    const char *attributes;
} *propertyStruct;

@implementation NSObject (Property)

+ (NSArray *)properties{
    NSArray *propertiesArray = [NSMutableArray array];
    // 1.获得所有的属性
    unsigned int outCount = 0;
    objc_property_t *properties = class_copyPropertyList(self, &outCount);

    for (int i = 0; i < outCount; i++) {
        objc_property_t property = properties[i];
        NSLog(@"name:%s---attributes:%s",((propertyStruct)property)->name,((propertyStruct)property)->attributes);
    }

    return propertiesArray;
}
@end

在外部调用+ (NSArray *)properties方法能够打印出一个类的全部属性,如:

NSArray *propertyArray = [User properties];

得到控制台输出:


从输出中可以看到该结构体的name成员表示成员属性的名字,attributes表示成员属性中的一些特性(如是什么类,原子性还是非原子性,是strong还是weak还是copy,生成的成员变量名等信息)...

从苹果的官方文档(Objective-C Runtime Programming Guide)可以得知,attributes是一个类型编码字符串.可以使用property_getAttributes函数获得这个类型编码字符串.这个字符串以T作为开始,接上@encode类型编码和一个逗号,以V接上实例变量名作为结尾,在它们之间是一些其他信息,以逗号分割.具体内容可以看官方文档中详细的表格.

在实际赋值过程中,我们并不用关心该属性的内存管理语义,生成的成员变量名,或者其他什么信息.在attributes中,只需要知道它所属的或者是什么基本数据类型,即T第一个逗号之前中间的内容,如果是的话还需要将@""去掉.

实际上,框架提供的运行时库已经给我们提供获取属性名属性特性的函数了.通过下面方式也能打印出相同结果.

NSLog(@"name:%s---attributes:%s",property_getName(property),
                                   property_getAttributes(property));

runtime源码中可以看到这两个函数的内部是这样实现的:

const char *property_getName(objc_property_t prop)
{
    return prop->name;
}

const char *property_getAttributes(objc_property_t prop)
{
    return prop->attributes;
}

再回顾前面说的思路,这时会更清晰:

1.拿到模型的属性名(注意属性名和成员变量名的区别),和对应的数据类型.

2.用该属性名作为键去字典中寻找对应的值.

3.拿到值后将值转换为属性对应的数据类型.

4.赋值.

现在已经进行到第一步,并且拿到了属性名,但是数据类型还要进一步截取,截取方法如下:

for (int i = 0; i < outCount; i++) {
        objc_property_t property = properties[i];

          // 为了以后方便,将C字符串转换成OC对象
        NSString *name = @(property_getName(property));
        NSString *attributes = @(property_getAttributes(property));

        NSUInteger loc = 1;
        NSUInteger len = [attributes rangeOfString:@","].location - loc;
        NSString *type = [attributes substringWithRange:NSMakeRange(loc, len)];
        NSLog(@"%@",type);
    }

控制台结果显示我们能够截取到其中的类型了.


该部分源码请看项目实例代码中的<打印类型>


回归我们拿到这些数据类型的初衷,是为了是用字典中的值的类型与模型中属性的类型进行对比,想要对比,需要拿到属性的类型,因此需要将这些编码转换成一个表示类型的类,创建一个类用来包装类型.

/**
 *  包装一种类型
 */
@interface MJPropertyType : NSObject

/** 是否为id类型 */
@property (nonatomic, readonly, getter=isIdType) BOOL idType;

/** 是否为基本数字类型:int、float等 */
@property (nonatomic, readonly, getter=isNumberType) BOOL numberType;

/** 是否为BOOL类型 */
@property (nonatomic, readonly, getter=isBoolType) BOOL boolType;

/** 对象类型(如果是基本数据类型,此值为nil) */
@property (nonatomic, readonly) Class typeClass;

@end

OC对象可以通过Class来表示类型,而基本数据类型只能用布尔来标识.

把这些名字和类型遍历出来,肯定是为了以后有用,所以需要把它们存起来,由于它们是一个"整体",所以还是设计一个类将他们包装起来比较好.创建一个包装成员属性的类—MJProperty.

@interface MJProperty : NSObject

/** 成员属性的名字 */
@property (nonatomic, readonly) NSString *name;
/** 成员属性的类型 */
@property (nonatomic, readonly) MJPropertyType *type;

@end

这时,代码就可以进行重构了,将属于不同类的功能封装到对应的类上,让MJProperty提供一个类方法用于返回一个将objc_property_t进行包装的类.

 for (int i = 0; i < outCount; i++) {
        objc_property_t property = properties[i];

        MJProperty *propertyObj = [MJProperty propertyWithProperty:property];
 }

propertyWithProperty:方法的实现如下:

+ (instancetype)propertyWithProperty:(objc_property_t)property{
    return  [[MJProperty alloc] initWithProperty:property];
}


- (instancetype)initWithProperty:(objc_property_t)property{
    if (self = [super init]) {
        _name = @(property_getName(property));
        _type = [MJPropertyType propertyTypeWithAttributeString:@(property_getAttributes(property))];;
    }
    return self;
}

MJPropertyType也提供类方法用于包装类型:

+ (instancetype)propertyTypeWithAttributeString:(NSString *)string{
    return [[MJPropertyType alloc] initWithTypeString:string];
}

- (instancetype)initWithTypeString:(NSString *)string
{
    if (self = [super init])
    {
        NSUInteger loc = 1;
        NSUInteger len = [string rangeOfString:@","].location - loc;
        NSString *type = [string substringWithRange:NSMakeRange(loc, len)];

        NSLog(@"%@",type);
    }
    return self;
}

重构完成之后,结构显得更加清晰.更有利于接下来的工作.下面继续完成type的提取.

该部分源码请看项目实例代码中的<重构>



上面获取到的这些类型,是类型编码,在苹果文档中告诉了我们编码对应的类型:


根据这个对应关系的图表,我们将常用的几个编码定义成常量字符串或者宏表示它所对应的类型,便于编码和阅读:

/**
 *  成员变量类型(属性类型)
 */
NSString *const MJPropertyTypeInt = @"i";
NSString *const MJPropertyTypeShort = @"s";
NSString *const MJPropertyTypeFloat = @"f";
NSString *const MJPropertyTypeDouble = @"d";
NSString *const MJPropertyTypeLong = @"q";
NSString *const MJPropertyTypeChar = @"c";
NSString *const MJPropertyTypeBOOL1 = @"c";
NSString *const MJPropertyTypeBOOL2 = @"b";
NSString *const MJPropertyTypePointer = @"*";

NSString *const MJPropertyTypeIvar = @"^{objc_ivar=}";
NSString *const MJPropertyTypeMethod = @"^{objc_method=}";
NSString *const MJPropertyTypeBlock = @"@?";
NSString *const MJPropertyTypeClass = @"#";
NSString *const MJPropertyTypeSEL = @":";
NSString *const MJPropertyTypeId = @"@";

设置完后,就可以进行提取类型了.

- (instancetype)initWithTypeString:(NSString *)string
{
    if (self = [super init])
    {
        NSUInteger loc = 1;
        NSUInteger len = [string rangeOfString:@","].location - loc;
        NSString *typeCode = [string substringWithRange:NSMakeRange(loc, len)];
        [self getTypeCode:typeCode];
        NSLog(@"%@",typeCode);
    }
    return self;
}

- (void)getTypeCode:(NSString *)code
{
    if ([code isEqualToString:MJPropertyTypeId]) {
        _idType = YES;
    } else if (code.length > 3 && [code hasPrefix:@"@\""]) {
        // 去掉@"和",截取中间的类型名称
        _code = [code substringWithRange:NSMakeRange(2, code.length - 3)];
        _typeClass = NSClassFromString(_code);
        _numberType = (_typeClass == [NSNumber class] || [_typeClass isSubclassOfClass:[NSNumber class]]);
    }

    // 是否为数字类型
    NSString *lowerCode = _code.lowercaseString;
    NSArray *numberTypes = @[MJPropertyTypeInt, MJPropertyTypeShort, MJPropertyTypeBOOL1, MJPropertyTypeBOOL2, MJPropertyTypeFloat, MJPropertyTypeDouble, MJPropertyTypeLong, MJPropertyTypeChar];
    if ([numberTypes containsObject:lowerCode]) {
        _numberType = YES;

        if ([lowerCode isEqualToString:MJPropertyTypeBOOL1]
            || [lowerCode isEqualToString:MJPropertyTypeBOOL2]) {
            _boolType = YES;
        }
    }
}

至此,一个MJProperty的骨架就大致搭好了.



13F73F26-1195-43BC-BC98-FF2641B7DA58.png

当想要使用字典转模型的功能时,提供一个类方法方便转换,该方法放在NSObject+keyValue2object分类中,该分类负责字典转模型的方法实现.

@implementation NSObject (keyValue2object)

+ (instancetype)objectWithKeyValues:(id)keyValues{
    if (!keyValues) return nil;
    return [[[self alloc] init] setKeyValues:keyValues];
}

- (instancetype)setKeyValues:(id)keyValues{
    NSArray *propertiesArray = [self.class properties];
    for (MJProperty *property in propertiesArray) {
        MJPropertyType *type = property.type;
        Class typeClass = type.typeClass;
        if (type.isBoolType) {
            NSLog(@"bool");
        }else if (type.isIdType){
            NSLog(@"ID");
        }else if (type.isNumberType){
            NSLog(@"Number");
        }else{
            NSLog(@"%@",typeClass);
        }
    }
    return self;
}

@end

打印结果:


然后进行下一步----2.用该属性名作为键去字典中寻找对应的值.

 id value = [keyValues valueForKey:property.name];
 if (!value) continue;

接下来是第三步:3.拿到值后将值的类型转换为属性对应的数据类型.

首先处理数字类型,如果模型的属性是数字类型,即type.isNumberType == YES.如果字典中的值是字符串类型的,需要将其转成NSNumber类型.如果本来就是基本数据类型,则不用进行任何转换.

if (type.isNumberType){
    // 字符串->数字
    if ([value isKindOfClass:[NSString class]])
        value = [[[NSNumberFormatter alloc]init] numberFromString:value];
}

其中有一种情况,是需要进行特殊处理的.当模型的属性是char类型或者bool类型时,获取到的编码都为c,并且bool还有可能是B编码,它们都对应_boolType.因为数字类型包含布尔类型,所以bool类型要在数字类型的条件下进行额外判断.

if (type.isNumberType){
            NSString *oldValue = value;
            // 字符串->数字
            if ([value isKindOfClass:[NSString class]]){
                value = [[[NSNumberFormatter alloc] init] numberFromString:value];
                if (type.isBoolType) {
                    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 (typeClass == [NSString class]) {
                if ([value isKindOfClass:[NSNumber class]]) {
                    if (type.isNumberType)
                        // NSNumber -> NSString
                        value = [value description];
                }else if ([value isKindOfClass:[NSURL class]]){
                    // NSURL -> NSString
                    value = [value absoluteString];
                }
            }
        }

最后,进行赋值.

[self setValue:value forKey:property.name];

最简单的字典转模型大致完成了,当然,还有很多细节没有完善,但细节总是随着需求的不断变化而不断增加的.



文/__微凉(简书作者)
原文链接:http://www.jianshu.com/p/d2ecef03f19e

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值