iOS学习之Runtime(一)

一、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
View Code

    都是和运行时相关的头文件,其中主要使用的函数定义在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
View Code

  这些类型的定义,对一个类进行了完全的分解,将类定义或者对象的每一个部分都抽象为一个类型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 }
View Code

三、技术点和应用场景

 在开始这部分之前,我们需要先定义一个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
View Code

 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 }
View Code

  以上代码的输出为:

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
View Code

  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 }
View Code

   可是如果只是想对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 }
View Code

  为什么要这样,而不再使用方法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 }
View Code

 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 }
View Code

 

ps:后续再继续学习Runtime的其他技术点和应用场景。

  代码地址:GitHub,每一个知识点都对应一个版本,需要的小伙伴可以下载查看,同时也欢迎评论交流,共同进步。

 

posted @ 2016-04-08 11:41  游玩屋  阅读(240)  评论(0编辑  收藏  举报