Hello, World!

runtime(2)

字数统计: 2.1k阅读时长: 8 min
2019/11/02 Share

消息机制

Objective-C是运行时语言,消息机制是运行时机制中一个重要的组成部分。方法调用的本质就是发送消息。

实现方式

定义一个Person类,如下所示。

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
@interface Person : NSObject

@property (nonatomic, assign)NSUInteger age;

- (NSString *)sayHello;

- (NSString *)howOldAreYou:(NSString *)name;

+ (NSString *)howOldAreYou:(NSString *)name;

@end

@implementation Person

- (NSString *)sayHello {
return @"Thank you very much!";
}

- (NSString *)howOldAreYou:(NSString *)name {
return [NSString stringWithFormat:@"%@ 18 years old!", name];
}

+ (NSString *)howOldAreYou:(NSString *)name {
return [[[self alloc] init] howOldAreYou:name];
}

@end

向Person实例发送howOldAreYou消息的方式,有如下几种:

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
- (void)demo {
Person *person = [[Person alloc] init];
// 1.直接调用
NSString *rst1 = [person howOldAreYou:@"kobe"];
NSLog(@"直接调用 -> %@", rst1);
// 2.performSelector
NSString *rst2 = [person performSelector:@selector(howOldAreYou:) withObject:@"kobe"];
NSLog(@"performSelector -> %@", rst2);
// 3.方法签名 + NSInvocation
NSMethodSignature *signature = [person methodSignatureForSelector:@selector(howOldAreYou:)];
// 获取方法签名对应的invocation
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
/**
设置消息接受者,与[invocation setArgument:(__bridge void * _Nonnull)(person) atIndex:0]等价
*/
[invocation setTarget:person];
/**设置要执行的selector,与[invocation setArgument:@selector(howOldAreYou:) atIndex:1] 等价*/
[invocation setSelector:@selector(howOldAreYou:)];
// 设置参数
NSString *str = @"kobe";
[invocation setArgument:&str atIndex:2];
// 开始执行
[invocation invoke];
void *returnValue = NULL;
if (signature.methodReturnLength) {
[invocation getReturnValue:&returnValue];
}
id rst3 = (__bridge id)returnValue;
NSLog(@"方法签名 -> %@", rst3);
// 4.objc_msgSend
id rst4 = ((id(*)(id, SEL, NSString *))objc_msgSend)(person, @selector(howOldAreYou:), @"kobe");
NSLog(@"objc_msgSend -> %@", rst4);
}

该代码的关键之处主要有以下几点:
  * import runtime相关的头文件<objc/message.h>
  * 使用objc_msgSend函数来负责消息发送。
  
向Person类发送howOldAreYou消息的方式,有如下几种:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)demo {
Class clazz = [Person class];
// 1.直接调用
NSString *rst1 = [Person howOldAreYou:@"Mary"];
NSLog(@"直接调用 -> %@", rst1);
// 2.performSelector
NSString *rst2 = [clazz performSelector:@selector(howOldAreYou:) withObject:@"Mary"];
NSLog(@"performSelector -> %@", rst2);
// 3.objc_msgSend
id rst3 = ((id(*)(id, SEL, NSString *))objc_msgSend)(clazz, @selector(howOldAreYou:), @"Mary");
NSLog(@"objc_msgSend -> %@", rst3);
}

注意:使用objc_msgSend函数时,第一步需要#import <objc/message.h>。第二步需要关闭objc_msgSend函数的严格check(Build Setting -> 搜索msg -> 设置属性为No)。如果不关闭,就需要手动对objc_msgSend函数进行类型转换。

分析:[person howOldAreYou:@"kobe"],它的含义是:向person发送名为howOldAreYou:的消息。通过clang -rewrite-objc ViewController.m指令,将该句重写为C代码:((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("howOldAreYou:"));
可知底层是基于objc_msgSend的,去掉那些强制转换,最终[person howOldAreYou:@"kobe"]会由编译器转化为以下的纯C调用。
objc_msgSend(person, @selector(howOldAreYou:), @"kobe");,所以说,objc发送消息,最终大都会转换为objc_msgSend的方法调用。

消息发送的主要步骤:

    OC的方法调用最终会生成C函数objc_msgSend(person, @selector(howOldAreYou:), @"kobe")。这个C函数objc_msgSend就负责消息发送。
    消息发送的时候,在C语言函数中发生了什么事情?编译器是如何找到这个方法的呢?消息发送的主要步骤如下:
    1.首先检查这个选择器是不是要忽略。比如Mac OSX开发,有了垃圾回收就不会理会retain,release这些函数。
    2.检测这个选择器的target是不是nil,OC允许我们对一个nil对象执行任何方法不会Crash,因为运行时会被忽略掉。
    3.如果上面两步都通过了,根据实例的isa指针找到类对象,开始查找这个SEL的实现IMP,先从类对象的cache里查找,如果找到了就运行对应的函数去执行相应的代码。
    4.如果cache中没有找到就找类的methodLists中是否有对应的方法。
    5.如果类的方法列表中找不到就到父类的方法列表中查找,一直找到NSObject类为止。
    6.如果还是没找到就要开始进入动态方法解析和消息转发,后面会说。

应用场景

1.字典转模型

原理:遍历模型类的所有属性,然后用属性名作为key去字典中找对应的value,如果找不到不会crash。

关键代码:

1

2.KVO

原理:系统自动新建一个被监听类的子类,重写被监听属性的set方法,修改被监听对象的isa指针的值,让它指向子类对象。

关键代码:
使用objc_setAssociatedObject和objc_getAssociatedObject关联对象。
使用object_setClass函数修改对象isa指针。

1

消息转发

触发机制

在正常消息流程走完,没有找到SEL对应的IMP的时候,就会进入消息转发机制。

定义一个示例类:ForwardObject

1
2
3
4
@interface ForwardObject : NSObject
@end
@implementation ForwardObject
@end

调用ForwardObject中不存在的test实例方法。

1
2
3
4
5
6
- (void)viewDidLoad {
[super viewDidLoad];

ForwardObject *obj = [[ForwardObject alloc] init];
[obj performSelector:@selector(test)];
}

运行之后,系统报错'-[ForwardObject test]: unrecognized selector sent to instance 0x600002a9c800'

类似的报错都是iOS的消息转发机制在无法响应方法之后抛出的错误。

实现原理

当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发,具体动作如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#pragma mark - 消息转发机制的第一步(动态方法解析)
// 允许用户在此时为该Class动态添加实现。
// 如果有实现了,则调用并返回。如果仍没实现,继续下面的动作。
+ (BOOL)resolveInstanceMethod:(SEL)sel;

#pragma mark - 消息转发机制的第二步
// 尝试找到一个能响应该消息的对象。
// 如果获取到,则直接转发给它。如果返回了nil,继续下面的动作。
- (id)forwardingTargetForSelector:(SEL)aSelector;

#pragma mark - 消息转发机制的第三步
// 尝试获得一个方法签名。
// 如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

// 将获取到的方法签名包装成Invocation传入,如何处理就在这里面了。
- (void)forwardInvocation:(NSInvocation *)anInvocation;

上面这4个方法均是模板方法,开发者可以重写,由runtime来调用。以下是最常见的实现消息转发方式:

1.重写resolveInstanceMethod:方法,在此时为该Class动态添加实现。(动态方法解析)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if ([NSStringFromSelector(sel) isEqualToString:@"test"]) {
Method method = class_getInstanceMethod(self.class, @selector(dynamicAddMethod));
IMP methodIMP = method_getImplementation(method);
const char *methodType = method_getTypeEncoding(method);
class_addMethod(self.class, sel, methodIMP, methodType);
return YES;
}
return [super resolveInstanceMethod:sel];
}

- (void)dynamicAddMethod {
NSLog(@"动态添加的test方法");
}

log信息为:动态添加的test方法,由此可知,通过重写resolveInstanceMethod:方法,给Class动态添加test实现,完成了消息转发机制。

2,重写forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

@interface BackupForwardObject : NSObject
- (void)test;
@end
@implementation BackupForwardObject
- (void)test {
NSLog(@"备用类的test方法");
}
@end

- (id)forwardingTargetForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"test"]) {
return [[BackupForwardObject alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}

log信息为:备用类的test方法,由此可知,通过重写forwardingTargetForSelector:方法,找到一个能响应该消息的对象,完成消息转发机制。

3.重写methodSignatureForSelector:方法和forwardInvocation:方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (![super methodSignatureForSelector:aSelector]) {
NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
return methodSignature;
}
return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
BackupForwardObject *backUp = [[BackupForwardObject alloc] init];
SEL sel = anInvocation.selector;
// 判断备用对象是否可以响应传递进来等待响应的SEL
if ([backUp respondsToSelector:sel]) {
[anInvocation invokeWithTarget:backUp];
} else {
[super forwardInvocation:anInvocation];
}
}

log信息为:备用类的test方法,由此可知,吞掉一个消息或者代理给其他对象都是没问题的。

方式3跟方式2本质上是一样的都是变更接受消息的对象,但是方式3变更响应目标更复杂一些,方式2只需返回一个可以响应的对象即可,方式3还需要手动将响应方法切换给备用响应对象。

以上三种方式都可以实现消息转发,但是越往后面处理代价越高,最好的情况是在第一步就处理消息,这样runtime会在处理完后缓存结果,下回再发送同样消息的时候,可以提高处理效率。方式2转移消息的接受者也比进入转发流程的代价要小,如果到最后一步forwardInvocation的话,就需要处理完整的NSInvocation对象了。

应用场景

1.JSPatch –iOS动态化更新方案
Demo1
Demo2
2.实现多重代理
Demo

CATALOG
  1. 1. 消息机制
    1. 1.1. 实现方式
    2. 1.2. 应用场景
  2. 2. 消息转发
    1. 2.1. 触发机制
    2. 2.2. 实现原理
    3. 2.3. 应用场景