iOS运行时 - Runtime的简介以及使用场景
1. runtime简介
因为Objc是一门动态语言,所以它总是想办法把一些决定工作从编译连接推迟到运行时。也就是说只有编译器是不够的,还需要一个运行时系统 (runtime system) 来执行编译后的代码。这就是 Objective-C Runtime 系统存在的意义,它是整个Objc运行框架的一块基石。
Runtime其实有两个版本:“modern”和 “legacy”。我们现在用的 Objective-C 2.0 采用的是现行(Modern)版的Runtime系统,只能运行在 iOS 和 OS X 10.5 之后的64位程序中。而OS X较老的32位程序仍采用 Objective-C 1中的(早期)Legacy 版本的 Runtime 系统。这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你需要重新编译它的子类,而现行版就不需要。
Runtime基本是用C和汇编(437版本开始较多使用mm文件,但是仍用C语法)实现的,可见苹果为了动态系统的高效而作出了很多努力,你可以在这里下到苹果维护的开源代码runtime源码。苹果和GNU各自维护一个开源的runtime版本,这两个版本之间都在努力的保持一致。
2. Runtime相关的头文件
ios的sdk中 usr/include/objc文件夹下面的文件
List.h
NSObjCRuntime.h
NSObject.h
Object.h
Protocol.h
a.txt
hashtable.h
hashtable2.h
message.h
module.map
objc-api.h
objc-auto.h
objc-class.h
objc-exception.h
objc-load.h
objc-runtime.h
objc-sync.h
objc.h
runtime.h
都是和运行时相关的头文件,其中主要使用的函数定义在message.h和runtime.h这两个文件中。 在message.h中主要包含了一些向对象发送消息的函数,这是OC对象方法调用的底层实现。 runtime.h是运行时最重要的文件,其中包含了对运行时进行操作的方法。 主要包括
//runtime.h
// An opaque type that represents a method in a class definition. 一个类型,代表着类定义中的一个方法
typedef struct objc_method *Method;
/// An opaque type that represents an instance variable.代表实例(对象)的变量
typedef struct objc_ivar *Ivar;
/// An opaque type that represents a category.代表一个分类
typedef struct objc_category *Category;
/// An opaque type that represents an Objective-C declared property.代表OC声明的属性
typedef struct objc_property *objc_property_t;
// Class代表一个类,它在objc.h中这样定义的 typedef struct objc_class *Class;
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
这些类型的定义,对一个类进行了完全的分解,将类定义或者对象的每一个部分都抽象为一个类型type,对操作一个类属性和方法非常方便。OBJC2_UNAVAILABLE
标记的属性是Ojective-C 2.0不支持的,但实际上可以用响应的函数获取这些属性,例如:如果想要获取Class的name属性,可以按如下方法获取:
Class classPerson = Person.class;
// printf("%s\n", classPerson->name); //用这种方法已经不能获取name了 因为OBJC2_UNAVAILABLE
const char *cname = class_getName(classPerson);
printf("%s", cname); // 输出:Person
2.1 函数的定义
对对象进行操作的方法一般以object_
开头
对类进行操作的方法一般以class_
开头
对类或对象的方法进行操作的方法一般以method_
开头
对成员变量进行操作的方法一般以ivar_
开头
对属性进行操作的方法一般以property_
开头
对协议进行操作的方法一般以protocol_
开头
根据以上的函数的前缀 可以大致了解到层级关系。对于以objc_
开头的方法,则是runtime最终的管家,可以获取内存中类的加载信息,类的列表,关联对象和关联属性等操作。
例如:使用runtime对当前的应用中加载的类进行打印,别被吓一跳。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
unsigned int count = 0;
Class *classes = objc_copyClassList(&count);
for (int i = 0; i < count; i++) {
const char *cname = class_getName(classes[i]);
printf("%s\n", cname);
}
}
3. 技术点和应用场景
在以下的代码中,都用到了Person类,Person类知识简单的定义了一个成员变量和两个属性
@interface Person : NSObject
{
@private
float _height;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@end
3.1 获取属性、成员变量列表
对于获取成员变量的列表可以使用class_copyIvarList
函数,如果想要获取属性列表可以使用class_copyPropertyList
函数,使用的示例如下:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
Class classPerson = NSClassFromString(@"Person"); // 与下面一句效果一样,可以不用导入头文件
// Class clazz = Person.class;
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList(classPerson, &count); // 获取成员变量数组
for (int i = 0; i < count; i++) {
const char *cname = ivar_getName(ivarList[i]); // 获取成员变量的名字
NSString *name = [NSString stringWithUTF8String:cname];
NSLog(@"%@", name);
}
NSLog(@"-------------------分割线------------------");
objc_property_t *propertyList = class_copyPropertyList(classPerson, &count); // 获取属性数组
for (int i = 0; i < count; i++) {
const char *cname = property_getName(propertyList[i]);
NSString *name = [NSString stringWithUTF8String:cname];
NSLog(@"%@", name);
}
}
以上代码的输出为:
2015-06-05 22:28:16.194 runtime终极[4192:195757] _height
2015-06-05 22:28:16.195 runtime终极[4192:195757] _age
2015-06-05 22:28:16.195 runtime终极[4192:195757] _name
2015-06-05 22:28:16.195 runtime终极[4192:195757] -------------------分割线------------------
2015-06-05 22:28:16.195 runtime终极[4192:195757] name
2015-06-05 22:28:16.195 runtime终极[4192:195757] age
为什么会有上面的输出结果,因为@property会做三份工作: 1.生成一个带下划线的成员变量 2.生成这个成员变量的get方法 3.生成这个成员变量的set方法
因此可以说:ivarList可以获取到@property关键字定义的属性 ,而propertyList不可以获取到成员变量。也就是:使用ivarList是可以将所有的成员变量和属性都获取的。
一个问题,对于一个readonly的属性,到底是didSet+set好,还是重写getter好?
大部分的readonly的属性是计算型的,依旧是依赖于其他属性,因此可以使用didSet+set,也就是在其他属性的set方法中,将本属性set。但是didSet+set有时候完全没有必要,不符合懒加载的规则,浪费了计算能力,用重写getter的方法好一些,因此重写getter总是好一些。
在KVC时,想要获取全部的成员变量和属性,怎么办?
首先要了解setValue: forKeyPath:
方法的底层实现:
以name为例:
-
首先先去类的方法列表去寻找有没有setName:,如果有,就直接调用[person setName:value]
-
找找有没有带下划线的成员变量
_name
,如果有_name = value
-
找有没有成员变量
name
,如果有name = value
-
如果都没有找到,直接报错
因此对于readonly的又重写了getter的属性而言:如果对propertyList的属性一次使用kvc,就会报错,因此为保证代码正常,不能使用propertyList的属性进行kvc;
另外:这种属性本来就是计算型的了,为什么还有为它赋值呢,因此对它进行kvc也不合情理。
当使用ivaList时,直接就无法获取到这种属性,因此是kvc的最佳方案。再者,使用propertyList无法获取成员变量(_height),无法对成员变量进行赋值。而使用ivaList是可以将该赋值的成员变量都获取的。
3.1.1 应用1: KVC字典转模型
获取属性、成员变量列表一个重要的应用就是,一次取出模型中的属性、成员变量,根据它的名称获取字典中的key然后去除字典中这个key对应的value,使用setValue: forKeyPath:
方法设置值,为什么使用这个,不使用setValueForKeysWithDictionary
。因为在setValueForKeysWithDictionary
方法内部会执行以下这个过程:
1)取出key
2)取出key对应的value,即dict[key],直接给模型中的属性、成员变量赋值
3)怎么给模型中的属性、成员变量赋值,使用setValue: forKeyPath:
,上文已经提过
这里会有一个问题,因此,开发中经常遇到的字典中的key比模型中多时,会出现的 this class is not key-value compliant for ‘xxx’
这个bug就很好解释了,通常是因为字典中的key,比模型中的属性\成员变量多。那么当模型中的属性比字典中多时,使用setValuesForKeysWithDictionary:
会不不会有bug呢?经测试:当多出来的属性是对象数据类型时,为null,当属性是基本数据类型时,会有一个系统默认值(如int为0)。
因此使用逐一为属性赋值的方法进行KVC: