【iOS】Runtime详解
 visitors

做了很久的iOS开发了,但依然还是没有将一些基础的知识弄清楚,想要真正的掌握一门技术或则语言,真的不能一知半解,就像你说你熟练掌握了iOS的开发,但是如果别人问你什么是Runtime,它的原理是什么,如果这你都不知道真的算不上对iOS已经熟练掌握了。以前一直有一个误区,拿到一门语言或则技术直接就开始写东西了,但是对很多的原理都是一知半解,以致于忽略了很多基本知识,这篇笔记我要将我丢掉runtime的一些知识都捡起来。

Runtime简介

Objective-C是一门动态语言,它将很多静态语言在编译和链接时期做的事情放到了运行时来处理。对于Objective-C来说,这个Runtime就像是一个操作系统一样,它让所有的工作可以正常运行。Runtime简称运行时。Objective-C就是运行时机制,也就是在运行时的一些机制,最主要的就是消息机制。

  • 对于C语言,函数的调用在编译的时候会决定调用那个函数。
  • 对于Objective-C的函数,属于动态调用过程,在编译的时候并不能真正的决定调用哪个函数,只有真正运行的时候才会根据函数的名称找到对应的函数来调用。

Runtime消息传递

一个对象的方法像这样[obj foo],编译器转成消息发送objc_msgSend(obj, foo),Runtime时的执行流程是这样的:

  • 首先,通过它的objisa指针找到它的class
  • classmethod_list中找到foo方法;
  • 如果class中没有找到foo,就继续往它的super_class中找;
  • 一旦找到foo这个函数,就去执行它的实现IMP(如果还是找不到就会报unrecognized selector的错)。

类对象(objc_class)

Objective-C类是由Class类型来表示,它实际上是指向objc_class结构体的一个指针。

1
typedef struct objc_class *Class

查看objc/runtime.h文件中objc_class结构体的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 类
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE; // 父类
const char * _Nonnull name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
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;

// 方法
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;

// 方法列表
struct objc_method_list {
struct objc_method_list * _Nullable obsolete OBJC2_UNAVAILABLE;

int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;

实例(objc_object)

objc_object是表示一个类的实例的结构体,在objc/objc.h文件中定义如下:

1
2
3
4
5
6
7
typedef struct objc_class *Class;

struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};

typedef struct objc_object *id;

可以看到这个结构体只有一个字段,及指向其类的isa指针。这样当我们向一个Objective-C对象发送消息时,Runtime库会根据实例对象的isa指针找到这个实例对象所属的类。Runtime库会在类方法列表以及父类的方法列表中去寻找消息对应的selector指向的方法,找到后即运行这个方法。

元类(Meta Class)

类对象中的元数据存储的是如何创建一个实例的相关信息,类对象和类方法都应该从哪里创建呢?就是从isa指针指向的结构体创建,类对象的isa指针指向的我们称之为元类(Meta Class),元类中保存了创建类对象以及类方法所需的所有信息。因此整个结构应该如下图所示:

通过上图我们可以看出整个体系构成了一个自闭环,struct objc_object结构体实例它的isa指针指向类对象,类对象的isa指向了元类,super_class指向了父类的类对象,而元类的super_class指向了父类的元类,那元类的isa又指向了自己。

元类(Meta Class)是一个类对象的类。所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用方法)。为了调用方法,这个类的isa指针必须指向一个包含类方法的一个objc_class结构体。这就引入了Meta Class概念,元类中保存了创建类对象以及类方法需要的所有信息。任何NSObject集成体系下的meta-class都使用NSObjectmeta-class作为自己的所属类,而基类的meta-class的isa指向它自己。

Method(objc_method)

objc/runtime.h中的定义如下:

1
2
3
4
5
6
// 方法
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE; // 方法名
char * _Nullable method_types OBJC2_UNAVAILABLE; // 方法类型
IMP _Nonnull method_imp OBJC2_UNAVAILABLE; // 方法实现
}

Method和我们平时理解的函数是一致的,就是表示能够独立完成一个功能的一段代码,比如:

1
2
3
4
- (void)logName
{
NSLog(@"name");
}

上面这段代码就是一个函数。

objc_method的结构体中,看到了SELIMP,说明SELIMP其实都是Method的属性。

SEL(objc_selector)

objc/objc.h中的定义为:

1
2
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

objc_msgSend函数第二个参数类型为SEL,它是selectorObjective-C中的表示类型。selector是方法选择器,可以理解为区分方法的ID,而这个ID的数据结构是SEL

1
@property SEL selector;

可以看到selectorSEL的一个实例。

其实selector就是映射到方法的C字符串,你可以用Objective-C编译器命令@selector()或则Runtime系统的sel_registerName函数来获得一个SEL类型的方法选择器。

selector既然是一个string,我觉得应该是类似于className+MethodName的组合,命名规则有两条:

  • 同一个类,selector不能重复
  • 不同的类,selector可以重复

所以在Objective-C中如下的代码会报错:

1
2
- (void)caculate(NSInteger)num;
- (void)caculate(CGFloat)num;

只能通过方法名来进行区别:

1
2
- (void)caculateWithInt(NSInteger)num;
- (void)caculateWithFloat(CGFloat)num;

IMP

objc/objc.h中IMP的定义如下:

1
2
3
4
5
6
/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif

就是指向最终实现程序函数的内存地址的指针。

iOSRuntime中,Method通过SELIMP两个属性,实现了快速方法的查询以及实现,相对提高了性能又保持了灵活性。

类缓存(objc_cache)

当Objective-C运行时通过跟踪它的isa指针检查对象时,它可以找到一个实现多个方法的对象。然而你只调用其中的以一小部分,并且每次检查时,搜索所有选择器的分派表没有意义。所以实现一个缓存,每当你搜索一个类分派表,并找到相应的选择器,它把它放入缓存。所以当objc_msgSend查找一个类的选择器,它首先搜索缓存。

为了加速消息分发,系统会对方法和对应的地址进行缓存,就放在上面所述的objc_cache,所以在实际运行中,大部分常用的方法都会被缓存起来,Runtime系统实际上非常快,接近于直接执行内存地址程序的速度。

Category(objc_category)

obj/runtime.hobjc_category的定义如下:

1
2
3
4
5
6
7
struct objc_category {
char * _Nonnull category_name OBJC2_UNAVAILABLE; // 分类名
char * _Nonnull class_name OBJC2_UNAVAILABLE; // 分类所属的类名
struct objc_method_list * _Nullable instance_methods OBJC2_UNAVAILABLE; // 实例方法列表
struct objc_method_list * _Nullable class_methods OBJC2_UNAVAILABLE; // 类方法列表
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE; // 分类所现实的协议列表
}

从上面的objc_category的结构体可以看出,分类中可以添加实例方法、类方法,甚至可以实现协议,不能添加实例变量和属性。

Runtime消息转发

上面Runtime消息传递中已经介绍了一次发送消息会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索直到继承树根部(通常为NSObject),如果还是找不到并且消息转发都失败了就会执行doesNotRecognizeSelector:方法报unrecognized selector错。

因此对于对象尝试调用未实现的方法会报错,遇到这种情况会不会有什么“补救措施”,当然有,这就需要了解消息的转发机制。

当没有找到实现方法时,会调用一下函数:

  • 动态方法解析

    1
    2
    +(BOOL)resolveInstanceMethod:(SEL)sel
    +(BOOL)resolveClassMethod:(SEL)sel
  • 备用接受者

    1
    -(id)forwardingTargetForSelector:(SEL)aSelector
  • 完整地消息转发

    1
    2
    -(NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector
    -(void)forwardInvocation:(NSInvocation *)anInvocation

消息转发流程简图:

动态解析方法

首先会调用动态方法的解析方法,我们可以尝试在+(BOOL)resolveInstanceMethod:(SEL)selector(针对实例方法)和+(BOOL)resolveClassMethod:(SEL)sel(针对类方法)中添加实现方法。

实现一个动态方法解析的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//执行eat函数
[self performSelector:@selector(eat:)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(eat:)) {//如果是执行eat函数,就动态解析,指定新的IMP
class_addMethod([self class], sel, (IMP)eatMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}

void eatMethod(id obj, SEL _cmd) {
NSLog(@"Person eat");//新的eat函数
}

2018-08-08 15:54:30.652862+0800 Runtime[32473:3482683] Person eat

从上面的例子可以看到虽然没有实现eat:这个函数,但是通过class_addMethod动态添加eatMethod函数,并执行eatMethod这个函数的IMP

如果+ (BOOL)resolveInstanceMethod:(SEL)sel+(BOOL)resolveClassMethod:(SEL)sel方法没有处理eat:方法,运行时就会移到下一步:- (id)forwardingTargetForSelector:(SEL)aSelector

备用接受者

如果目标对象实现了- (id)forwardingTargetForSelector:(SEL)aSelector,那么运行时就会调用这个方法,把这个消息转发给其他对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#import "ViewController.h"
#import "objc/runtime.h"

@interface Person: NSObject

@end

@implementation Person
- (void)eat {
NSLog(@"forwardingTargetForSelector Person eat");
}
@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//执行eat函数
[self performSelector:@selector(eat)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO; // 这里不管返回是YES还是NO都会进入forwardingTargetForSelector
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(eat)) {
return [Person new];
}
return [super forwardingTargetForSelector:aSelector];
}

void eatMethod(id obj, SEL _cmd) {
NSLog(@"resolveInstanceMethod Person eat");//新的eat函数
}

@end

打印结果:

2018-08-08 16:14:54.714890+0800 Runtime[35945:3529505] forwardingTargetForSelector Person eat

从上面的例子我们可以看到通过forwardingTargetForSelector把当前ViewController的方法传给了Person去执行了。

完整消息转发

如果上面两部步都无法处理未知消息,那么唯一能做的就是启用完整消息转发机制了。首先它会发送- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector消息获得函数的参数和返回值类型。如果- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector返回nilRuntime则会发-doesNotRecognizeSelector:消息,程序也会挂掉。如果返回的了一个函数签名,Runtime就会创建一个NSInvocation对象并发送- (void)forwardInvocation:(NSInvocation *)anInvocation消息给目标对象。

实现的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#import "ViewController.h"
#import "objc/runtime.h"

@interface Person: NSObject

@end

@implementation Person
- (void)eat {
NSLog(@"完整消息转发 Person eat");
}
@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//执行eat函数
[self performSelector:@selector(eat)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO; // 这里不管返回是YES还是NO都会进入forwardingTargetForSelector
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"eat"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector;
Person *p = [Person new];
if ([p respondsToSelector:sel]) {
[anInvocation invokeWithTarget:p];
} else {
[self doesNotRecognizeSelector:sel];
}
}

void eatMethod(id obj, SEL _cmd) {
NSLog(@"resolveInstanceMethod Person eat");//新的eat函数
}

@end

打印结果:

2018-08-08 16:38:29.076233+0800 Runtime[39848:3579675] 完整消息转发 Person eat

从打印结果来看,我们实现了完整的消息转发。通过签名,Runtime生成了一个对象(NSInvocation *)anInvocation发送给forwardInvocation方法,我们在forwardInvocation方法中让Person对象去执行eat函数。

关于签名参数v@:的解释,在苹果官方文档Type Encoding中有详细的解释。