预备知识
理解Class与MetalClass
下面这张图很重要,得记住哦。具体的介绍,还是看权威的文章吧,参考文章1、文章2;
个人理解,在我们获取每一个类、实例对象的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
信号量来确保字典读取的线程安全性。
没有缓存,则创建_YYModelMeta
和YYClassInfo
对象,其中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);
}
}
问题
- 为何要做metaclass的处理和判断?
- 获取方法列表和实例变量列表的意义在哪?