Objective-C 的方法调用被苹果成为”发消息”. objc的消息机制是由运行时实现、非常灵活动态。这篇文章简单记录一下objc运行时对于消息发送和转发的实现. 本文Demo

编译器转换

1
[cat mew];

意为cat对象调用mew方法, 也就是向cat对象发送mew消息, 实际上编译器会将这行代码转换为

1
objc_msgSend(cat, @selector(mew));

验证如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#import <objc/message.h>
@interface Cat : NSObject
@property (nonatomic, copy) NSString *name;
- (void)mew;
@end
@implementation Cat
- (void)mew {
NSLog(@"喵~");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Cat *cat = Cat.new;
[cat mew];
objc_msgSend(cat, @selector(mew));
}
return 0;
}

结果是输出两次:

1
2
喵~
喵~

当然我们也可以执行 clang -rewrite-objc main.m 将oc转换为C++代码来查看.

objc_msgSend

文档中这样写:

1
id objc_msgSend(id self, SEL _cmd, ...)

将一个消息发送给一个对象,并且返回一个值。
其中,self是消息的接受者,_cmd是selector, …是可变参数列表。

为了了解objc_msgSend方法做了什么,这里需要查看一下objc runtime的源码
首先 runtime定义了如下的数据类型:

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
typedef struct objc_class *Class;
typedef struct objc_object *id;
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
struct objc_class : objc_object {
Class superclass;
cache_t cache; // 方法缓存
class_data_bits_t bits; // 类具体的信息
class_rw_t *data() {
return bits.data();
}
}
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods; // 方法列表
property_array_t properties; // 属性列表
protocol_array_t protocols; // 协议列表
...
};
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize; // 对象占用空间
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name; // 类名
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars; // 成员变量列表
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
typedef struct objc_selector *SEL;
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);

id指代objc中的对象,每个对象的在内存的结构并不是确定的,但其首地址指向的肯定是isa。通过isa指针,运行时就能获取到objc_class。

objc_class表示对象的Class,它的结构是确定的,由编译器生成。

SEL表示选择器,这是一个不透明结构体。但是实际上,通常可以把它理解为一个字符串。例如printf(“%s”,@selector(isEqual:))会打印出”isEqual:”。运行时维护着一张SEL的表,将相同字符串的方法名映射到唯一一个SEL。 通过sel_registerName(char *name)方法,可以查找到这张表中方法名对应的SEL。苹果提供了一个语法糖@selector用来方便地调用该函数。

IMP是一个函数指针。objc中的方法最终会被转换成纯C的函数,IMP就是为了表示这些函数的地址。

那么objc_msgSend究竟做了什么呢? 这个方法是由汇编实现得, 用伪代码大概可以表示为:

1
2
3
4
5
id objc_msgSend(id self, SEL op, ...) {
if (!self) return nil;
IMP imp = class_getMethodImplementation(self->isa, SEL op);
imp(self, op, ...); //调用这个函数,伪代码...
}

另外在objc-msg-arm64.s 中找到如下关键代码

1
2
3
.macro MethodTableLookup
...
bl __class_lookupMethodAndLoadCache3

以及objc-runtime-new.mm :

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
...
// Try this class's cache.
imp = cache_getImp(cls, sel);
if (imp) goto done;
// Try this class's method lists.
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
// Try superclass caches and method lists.
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
}
// Superclass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
// No implementation found. Try method resolver once.
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
goto retry;
}
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
}

[cat mew];为例, 总结一下就是:

  1. 通过cat的isa指针找到它的class, 也就是Cat类
  2. 在Cat的cache列表中查找mew
  3. 在Cat的method列表中查找mew
  4. 在父类的cache和method列表中查找mew
  5. 在2-4的过程中, 一旦找到就将该方法添加到缓存列表并执行该方法
  6. 没有找到任何的方法实现, Try method resolver once, 也就是我们说的resolveInstanceMethod方法
  7. resolveInstanceMethod没实现, 就进入转发机制

另需注意: 如果是类方法的调用, 则是去Cat的元类中查找, 因为类方法都是保存在meta class中, 依次向父元类查找, 注意, 到NSObject meta class, 它的父类是NSObject本类, 如果这个时候有一个同名的实例方法, 也可以调用. 例:

1
2
3
4
5
6
7
8
9
10
11
@interface NSObject (Sark)
+ (void)foo;
@end
@implementation NSObject (Sark)
- (void)foo {
NSLog(@"IMP: - [NSObject (sark) foo]");
}
@end
[NSObject foo]; // 打印 IMP: - [NSObject (sark) foo]

消息转发

首先我们先来验证一下, 调用一个不存在的方法, 在运行时发了哪些消息

1
2
Cat *cat = Cat.new;
[cat performSelector:@selector(lalala)];

具体方法如下:

  1. 断点暂停后执行call (void)instrumentObjcMessageSends(YES)

  2. 然后过掉断点, 程序崩溃
1
2
3
4
5
6
7
8
9
10
11
12
13
2018-03-01 17:37:05.671634+0800 msg_send[18226:862391] -[Cat lalala]: unrecognized selector sent to instance 0x10054d9e0
2018-03-01 17:37:05.676829+0800 msg_send[18226:862391] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Cat lalala]: unrecognized selector sent to instance 0x10054d9e0'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff4eabc54b __exceptionPreprocess + 171
1 libobjc.A.dylib 0x00007fff7658bc76 objc_exception_throw + 48
2 CoreFoundation 0x00007fff4eb55024 -[NSObject(NSObject) doesNotRecognizeSelector:] + 132
3 CoreFoundation 0x00007fff4ea32a90 ___forwarding___ + 1456
4 CoreFoundation 0x00007fff4ea32458 _CF_forwarding_prep_0 + 120
5 msg_send 0x0000000100001c76 main + 86
6 libdyld.dylib 0x00007fff771a5015 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

3.在Terminal中输入:

1
open /private/tmp


4.打开查看

1
2
3
4
5
6
7
8
9
10
11
- Cat NSObject performSelector:
+ Cat NSObject resolveInstanceMethod:
+ Cat NSObject resolveInstanceMethod:
- Cat NSObject forwardingTargetForSelector:
- Cat NSObject forwardingTargetForSelector:
- Cat NSObject methodSignatureForSelector:
- Cat NSObject methodSignatureForSelector:
- Cat NSObject class
- Cat NSObject doesNotRecognizeSelector:
- Cat NSObject doesNotRecognizeSelector:
- Cat NSObject class

结合 NSObject官方文档, 转发机制可如图表示:

1.调用resolveInstanceMethod:方法,允许用户在此时为该Class动态添加实现。如果有实现了,则调用并返回。如果仍没实现,继续下面的动作。

2.调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接转发给它。如果返回了nil,继续下面的动作。

3.调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。

4.调用forwardInvocation:方法,将地3步获取到的方法签名包装成Invocation传入,如何处理就在这里面了。

上面这4个方法均是模板方法,开发者可以override,由runtime来调用。最常见的实现消息转发,就是重写方法3和4,吞掉一个消息或者代理给其他对象都是没问题的。

示例: 我们为Dog类添加name属性和实例, 但通过@dynamic使其不自动生成setter和getter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@interface Dog : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Dog {
NSString *_name;
}
@dynamic name;
int main(int argc, const char * argv[]) {
@autoreleasepool {
Dog *dog = [Dog new];
dog.name = @"Kiki";
NSLog(@"%@", dog.name);
}
return 0;
}

此时dog对象没有setName: 方法, 会尝试解决, 我们来实现resolveInstanceMethod 方法, 并为name添加setter 和 getter (他们的内部实现可以用指针指向, 也可以用运行时api)

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
@implementation Dog {
NSString *_name;
}
@dynamic name;
void mySetName(id self, SEL _cmd, NSString *newValue) {
// Ivar ivar = class_getInstanceVariable(Dog.class, "_name");
// object_setIvar(self, ivar, [newValue copy]);
if (((Dog *)self)->_name != newValue) {
((Dog *)self)->_name = [newValue copy];
}
}
NSString * myGetName(id self, SEL _cmd) {
// Ivar ivar = class_getInstanceVariable(Dog.class, "_name");
// return object_getIvar(self, ivar);
return ((Dog *)self)->_name;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"%s", __func__);
if ([NSStringFromSelector(sel) isEqualToString:@"setName:"]) {
class_addMethod(self, sel, (IMP)mySetName, "v@:@");
}
else {
class_addMethod(self, sel, (IMP)myGetName, "@@:");
}
return YES;
}
@end

如果不实现上述方法, 也可以转发给别的对象:

1
2
3
4
5
6
7
8
9
10
@implementation Dog {
NSString *_name;
}
@dynamic name;
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s", __func__);
return Cat.new;
}
@end

该消息转发给Cat类的一个对象, 去调用Cat的setName:方法, 当然这没什么实际的意义.

第三次补救:

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
@implementation Dog {
NSString *_name;
}
@dynamic name;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%s", __func__);
NSString *selStr = NSStringFromSelector(aSelector);
if([selStr isEqualToString:@"name"]) {
NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"@@:"];
return sig;
}
else {
NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
return sig;
}
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"%s", __func__);
SEL sel = [anInvocation selector];
Cat *cat = [[Cat alloc] init];
if([cat respondsToSelector:sel]) {
[anInvocation invokeWithTarget:cat];
}
else {
[self doesNotRecognizeSelector:sel];
}
}
@end

首先生成方法签名, 然后转发调用, 如果没有实现forward, 那直接调用doesNotRecognizeSelector, 抛出exception, 也可以不使用doesNotRecognizeSelector, 这样就吞没了这个消息.

__原创文章, 转载请注明出处