一、Runtime简介
因为Objective-C是一门动态语言,所以它总是想办法把一些决定性工作从编译链接推迟到运行时,也就是说只有编译器是不够的,还需要一个运行时系统(runtime system)来执行编译后的代码。这就是Objective-C Runtime系统存在的意义,它是整个Objective-C运行框架的一块基石。
Runtime其实有两个版本:modern和legacy。我们现在用的Objective-C 2.0采用的是modern版的Runtime系统,只能运行在iOS 2.0和OS X 10.5之后的64位程序中。而OS X较老的32位程序仍采用Objective-C 1中的legacy版的Runtime系统。
关于这两个版本之间的区别,对于接触Objective-C时间不长的我来说,自然是没有研究过。目前如果能把现行的Runtime研究得比较透彻,就算是一个不小的进步。
二、Runtime相关头文件
iOS的SDK中usr/include/objc文件夹下有这样几个文件:
1 List.h 2 NSObjCRuntime.h 3 NSObject.h 4 Object.h 5 Protocol.h 6 a.txt 7 hashtable.h 8 hashtable2.h 9 message.h 10 module.map 11 objc-api.h 12 objc-auto.h 13 objc-class.h 14 objc-exception.h 15 objc-load.h 16 objc-runtime.h 17 objc-sync.h 18 objc.h 19 runtime.h
都是和运行时相关的头文件,其中主要使用的函数定义在message.h和runtime.h这两个文件中。在message.h中主要包含了一些向对象发送消息的函数,这是OC对象方法调用的底层实现。runtime.h是运行时最重要的文件,其中包含了对运行时进行操作的方法。
主要包括:
1、操作对象的类型的定义
1 #if !OBJC_TYPES_DEFINED 2 3 /// An opaque type that represents a method in a class definition. 4 typedef struct objc_method *Method; 5 6 /// An opaque type that represents an instance variable. 7 typedef struct objc_ivar *Ivar; 8 9 /// An opaque type that represents a category. 10 typedef struct objc_category *Category; 11 12 /// An opaque type that represents an Objective-C declared property. 13 typedef struct objc_property *objc_property_t; 14 15 struct objc_class { 16 Class isa OBJC_ISA_AVAILABILITY; 17 18 #if !__OBJC2__ 19 Class super_class OBJC2_UNAVAILABLE; 20 const char *name OBJC2_UNAVAILABLE; 21 long version OBJC2_UNAVAILABLE; 22 long info OBJC2_UNAVAILABLE; 23 long instance_size OBJC2_UNAVAILABLE; 24 struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; 25 struct objc_method_list **methodLists OBJC2_UNAVAILABLE; 26 struct objc_cache *cache OBJC2_UNAVAILABLE; 27 struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; 28 #endif 29 30 } OBJC2_UNAVAILABLE; 31 /* Use `Class` instead of `struct objc_class *` */ 32 33 #endif
这些类型的定义,对一个类进行了完全的分解,将类定义或者对象的每一个部分都抽象为一个类型type,这对于操作一个类属性和方法来说都是非常方便的。OBJC2_UNAVAILABLE标记的属性是Objective-C 2.0不支持的。
2、函数的定义
对对象进行操作的方法一般以object_开头;
对类进行操作的方法一般以class_开头;
对类或对象的方法进行操作的方法一般以method_开头;
对成员变量进行操作的方法一般以ivar_开头;
对属性进行操作的方法一般以property_开头开头;
对协议进行操作的方法一般以protocol_开头;
根据以上的函数的前缀,可以大致了解到层级关系。对于以object_开头的方法,则是runtime最终的管家,可以获取内存中类的加载信息、类的列表、关联对象和关联属性等操作。
例如:使用runtime对当前的应用中加载的类进行打印。
1 - (void)viewDidLoad 2 { 3 [super viewDidLoad]; 4 5 unsigned int outCount; 6 Class *classes = objc_copyClassList(&outCount); 7 for (int i = 0; i < outCount; i++) 8 { 9 const char *cname = class_getName(classes[i]); 10 printf("%s\n", cname); 11 } 12 }
三、技术点和应用场景
在开始这部分之前,我们需要先定义一个Person类,方便后面的叙述。Person类只是简单的定义了一个成员变量和两个属性。
1 @interface Person : NSObject 2 { 3 double _height; 4 } 5 @property (nonatomic, copy) NSString *name; 6 @property (nonatomic, assign) int age; 7 @end
1、获取属性/成员变量列表
对于获取成员变量列表,可以使用class_copyIvarList函数;对于获取属性列表,可以使用class_copyPropertyList函数。使用示例如下:
1 - (void)viewDidLoad 2 { 3 [super viewDidLoad]; 4 5 unsigned int outCount; 6 Ivar *ivarList = class_copyIvarList([Person class], &outCount); 7 for (int i = 0; i < outCount; i++) 8 { 9 Ivar *ivar = &ivarList[i]; 10 NSLog(@"%s---%s", ivar_getName(*ivar), ivar_getTypeEncoding(*ivar)); 11 } 12 13 NSLog(@"--------------------"); 14 15 objc_property_t *propertyList = class_copyPropertyList([Person class], &outCount); 16 for (int i = 0; i < outCount; i++) 17 { 18 objc_property_t *property = &propertyList[i]; 19 NSLog(@"%s", property_getName(*property)); 20 } 21 }
以上代码的输出为:
1 2016-04-05 16:45:51.038 RunTimeTest[9473:382259] _height---d 2 2016-04-05 16:45:51.039 RunTimeTest[9473:382259] _name---@"NSString" 3 2016-04-05 16:45:51.039 RunTimeTest[9473:382259] -------------------- 4 2016-04-05 16:45:51.039 RunTimeTest[9473:382259] name 5 2016-04-05 16:45:51.039 RunTimeTest[9473:382259] age
class_copyIvarList函数,官方解释是这样的:return An array of pointers of type Ivar describing the instance variables declared by the class. Any instance variables declared by superclasses are not included. 大致意思就是,这个方法会返回一个包含了所有成员变量的数组,但是所有父类的成员变量都不包含在内,这是需要注意的一点。同时官方解释还有一句话:You must free the array with free(). 我们必须手动释放这个数组,这是需要注意的第二点。
ivar_getTypeEncoding函数获取到的是成员变量的类型编码。类型编码是苹果对数据类型、对象类型规定的另一个表现形式,比如"@"代表的是对象,":"表示的是SEL指针,"v"表示的是void。具体可以看苹果官方文档对类型编码的具体规定:戳我!!!
我们都知道,@property会做三份工作:
(1)生成一个带下划线的成员变量 (2)生成这个成员变量的get方法 (3)生成这个成员变量的set方法。因此会输出三个成员变量_height、_age和_name。
因此可以说,class_copyIvarList可以获取到所有的成员变量和属性,class_copyPropertyList获取不到成员变量。
但是如果属性是readonly的并且重写了getter,此时生成的带下划线的成员变量就不在了(这里暂时还不清楚为什么),通过class_copyIvarList获取不到对应的属性,所以无论使用class_copyIvarList还是使用class_copyPropertyList都无法获取全部的成员变量和属性。
有了上面的结论,下面我们假设一个不合理的需求,以此来论证在执行KVC时是使用copyIvarList好还是使用copyPropertyList好。
这个不合理的需求就是,已经确定了对象的某个属性是readonly的并且重写了getter,在进行KVC时,想要获取全部的成员变量和属性,该怎么办呢?为什么说它不合理呢?因为这个属性已经是readonly的了,却还是想要获取到它然后执行赋值操作,这是不合常理的。
在开始之前,首先要了解setValue: forKeyPath:方法的底层实现,以name属性为例:
(1)首先去类的方法列表中寻找有没有setName,如果有,就直接调用[person setName:value];
(2)继续寻找有没有带下划线的成员变量_name,如果有,_name = value;
(3)继续寻找有没有成员变量name,如果有,name = value;
(4)如果都没有,就直接报错。
因此对于readonly的并且重写了getter的属性而言,如果使用copyPropertyList执行KVC必然报错,因此为保证代码正常,不能使用copyPropertyList为属性执行KVC。并且copyPropertyList无法获取到成员变量,无法对成员变量进行赋值。而copyIvarList的好处在于,它恰恰获取不到readonly的并且重写了getter的属性,所以很自然的为其他获取的成员变量和属性执行赋值操作。
1 - (void)viewDidLoad 2 { 3 [super viewDidLoad]; 4 5 unsigned int outCount; 6 Ivar *ivarList = class_copyIvarList([Person class], &outCount); 7 for (int i = 0; i < outCount; i++) 8 { 9 Ivar *ivar = &ivarList[i]; 10 NSLog(@"%s", ivar_getName(*ivar)); 11 12 // 这里能够获取到除了readonly并且重写了getter的所有属性和成员变量,所以可以执行KVC。 13 } 14 15 NSLog(@"--------------------"); 16 17 objc_property_t *propertyList = class_copyPropertyList([Person class], &outCount); 18 for (int i = 0; i < outCount; i++) 19 { 20 objc_property_t *property = &propertyList[i]; 21 NSLog(@"%s", property_getName(*property)); 22 23 // 这里能够获取所有的属性,如果属性是readonly并且重写了getter的,这里照样可以获取, 24 // 此时如果执行KVC,必然报错。 25 } 26 }
可是如果只是想对public的成员变量执行KVC,而不想对private的成员变量执行KVC,那又该怎么办呢?上面已经论证过了,如果有的成员变量是readonly并且重写了getter的话,不能使用copyPropertyList,而我们又不想对private的成员变量执行KVC,那是不是就没有办法了呢?当然不是,此时我们可以通过copyIvarList获取所有的成员变量和属性,然后去掉copyPropertyList没有的成员变量,那么剩下的就是我们想要的成员变量了。
例如:Person类有一个private成员变量_height和两个public成员变量name、age,其中age是readonly并且重写了getter的了,那么利用copyIvarList可以获取_height和_name,利用copyPropertyList可以获取name和age,然后去掉copyPropertyList没有的成员变量,也即去掉_height,剩下的_name就是我们想要的结果。
1.1 应用1 KVC字典转模型
获取属性/成员列表一个重要的应用就是,一次取出模型中的属性/成员变量,根据它的名字获取字典中的key,然后取出字典中这个key对应的value,使用setValue: forKeyPath:方法设置值。
1 - (void)viewDidLoad 2 { 3 [super viewDidLoad]; 4 5 NSDictionary *dict = @{@"name":@"zhangsan", @"age":@20, @"height":@175, @"test1":@"asdfasdf", @"test2":@"asdfsadfsad"}; 6 7 Person *person = [[Person alloc] init]; 8 9 unsigned int outCount; 10 Ivar *ivarList = class_copyIvarList([Person class], &outCount); 11 for (int i = 0; i < outCount; i++) 12 { 13 Ivar *ivar = &ivarList[i]; 14 NSString *name = [NSString stringWithUTF8String:ivar_getName(*ivar)]; 15 NSString *key = [name substringFromIndex:1]; // 去掉成员变量前面的'_' 16 [person setValue:dict[key] forKey:key]; 17 } 18 19 NSLog(@"%@", person); // 已经重写了description 20 }
为什么要这样,而不再使用方法setValuesForKeysWithDictionary:,因为在setValuesForKeysWithDictionary:方法内部会执行这样一个过程:遍历字典里面的所有key,取出key的value,即dict[key],使用方法setValue: forKeyPath:进行赋值(这个方法的执行过程在前面已经提及)。这也就解释了当字典中的key比模型中多时,会出现" this class is not key-value compliant for 'xxx' "的bug了。那么当模型中的属性比字典中多时,使用setValuesForKeysWithDictionary:会不会有bug呢?经测试:当多出来的属性是对象数据类型时,为null;当属性是基本数据类型时,会有一个系统默认值(如int为0)。
使用运行时KVC字典转模型,即使字典中的key比模型中多的时候也不会有bug,但是新的问题出现了,如果模型中的属性比字典中的key多便会出现bug,而且是这样一种情况:如果多的是对象类型,则不会有bug,该属性的值为null;如果多的是基本数据类型,就会出错" could not set nil as the value for the key 'xxx' "。
那么如何解决上面的bug呢?可以在setValue: forKeyPath:方法调用之前进行如下处理:取出属性对应的类型,如果类型是基本数据类型,value替换为默认值(如int对应默认值为0)。
1 - (void)viewDidLoad 2 { 3 [super viewDidLoad]; 4 5 NSDictionary *dict = @{@"name":@"zhangsan", @"age":@20, @"test1":@"asdfasdf", @"test2":@"asdfsadfsad"}; 6 7 Person *person = [[Person alloc] init]; 8 9 unsigned int outCount; 10 Ivar *ivarList = class_copyIvarList([Person class], &outCount); 11 for (int i = 0; i < outCount; i++) 12 { 13 Ivar *ivar = &ivarList[i]; 14 NSString *name = [NSString stringWithUTF8String:ivar_getName(*ivar)]; 15 NSString *key = [name substringFromIndex:1]; // 去掉成员变量前面的'_' 16 17 id value = dict[key]; 18 19 NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(*ivar)]; // 获取属性类型 20 if ([type isEqualToString:@"d"]) // 判断属性类型是否为基本类型 21 { 22 value = @0.0; 23 } 24 25 // 这样即便dict中没有height这个key,也不会报错了 26 [person setValue:value forKey:key]; 27 } 28 29 NSLog(@"%@", person); // 已经重写了description 30 }
1.2 应用2 NSCoding归档和解归档
获取属性/成员列表另外一个重要的应用就是进行归档和解归档,其原理和上面的KVC基本上一样,这里只是展示一些代码:
1 - (instancetype)initWithCoder:(NSCoder *)aDecoder 2 { 3 if (self = [super init]) 4 { 5 unsigned int outCount; 6 Ivar *ivarList = class_copyIvarList(self.class, &outCount); 7 for (int i = 0; i < outCount; i++) 8 { 9 Ivar *ivar = &ivarList[i]; 10 NSString *name = [NSString stringWithUTF8String:ivar_getName(*ivar)]; 11 NSString *key = [name substringFromIndex:1]; 12 id value = [aDecoder decodeObjectForKey:key]; 13 14 [self setValue:value forKey:key]; 15 } 16 } 17 return self; 18 } 19 20 - (void)encodeWithCoder:(NSCoder *)aCoder 21 { 22 unsigned int count = 0; 23 Ivar *ivarList = class_copyIvarList(self.class, &count); 24 for (int i = 0; i < count; i++) 25 { 26 Ivar *ivar = &ivarList[i]; 27 NSString *name = [NSString stringWithUTF8String:ivar_getName(*ivar)]; 28 NSString *key = [name substringFromIndex:1]; 29 30 id value = [self valueForKey:key]; 31 [aCoder encodeObject:value forKey:key]; 32 } 33 }
ps:后续再继续学习Runtime的其他技术点和应用场景。
代码地址:GitHub,每一个知识点都对应一个版本,需要的小伙伴可以下载查看,同时也欢迎评论交流,共同进步。