Runtime
Runtime 是一个运行时库,主要使用 C 和汇编写的库,为 C 添加了面向对象的能力并创造了 Objective-C,并且拥有消息分发,消息转发等功能。
也就是 Runtime 涉及三个点,面向对象,消息分发,消息转发。
面向对象:
Objective-C 的对象是基于 Runtime 创建的结构体。先从代码层面分析一下。
Class *class = [[Class alloc] init];
alloc 方法会为对象分配一块内存空间,空间的大小为 isa_t(8 字节)的大小加上所有成员变量所需的空间,再进行一次内存对齐。分配完空间后会初始化 isa_t ,而 isa_t 是一个 union 类型的结构体(或者称之为联合体),它的结构是在 Runtime 里被定义的。
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
};
};
从 isa_t 的结构可以看出,isa_t 可以存储 struct,uintptr_t 或者 Class 类型
。
init 方法就直接返回了初始化好的对象,class 指针指向这个初始化好的对象。
也就是在 Runtime 的协助之下,一个对象完成了创建。
你可能想知道,这个对象只存放了一个 isa_t 结构体和成员变量,对象的方法在哪里?
在编译的时候,类在内存中的位置就已经确定,而在 main 方法之前,Runtime 将可执行文件中和动态库所有的符号(Class,Protocol,Selector,IMP,…)加载到内存中,由 Runtime 管理,这里也包括了也是一个对象的类。
类对象里储存着一个 isa_t 的结构体,super_class 指针,cache_t 结构体,class_data_bits_t 指针。
struct objc_class : objc_object {
isa_t isa;
Class superclass;
cache_t cache;
class_data_bits_t bits;
}
class_data_bits_t 指向类对象的数据区域,数据区域存放着这个类的实例方法链表。而类方法存在元类对象的数据区域。也就是有对象,类对象,元类对象三个概念,对象是在运行时动态创建的,可以有无数个,类对象和元类对象在 main 方法之前创建的,分别只会有一个。
消息分发:
在 Objective-C 中的“方法调用”其实应该叫做消息传递,[object message] 会被编译器翻译为 objc_msgSend(object, @selector(message)),这是一个 C 方法,首先看它的两个参数,第一个是 object ,既方法调用者,第二个参数称为选择子 SEL,Objective-C 为我们维护了一个巨大的选择子表,在使用 @selector() 时会从这个选择子表中根据选择子的名字查找对应的 SEL。如果没有找到,则会生成一个 SEL 并添加到表中,在编译期间会扫描全部的头文件和实现文件将其中的方法以及使用 @selector() 生成的选择子加入到选择子表中。
通过第一个参数 object,可以找到 object 对象的 isa_t 结构体,从上文中能看 isa_t 结构体的结构,在 isa_t 结构体中,shiftcls 存放的是一个 33 位的地址,用于指向 object 对象的类对象,而类对象里有一个 cache_t 结构体,来看一下 cache_t 的具体代码,
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
}
_mask:分配用来缓存 bucket 的总数。
_occupied:表明目前实际占用的缓存 bucket 的个数。
_buckets:一个散列表,用来方法缓存,bucket_t 类型,包含 key 以及方法实现 IMP。
struct bucket_t {
private:
cache_key_t _key;
IMP _imp;
}
objc_msgSend() 方法会先从缓存表里,查找是否有该 SEL 对应的 IMP,有的话算命中缓存,直接通过函数指针 IMP ,找到方法的具体实现函数,执行。
当然缓存表里可能并不会命中,则此时会根据类对象的 class_data_bits_t 指针找到数据区域,数据区域里用链表存放着类的实例方法,实例方法也是一个结构体,其结构为:
struct method_t {
SEL name;
const char *types;
IMP imp;
};
编译器将每个方法的返回值和参数类型编码为一个字符串,types 指向的就是这样一个字符串,objc_msgSend() 会在类对象的方法链表里按链表顺序去匹配 SEL,匹配成功则停止,并将此方法加入到类对象的 _buckets 里缓存起来。如果没找到则会通过类对象的 superclass 指针找到其父类,去父类的方法列表里寻找(也会从父类的方法缓存列表开始)。
如果继续没有找到会一直向父类寻找,直到遇见 NSObject,NSObject 的 superclass 指向 nil。也就意味着寻找结束,并没有找到实现方法。(如果这个过程找到了,也同样会在 object 的类对象的 _buckets 里缓存起来)。
选择子在当前类和父类中都没有找到实现,就进入了方法决议(method resolve),首先判断当前 object 的类对象是否实现了 resolveInstanceMethod: 方法,如果实现的话,会调用 resolveInstanceMethod:方法,这个时候我们可以在 resolveInstanceMethod:方法里动态的添加该 SEL 对应的方法(也可以去做点别的,比如写入日志)。之后会重新执行查找方法实现的流程,如果依旧没找到方法,或者没有实现 resolveInstanceMethod: 方法,Runtime 还有另一套机制,消息转发。
消息转发:
消息转发分为以下几步:
1.调用 forwardingTargetForSelector: 方法,尝试找到一个能响应该消息的对象。如果获取到,则直接转发给它。如果返回了 nil,继续下面的动作。
2.调用 methodSignatureForSelector: 方法,尝试获得一个方法签名。如果获取不到,则直接调用 doesNotRecognizeSelector 抛出异常。
3.调用 forwardInvocation: 方法,将第 2 步获取到的方法签名包装成 Invocation 传入,如何处理就在这里面了。
以上三个方法都可以通过在 object 的类对象里实现, forwardingTargetForSelector: 可以通过对参数 SEL 的判断,返回一个可以响应该消息的对象。这样则会重新从该对象开始执行查找方法实现的流程,找到了也同样会在 object 的类对象的 _buckets 里缓存起来。而 2,3 方法则一般是配套使用,实现 methodSignatureForSelector: 方法根据参数 SEL ,做相应处理,返回 NSMethodSignature (方法签名) 对象,NSMethodSignature 对象会被包装成 NSInvocation 对象,forwardInvocation: 方法里就可以对 NSInvocation 进行处理了。
上面是讲的是实例方法,类方法没什么区别,类方法储存在元类对象的数据区域里,通过类对象的 isa_t 找到元类对象,执行查找方法实现的流程,元类对象的 superclass 最终也会指向 NSObject。没找到的话,也会有方法决议以及消息转发。
runtime 可以做什么:
实现多继承:从 forwardingTargetForSelector: 方法就能知道,一个类可以做到继承多个类的效果,只需要在这一步将消息转发给正确的类对象就可以模拟多继承的效果。
Method m1 = class_getInstanceMethod([M1 class], @selector(hello1));
Method m2 = class_getInstanceMethod([M2 class], @selector(hello2));
method_exchangeImplementations(m2, m1);
关联对象:
通过下面两个方法,可以给 category 实现添加成员变量的效果。
objc_setAssociatedObject
objc_getAssociatedObject
动态添加类和方法:
objc_allocateClassPair 函数与 objc_registerClassPair 函数可以完成一个新类的添加,class_addMethod 给类添加方法,class_addIvar 添加成员变量,objc_registerClassPair 来注册类,其中成员变量的添加必须在类注册之前,类注册后就可以创建该类的对象了,而再添加成员变量就会破坏创建的对象的内存结构。
用到了 Runtime 获取某一个类的全部属性的名字,以及 Runtime 获取属性的类型。