iOS Runtime详解
一、什么是Runtime?
我们都知道,从源代码到可执行文件需要经历三个阶段:编译
、链接
、运行
。
Objective-C
是一门动态语言,会尽可能的将决定性的工作从编译时和链接时推迟到运行时
,也就是说只有编译器是不够的,还需要一个运行时系统 (runtime system) 来执行编译后的代码。这就是 Objective-C Runtime 系统存在的意义,它是整个Objc运行框架的一块基石。
Runtime
简称运行时。OC就是运行时机制,其中最主要的是消息机制。对于C语言,函数的调用在编译的时候会决定调用哪个函数。对于OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数(事实证明,在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错。而C语言在编译阶段就会报错),只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。
二、Runtime源码
苹果和GNU各自维护一个开源的Runtime
版本,这两个版本之间都在努力的保持一致。
1.苹果公司Runtime开源代码
2.GNU Runtime开源代码
三、Runtime底层解析
我们首先来看下runtime
中对象(object)
、类(class)
、方法(method)
等都是这么定义的
1. 对象(object)
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
// 对象
struct objc_object {
// 对象的isa指针指向类对象
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
从上面源码中可以看到这里的 id
被定义为一个指向 objc_object
结构体 的指针。从中可以看出 objc_object 结构体
只包含一个 Class
类型的 isa
指针,而Class
是一个指向objc_class
结构体的指针。
由此可以得出对象的本质
是一个objc_object的结构体
,类的本质
是一个objc_class的结构体
2. 类(class)
// 类对象
struct objc_class {
// 类对象的isa指针指向元类对象
// 元类对象的isa指针指向的是根元类
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
// 指向父类的指针
Class _Nullable super_class OBJC2_UNAVAILABLE;
// 类的名称
const char * _Nonnull name OBJC2_UNAVAILABLE;
// 类的版本信息,默认为 0
long version OBJC2_UNAVAILABLE;
// 类的信息,供运行期使用的一些位标识
long info OBJC2_UNAVAILABLE;
// 该类的实例变量大小
long instance_size OBJC2_UNAVAILABLE;
// 该类的属性列表
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
// 该类的方法列表
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
// 该类的方法缓存
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
// 该类的协议列表
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
从上面源码可以看出objc_class
结构体定义了很多变量,其中包含了自身的所有实例变量(ivars)
、所有方法定义(methodLists)
、遵守的协议列表(protocols)
等。objc_class 结构体
存放的数据称为元数据(metadata)
。
objc_class的第一个成员变量是isa指针
,此isa
指针指向的是本身的元类(meta class)
。
3. 元类(meta class)
那么什么是元类
呢?
元类
是编译器在创建类的同时创建的一个虚拟的类,用来存储类对象的类方法等信息的类。
类和元类的关系就和实例对象和类的关系一样:类就是实例对象所属的类,元类就是类对象所属的类
。
元类
也是一个指向objc_class结构体
的指针,元类
的isa
指针指向的是根元类
。
4. 实例对象、类、元类的关系
下面用一张图来总结下这三者之间的关系
isa走位图 由图中可以看出:实例对象
中有个isa
指针,这个isa
指针指向实例对象
所在的类,类对象
中也有个isa
指针,这个isa
指针指向类对象
所在的元类,元类对象还有个isa
指针,这个isa
指针指向根元类
,根元类
中的isa
指针指向的是本身
。
5. 方法(method)
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;
// 方法
struct objc_method {
// 方法名称
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
// 方法类型
char * _Nullable method_types OBJC2_UNAVAILABLE;
// 方法实现
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}
其中method_name
和method_imp
分别是方法名称
和方法实现
,那么method_types
是什么呢?
method_types
是类型编码
,为了和运行时系统协作,编译器将方法的返回类型和参数类型都编码成一个字符串,并且和方法选标关联 在一起。method_types
的类型编码对照表如下:
四、消息传递
Objective-C
中方法的调用通常是这样的[obj run]
,编译器在编译时都会转化为objc_msgSend(obj, run)
进行消息发送;
如果obj
为实例对象则消息传递流程:
1.找到对象所在类
:通过obj
的isa
指针找到Class
类。
2.从缓存中查找
:从Class
类中的方法缓冲区cache
中查找方法(被调用过的方法都会存在方法缓冲区cache
中,以便下次更快的调用),如果没有找到则进入下一步
3.从方法列表中查找
:如果cache
中没有,则从methodLists
中查找。如果没找到则进入下一步。
4.通过继承链查找
:通过Class
的继承链找到父类直到根类NSObject
,每次重复2,3步
,如果还找不到则进入下一步。
5.动态方法解析
:调用 + (BOOL)resolveInstanceMethod:(SEL)sel
方法来查看是否能够返回一个selector
,如果存在则返回selector
。不存在进入下一步。
6.备用接收者
:- (id)forwardingTargetForSelector:(SEL)aSelector
这个方法来询问是否有接收者
可以接收这个方法。如果有接收者
,则交给它处理,否则进入下一步。
7.消息的转发
:如果到这一步还不能够找到相应的selector
的话,就要进行完整的方法转发过程。调用方法(void)forwardInvocation:(NSInvocation *)anInvocation
,如果这里还没有处理则会进入下一步。
8.奔溃
:最后还是没有找到的话就只有呵呵了,这时候unrecognized selector sent to instance 0x100111df0
的错误就来了。
动态方法解析
在上面方法传递过程中如果一直没找到方法会进入动态消息解析
过程,在此过程中可以动态的添加方法实现。如果你添加了方法实现, 那运行时系统就会重新启动一次消息发送的过程。
动态方法解析主要在+ (BOOL)resolveInstanceMethod:(SEL)sel
和+ (BOOL)resolveClassMethod:(SEL)sel
这两个方法中进行,通过例子我们来了解一下
@interface ViewController ()
// 声明run方法
- (void)run;
+ (void)walk;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 调用run方法,但run方法并未被实现
[self run];
[ViewController walk];
}
// 对象方法未找到时调起此方法,可以再次方法中添加方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel{
// 如果没有实现run方法
if (sel == @selector(run)) {
/**
* 可以在此添加一个方法实现
* @param cls 被添加方法的类
* @param name selector 方法名
* @param imp 实现方法的函数指针
* @param types imp 指向函数的返回值与参数类型
* @return 如果添加方法成功返回 YES,否则返回 NO
*/
return class_addMethod(self, sel, (IMP)runImp, "v@:");
}else if (sel == @selector(walk)) {
return class_addMethod(self, sel, (IMP)walkImp, "v@:");
}
return [super resolveInstanceMethod:sel];
}
// 类方法未找到时调起此方法,可以再次方法中添加方法实现
+ (BOOL)resolveClassMethod:(SEL)sel{
// 如果没有实现run方法
if (sel == @selector(walk)) {
/**
* 可以在此添加一个方法实现
* @param cls 被添加方法的类的元类。⚠️这是元类
* @param name selector 方法名
* @param imp 实现方法的函数指针
* @param types imp 指向函数的返回值与参数类型
* @return 如果添加方法成功返回 YES,否则返回 NO
*/
return class_addMethod(objc_getMetaClass(object_getClassName(self)), sel, (IMP)walkImp, "v@:");;
}
return [super resolveClassMethod:sel];
}
// 方法实现
void runImp(id obj, SEL sel){
NSLog(@"实例方法实现 %s",__func__);
}
// 方法实现
void walkImp(id obj, SEL sel){
NSLog(@"类方法实现 %s",__func__);
}
@end
这是打印的信息
2020-09-02 15:55:13.694867+0800 RuntimeDemo[5899:162375] 实例方法实现 runImp
2020-09-02 15:55:13.695411+0800 RuntimeDemo[5899:162375] 类方法实现 walkImp
备用接收者
如果在动态消息转发过程
中没有添加方法的实现,那么此时Runtime
就会调用- (id)forwardingTargetForSelector:(SEL)aSelector
这个方法来返回一个备用接收者
,然后由这个备用接收者
来实现这个方法。下面通过一个例子我们来了解一下
@interface Person : NSObject
@end
@implementation Person
- (void)run{
NSLog(@"%s",__func__);
}
+ (void)walk{
NSLog(@"%s",__func__);
}
@end
@interface ViewController ()
// 声明run方法
- (void)run;
+ (void)walk;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 调用run方法,但run方法并未被实现
[self run];
[ViewController walk];
}
// 返回一个备用接收者
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"instance method : %@", NSStringFromSelector(aSelector));
if (aSelector == @selector(run)) {
return [[Person alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
+ (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"class method : %@", NSStringFromSelector(aSelector));
if (aSelector == @selector(walk)) {
return [Person class];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
下面是此次运行打印的结果
2020-09-02 18:07:40.491838+0800 RuntimeDemo[6821:230239] instance method : run
2020-09-02 18:07:40.492687+0800 RuntimeDemo[6821:230239] -[Person run]
2020-09-02 18:07:40.493125+0800 RuntimeDemo[6821:230239] class method : walk
2020-09-02 18:07:40.493510+0800 RuntimeDemo[6821:230239] +[Person walk]
可以看到虽然ViewController
没有实现这两个方法,动态方法解析
也没有添加这个两个方法实现,但是我们通过 forwardingTargetForSelector
把当前 ViewController
的方法转发给了 Person
对象去执行了。打印结果也证明我们成功实现了转发。
我们通过forwardingTargetForSelector
可以修改消息的接收者,该方法返回参数是一个对象,如果这个对象是不是 nil,也不是 self,系统会将运行的消息转发给这个对象执行。否则,继续进行下一步:消息转发(重定向)流程
。
消息转发(重定向)
如果经过前面两步Runtime
系统还是找不到相应的方法实现而无法响应消息,那么就会进入消息转发流程:
首先它会发送-methodSignatureForSelector:
消息获得函数的参数和返回值类型。如果 methodSignatureForSelector:
返回了一个 NSMethodSignature
对象(函数签名),Runtime
系统就会创建一个 NSInvocation
对象,并通过 forwardInvocation:
消息通知当前对象,给予此次消息发送最后一次寻找 IMP
的机会。如果 methodSignatureForSelector:
返回 nil。则 Runtime
系统会发出doesNotRecognizeSelector:
消息,程序也就崩溃了。
下面我们通过一个例子来了解一下
@interface Person : NSObject
@end
@implementation Person
- (void)run{
NSLog(@"%s",__func__);
}
+ (void)walk{
NSLog(@"%s",__func__);
}
- (void)run:(NSString *)type{
NSLog(@"%s %@",__func__, type);
}
@end
@interface ViewController ()
// 声明run方法
- (void)run;
- (void)run:(NSString *)type;
+ (void)walk;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 调用run方法,但run方法并未被实现
[self run];
[self run:@"slowly"];
[ViewController walk];
}
// 获取方法函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == @selector(run)) {
//签名,进入forwardInvocation
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}else if (aSelector == @selector(run:)) {
//签名,进入forwardInvocation
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
return [super methodSignatureForSelector:aSelector];
}
// 消息转发(重定向)
- (void)forwardInvocation:(NSInvocation *)anInvocation{
SEL sel = anInvocation.selector;
NSLog(@"- forwardInvocation %@", NSStringFromSelector(sel));
Person *p = [[Person alloc] init];
// 第一种方式 调用时候传的是什么参数就是什么参数
if ([p respondsToSelector:sel]) {
[anInvocation invokeWithTarget:p];
}else {
// 若仍然无法响应,则报错:找不到响应方法
[self doesNotRecognizeSelector:sel];
}
// // 第二种方式 可以自定义传参
// NSMethodSignature *signature = [p methodSignatureForSelector:sel];
// NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
// invocation.target = p;
// invocation.selector = sel;
// if (sel == @selector(run:)) {
// NSString *runType = @"fast";
// //注意:设置参数的索引时不能从0开始,因为0已经被self占用,1已经被_cmd占用
// [invocation setArgument:&runType atIndex:2];
// }
// [invocation invoke];
}
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == @selector(walk)) {
//签名,进入forwardInvocation
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation{
SEL sel = anInvocation.selector;
NSLog(@"+ forwardInvocation %@", NSStringFromSelector(sel));
if ([Person respondsToSelector:sel]) {
[anInvocation invokeWithTarget:objc_getClass(object_getClassName([Person class]))];
}else {
// 若仍然无法响应,则报错:找不到响应方法
[self doesNotRecognizeSelector:sel];
}
}
消息转发的实现有两种方式
,第一种
调用时候传的是什么参数转发的就是什么参数,第二种
可以自定义参数值,你想要什么参数就传什么参数。让我们来看下两种方式的打印结果
第一种方式
2020-09-06 11:36:44.051691+0800 RuntimeDemo[1377:38366] - forwardInvocation run
2020-09-06 11:36:44.052208+0800 RuntimeDemo[1377:38366] -[Person run]
2020-09-06 11:36:44.052624+0800 RuntimeDemo[1377:38366] - forwardInvocation run:
2020-09-06 11:36:44.052965+0800 RuntimeDemo[1377:38366] -[Person run:] slowly
2020-09-06 11:36:44.053331+0800 RuntimeDemo[1377:38366] + forwardInvocation walk
2020-09-06 11:36:44.053691+0800 RuntimeDemo[1377:38366] +[Person walk]
可以看到第四行这里打印的是slowly。
第二种方式
2020-09-06 11:43:33.036825+0800 RuntimeDemo[1404:40952] - forwardInvocation run
2020-09-06 11:43:33.037358+0800 RuntimeDemo[1404:40952] -[Person run]
2020-09-06 11:43:33.037811+0800 RuntimeDemo[1404:40952] - forwardInvocation run:
2020-09-06 11:43:33.038203+0800 RuntimeDemo[1404:40952] -[Person run:] fast
2020-09-06 11:43:33.039525+0800 RuntimeDemo[1404:40952] + forwardInvocation walk
2020-09-06 11:43:33.040117+0800 RuntimeDemo[1404:40952] +[Person walk]
可以看到第四行这里打印的是fast。
所以,可以根据实际开发中的需求来确定使用哪种方式。