YYModel实现原理探究

预备知识

理解Class与MetalClass

下面这张图很重要,得记住哦。具体的介绍,还是看权威的文章吧,参考文章1、文章2;

YYModel实现原理探究_第1张图片
类图

个人理解,在我们获取每一个类、实例对象的Class时,实际上获取的是isa对象,正如上图所示,下面的示例代码也说明了情况, 对object1、object2对象获取class时获取的是各自的isa对像得到class1、class2,实际上class1、class2为同一个对象,当再次获取class1、class2的class时获取的就是元类了:

    NSObject *object1 = [[NSObject alloc] init];
    NSObject *object2 = [[NSObject alloc] init];
    Class class1 = [object1 class];
    Class class2 = [object2 class];
    NSLog(@"class1:%p, class2:-%p", class1, class2); // class1:0x1b7af3ea0, class2:-0x1b7af3ea0
    
    Class metaClass1 = object_getClass(class1);
    Class metaClass2 = object_getClass(class2);
    NSLog(@"metaClass1:%p, metaClass2:-%p", metaClass1, metaClass2); // metaClass1:0x1b7af3ec8, metaClass2:-0x1b7af3ec8
    
    Class metaClass3 = object_getClass(metaClass1);
    Class metaClass4 = object_getClass(metaClass2);
    NSLog(@"metaClass3:%p, metaClass4:-%p", metaClass3, metaClass4); // metaClass3:0x1b7af3ec8, metaClass4:-0x1b7af3ec8

Method

Method本身是一个objc_method类型的结构体指针即方法的具体表示,objc_method是一个结构体, 其定义如下:

struct objc_method {
    SEL _Nonnull method_name                                方法名;
    char * _Nullable method_types                           方法类型, 即参数、返回值类型;
    IMP _Nonnull method_imp                                 方法对应的函数指针;
}   
// 获取类所有的方法
unsigned int methodCount = 0;
Method *methods = class_copyMethodList(cls, &methodCount);
method_getName(method) // 获取方法的名称,返回的是一个SEL类型
method_getImplementation(method) // 获取方法的函数指针
sel_getName(_sel) // 将SEL类型转化为C字符串
method_getTypeEncoding(method) // 获取方法的参数、返回值类型,例如:“v16@0:8”
method_copyReturnType(method) // 获取方法的返回值类型,例如:“v”
method_getNumberOfArguments(method) // 获取方法参数的个数
method_copyArgumentType(method, i) // 获取方法指定参数的类型, 例如“@”

objc_property、类型编码

objc_property_t本身是一个objc_property类型的结构体指针即属性的具体表示,我承认我没有找到objc_property的定义,但是找到一个objc_property_attribute_t结构体,它是属性的attribute,也就是其实是对属性的详细描述,包括属性名称、属性编码类型、原子类型/非原子类型等,可以参考类型编码的文章。它的定义如下:


/// Defines a property attribute
typedef struct {
    const char *name;           /**< The name of the attribute */
    const char *value;          /**< The value of the attribute (usually empty) */
} objc_property_attribute_t;

// 通过property_copyAttributeList(property, &attrCount)函数可以获取一个存有该结构体的数组。

该结构体的name和value均为char型的指针,也就是一个C字符串,在YYModel中可以看到对于他们的使用是获取name[0]、*name的值,也就是获取C字符串的第一个字符。需要注意的是,当name[0]为‘T’时,证明此时的objc_property_attribute_t结构体描述的是属性对应的数据类型,是BOOL还是NSString类型等。再比如name[0]为’N‘时表明该结构体描述的是属性的原子性,即为nonatomic。另外当name[0]为’G‘、’S‘时分别表示该属性自定义了getter、setter方法。

// 获取所有的属性
unsigned int propertyCount = 0;
objc_property_t *properties = class_copyPropertyList(cls, &propertyCount);
property_getName(property)  // 获取属性的名称
property_copyAttributeList(property, &attrCount) // 获取属性的详细信息,返回一个数组

备注:所有通过C函数所获取的数据,都应该手动调用free()函数释放内存

Ivar

Ivar本身是一个objc_ivar类型的结构体指针即实例变量的具体表示,objc_ivar是一个结构体, 其定义如下:

struct objc_ivar {
    char * _Nullable ivar_name                               变量名;
    char * _Nullable ivar_type                               变量类型;
    int ivar_offset                                          基地址偏移字节;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
} 
ivar_getName(ivar) // 获取实例变量名称
ivar_getOffset(ivar) // 获取基地址偏移字节
ivar_getTypeEncoding(ivar) // 获取变量类型

NSScanner

NSScanner是一个类,用于在字符串中扫描指定的字符,尤其是把它们翻译/转换为数字和别的字符串。可以在创建NSScaner时指定它的string属性,然后scanner会按照你的要求从头到尾地扫描这个字符串的每个字符。
对于查询字符串而言,这真是一个强大的类,下面简单学习下YYModel是如何利用该类进行类型编码解析的:

// 该类用于解析属性的数据类型,YYModel考虑得很全面,连遵循协议的id类型都有考虑

// 如果解析到是一个NSObject对象则进行如下详细的解析,例如:NSString类型时_typeEncoding为@"NSString"
            if ((type & YYEncodingTypeMask) == YYEncodingTypeObject && _typeEncoding.length) {
            
                        // 用需要扫描的_typeEncoding字符串,初始化scanner对象
                        NSScanner *scanner = [NSScanner scannerWithString:_typeEncoding];
                        // 我们的目的是拿到NSString,所以需要更改scanner扫描的起始位置为字符N所在的位置
                        if (![scanner scanString:@"@\"" intoString:NULL]) continue;
                        
                        NSString *clsName = nil;
                        //  开始扫描_typeEncoding直到遇到字符”或者<,遇到了则返回YES,并将扫描到的字符串存入clsName。
                        //  需要注意的是NSCharacterSet其实设置的是停止扫描的字符,如果该字符出现在NSScanner扫描的起始位置,该方法会返回NO,clsName也会为null
                        if ([scanner scanUpToCharactersFromSet: [NSCharacterSet characterSetWithCharactersInString:@"\"<"] intoString:&clsName]) {
                            if (clsName.length) _cls = objc_getClass(clsName.UTF8String);
                        }
                        
                        
                        // 当上面返回NO,进行协议的捕获
                        NSMutableArray *protocols = nil;
                        while ([scanner scanString:@"<" intoString:NULL]) {
                            NSString* protocol = nil;
                            if ([scanner scanUpToString:@">" intoString: &protocol]) {
                                if (protocol.length) {
                                    if (!protocols) protocols = [NSMutableArray new];
                                    [protocols addObject:protocol];
                                }
                            }
                            [scanner scanString:@">" intoString:NULL];
                        }
                        _protocols = protocols;
                    }

objc_msgSend

YYModel中设置和获取属性的值均经过objc_msgSend函数实现,我们要知道,所有OC的方法的调用(消息的接收)在编译之后均是通过调用objc_msgSend函数完成的。关于objc_msgSend的使用可参考链接。要想验证编译后的OC代码,可以在终端下使用Clang命令进行验证。具体可以参考该文章.

实现解析

重要的类

  • _YYModelMeta —————— 完成Model属性、方法、实例变量的解析,并生成与数据源相对应的字典映射;
  • YYClassInfo —————— 存储Model解析后的各种信息;
  • YYClassMethodInfo —————— 存储方法的信息;
  • YYClassIvarInfo —————— 存储实例变量的信息;
  • YYClassPropertyInfo —————— 存储属性的信息;
  • _YYModelPropertyMeta —————— 该类包含了属性的信息和设置属性时所需要的信息,有上述解析得到的信息和自定义的信息合成,用于第二步中的映射关系生成。

实现步骤

1、解析实体信息

自定义的Model类调用modelWithDictionary:modelWithJSON:进行Model的初始化,接下来_YYModelMeta类将会为我们完成解析的工作。解析的目的是获取Model类的方法、属性、实例变量信息,这些信息将保存在YYClassInfo中。

在解析之初会首先检查是否存在缓存,如果有缓存则直接返回缓存的_YYModelMeta对象。通常情况下,每一个类的属性、实例变量的解析只会进行一次,成功解析一次后的数据将会被缓存起来,只有当设置了YYClassInfo_needUpdate才会进行新的解析,也就是进行动态的添加属性、修改方法后需要更新。缓存数据保存在一个静态的CFMutableDictionaryRef字典当中,并通过dispatch_semaphore_t信号量来确保字典读取的线程安全性。

没有缓存,则创建_YYModelMetaYYClassInfo对象,其中YYClassInfo也加入了缓存处理,个人觉得这里是没有必要的,因为_YYModelMeta已经有属性引用了YYClassInfo

YYClassInfo_update会为我们完成具体的解析,该方法依次对方法、属性、实例变量进行了解析,个人发现方法、实例变量解析的信息并未用到。所以这里着重说一下属性的解析,通过class_copyPropertyList获取到属性列表,并为每一个属性生成一个YYClassPropertyInfo对象,该对象会保存在YYClassInfo_propertyInfos数组中。生成的YYClassPropertyInfo对象主要包含了属性名、setter、getter、属性数据类型等,这些信息都有对应的runtime方法。其中核心在于利用类型编码完成属性数据类型的解析。

解析属性的核心代码:

/// 1.获取Model类的属性列表
 unsigned int propertyCount = 0;
    objc_property_t *properties = class_copyPropertyList(cls, &propertyCount);
    if (properties) {
        NSMutableDictionary *propertyInfos = [NSMutableDictionary new];
        // 将生成的YYClassPropertyInfo对象保存在该数组中
        _propertyInfos = propertyInfos;
        for (unsigned int i = 0; i < propertyCount; i++) {
            YYClassPropertyInfo *info = [[YYClassPropertyInfo alloc] initWithProperty:properties[i]];
            if (info.name) propertyInfos[info.name] = info;
        }
        // 记得释放内存
        free(properties); 
    }

/// 2.初始化YYClassPropertyInfo对象
- (instancetype)initWithProperty:(objc_property_t)property {
    if (!property) return nil;
    self = [super init];
    _property = property;
    
    // 获取属性的名称
    const char *name = property_getName(property);
    if (name) {
         // 将属性名称有C字符串转化为NSString
        _name = [NSString stringWithUTF8String:name];
    }
    
    YYEncodingType type = 0;
    unsigned int attrCount;
    // 获取属性对应的所有描述,即objc_property_attribute_t的结构体数组,包括原子性、数据类型、内存语义等
    objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);
    for (unsigned int i = 0; i < attrCount; i++) {
        // objc_property_attribute_t的name为C字符串,name[0]表示获取第一个字符
        switch (attrs[i].name[0]) {
            case 'T': { // 这里将解析数据的类型,比如NSString、BOOL、NSArray等
                if (attrs[i].value) {
                    _typeEncoding = [NSString stringWithUTF8String:attrs[i].value];
                    // 刚方法用于解析具体的数据类型
                    type = YYEncodingGetType(attrs[i].value);
                    
                    // 只有当数据类型为NSObject类,包括采纳了协议的属性,才需要如下进一步的解析
                    if ((type & YYEncodingTypeMask) == YYEncodingTypeObject && _typeEncoding.length) {
                        NSScanner *scanner = [NSScanner scannerWithString:_typeEncoding];
                        if (![scanner scanString:@"@\"" intoString:NULL]) continue;
                        
                        NSString *clsName = nil;
                        if ([scanner scanUpToCharactersFromSet: [NSCharacterSet characterSetWithCharactersInString:@"\"<"] intoString:&clsName]) {
                             // 将解析到的数据类型的类保存在_cls属性中
                            if (clsName.length) _cls = objc_getClass(clsName.UTF8String);
                        }
                        
                        // 对于采纳协议的属性的解析
                        NSMutableArray *protocols = nil;
                        while ([scanner scanString:@"<" intoString:NULL]) {
                            NSString* protocol = nil;
                            if ([scanner scanUpToString:@">" intoString: &protocol]) {
                                if (protocol.length) {
                                    if (!protocols) protocols = [NSMutableArray new];
                                    [protocols addObject:protocol];
                                }
                            }
                            [scanner scanString:@">" intoString:NULL];
                        }
                        _protocols = protocols;
                    }
                }
            } break;
            case 'V': { // Instance variable
                if (attrs[i].value) {
                    _ivarName = [NSString stringWithUTF8String:attrs[i].value];
                }
            } break;
            case 'R': {
                type |= YYEncodingTypePropertyReadonly;
            } break;
            case 'C': {
                type |= YYEncodingTypePropertyCopy;
            } break;
            case '&': {
                type |= YYEncodingTypePropertyRetain;
            } break;
            case 'N': {
                type |= YYEncodingTypePropertyNonatomic;
            } break;
            case 'D': {
                type |= YYEncodingTypePropertyDynamic;
            } break;
            case 'W': {
                type |= YYEncodingTypePropertyWeak;
            } break;
            case 'G': { // 获取自定义的getter方法
                type |= YYEncodingTypePropertyCustomGetter;
                if (attrs[i].value) {
                    _getter = NSSelectorFromString([NSString stringWithUTF8String:attrs[i].value]);
                }
            } break;
            case 'S': { // 获取自定义的setter方法
                type |= YYEncodingTypePropertyCustomSetter;
                if (attrs[i].value) {
                    _setter = NSSelectorFromString([NSString stringWithUTF8String:attrs[i].value]);
                }
            } // break; commented for code coverage in next line
            default: break;
        }
    }
    if (attrs) {
        free(attrs);
        attrs = NULL;
    }
    
    // 保存获取的type以及验证_setter、_getter是否已经获取到,这三个要素在设置属性值时将会被用到
    _type = type;
    if (_name.length) {
        if (!_getter) {
            _getter = NSSelectorFromString(_name);
        }
        if (!_setter) {
            _setter = NSSelectorFromString([NSString stringWithFormat:@"set%@%@:", [_name substringToIndex:1].uppercaseString, [_name substringFromIndex:1]]);
        }
    }
    return self;
}

2、生成映射关系

通过生成的YYClassInfo来建立与数据源之间的映射关系,也就是生成了一个以数据源字典的key为key,以_YYModelPropertyMeta对象为值的字典_mapper,核心类为_YYModelPropertyMeta,该类包含了属性的信息和用户自定义的信息,例如属性的setter、getter、对应数据源的key、子类容器等。

在生成_mapper的过程中包括白名单、黑名单、容器类、自定义数据源的key与属性名映射关系的处理,具体可参见_YYModelMeta类的initWithClass:方法。

3、设置属性值

上述两个步骤均是在_YYModelMeta中完成的,第三部回到NSObject+YYModel分类中,用modelSetWithDictionary:方法完成属性值的设置。

核心代码:

/// 使用下面的函数,遍历字典并通过ModelSetWithDictionaryFunction函数完成Model属性值的设置
CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context);

/// context为ModelSetContext结构体
typedef struct {
    void *modelMeta;  ///  为步骤1、2完成后生成的_YYModelMeta对象
    void *model;      ///  要生成的实体类,相当于id类型
    void *dictionary; ///  数据源
} ModelSetContext;

/// 属性值的设置最终通过objc_msgSend函数实现,例如下列对NSString类型的属性进行设置
if ([value isKindOfClass:[NSString class]]) {
                        if (meta->_nsType == YYEncodingTypeNSString) {
                            ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, value);
                        } else {
                            ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, ((NSString *)value).mutableCopy);
                        }
                    }

问题

  1. 为何要做metaclass的处理和判断?
  2. 获取方法列表和实例变量列表的意义在哪?

你可能感兴趣的:(YYModel实现原理探究)