0%

深入了解Objective-C消息发送与转发过程

在 Objective-C 语言中,对象/类(其实类也是一个对象) 执行方法最后会转化成给对象发送消息:

objc_msgSend(receiver, @selector(message))

如果 reveiver 中没有找到对应方法 message, 则会开始消息转发的过程,也就是过程:

  1. 动态方法解析 Method Resolution
  2. 快速转发 Fast Rorwarding
  3. 完整消息转发 Normal Forwarding

接下来通过OC的源码来分析以上几个步骤具体的调用过程

消息发送

当在 OC 中给对象执行方法,如 [object foo],会被翻译为 objc_msgSend(object, @selector(foo))@seletor 会将 foo 方法生成对应的选择子(SEL),选择子只跟方法名有关系,不同的类之间可以存在相同的方法选择子,但是同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也不行,也就是说 OC 中不支持像 C++ 那样的函数重载。

当编译器遇到一个方法调用时,它会将方法的调用翻译成以下函数中的一个 objc_msgSendobjc_msgSend_stretobjc_msgSendSuper objc_msgSendSuper_stret。 发送给对象的父类的消息会使用 objc_msgSendSuper 有数据结构作为返回值的方法会使用 objc_msgSendSuper_stret objc_msgSend_stret 其它的消息都是使用 objc_msgSend 发送的

objc_msgSend 的具体实现是由汇编语言编写的,其中具体过程细节可以参考我另一篇文章objc-msg-arm64源码深入分析

objc_msgSend 函数执行过程中,如果根据 SEL 在接受者(object)方法列表的 cache 缓存中没有查找到对应的方法 IMP,会执行 C 语言函数 __class_lookupMethodAndLoadCache3

1
2
3
4
5
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

这个方法只允许被 _objc_msgSend 内部调度,其他方式应该使用 lookUpImp 此函数将忽略缓存查询,因为执行此函数之前能确保已经查询过对应的内存

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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
bool initialize, bool cache, bool resolver)
{
Class curClass;
IMP imp = nil;
Method meth;
bool triedResolver = NO;

runtimeLock.assertUnlocked();

// Optimistic cache lookup
// 这里传入cache==false,因为objc_msgSend汇编阶段已经查找过缓存,故直接跳过
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}

// 实现对应的类,设置父类、元类等等相关信息,分配可读写结构体 class_rw_t 的空间
if (!cls->isRealized()) {
rwlock_writer_t lock(runtimeLock);
realizeClass(cls);
}

// 判断类别是否已经初始化过,初始化过程会触发+initialize
if (initialize && !cls->isInitialized()) {
_class_initialize (_class_getNonMetaClass(cls, inst));
}

// 这里加锁是因为OC在运行时能动态添加方法,
// 比方说分类 category 添加方法是在运行时期添加
// 如果此时不添加锁进行原子读操作,很可能因为新方法添加导致缓存被冲洗(flush)
retry:
runtimeLock.read();//加读锁

// 支持GC的环境需要对一些方法进行忽略,比如retain、release...等等
if (ignoreSelector(sel)) {
imp = _objc_ignored_method;
cache_fill(cls, sel, imp, inst);
goto done;
}

// Try this class's cache.
// 再次查询缓存
// TODO: 这里为什么会再次查询缓存列表?一开始cache==NO直接忽略了缓存查询,为什么加锁之后却要重新从缓存查询
// 结合加锁的逻辑,是否因为调度的时候是并列的,但是读的时候是原子,很可能加锁之后因为上一次查找过程中重新更新了方法列表缓存?
imp = cache_getImp(cls, sel);
if (imp) goto done;

// Try this class's method lists.
// 缓存没有查到,到方法列表中查询
meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
// 查到就更新缓存列表
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}

// 开始在父类中进行查找
curClass = cls;
while ((curClass = curClass->superclass)) {
// 从父类缓存中查询
imp = cache_getImp(curClass, sel);
if (imp) {
// 如果是 _objc_msgForward_impcache 则不进行缓存
if (imp != (IMP)_objc_msgForward_impcache) {
// 在父类查询到也存在本类的缓存中
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
// 如果查找到的 IMP 为 _objc_msgForward_impcache 直接结束查找
// 并执行 -resolveInstanceMethod: / +resolveClassMethod:
break;
}
}

// 从父类方法列表中查
meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}

// 父类中也没有找到方法
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
// 进行 -resolveInstanceMethod: / +resolveClassMethod: 动态添加方法
_class_resolveMethod(cls, sel, inst);
// 动态实现完了之后,因为之前锁已经解锁,方法列表可能已经更新了,所以会从新进行一轮方法查找
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);

done:
runtimeLock.unlockRead();

assert(!(ignoreSelector(sel) && imp != (IMP)&_objc_ignored_method));

assert(imp != _objc_msgSend_uncached_impcache);

return imp;
}

整个方法查找的过程,可以简单的概括为以下几个步骤

  1. 实现、初始化对应的类
  2. 根据是否支持垃圾回收机制(GC)判断是否忽略当前的方法调用
  3. 从cache中查找方法
  4. cache中没有找到对应的方法,则到方法列表中查,查到则缓存
  5. 如果本类中查询到没有结果,则遍历所有父类重复上面的查找过程
  6. 最后都没有找到的方法的话,则执行 _class_resolveMethod 让调用者动态添加方法,并重复一轮查询方法的过程
  7. 若第六步没有完成动态添加方法,则把 _objc_msgForward_impcache 作为对应 SEL 的方法进行缓存,然后调用 _objc_msgForward_impcache 方法

动态方法解析

消息发送的过程中,如果没有找到先进行 _class_resolveMethod 允许开发者动态的根据 SEL 实现对应的 IMP,实现前先执行 runtimeLock.unlockRead() 打开了读锁,所以开发者在此动态实现的过程添加了方法实现,故不需要缓存方法;

_class_resolveMethod 调用过程又是非原子性的,执行完的时候方法列表可能已经更新了,所以执行完了之后需要重复一轮查询方法的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}

如果 cls 不是元类,则执行 _class_resolveInstanceMethod 函数;否则 cls 属于元类则会调用 _class_resolveClassMethod ,然后执行 lookUpImpOrNil

1
2
3
4
5
6
7
IMP lookUpImpOrNil(Class cls, SEL sel, id inst, 
bool initialize, bool cache, bool resolver)
{
IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
if (imp == _objc_msgForward_impcache) return nil;
else return imp;
}

lookUpImpOrNillookUpImpOrForward 类似,前者内部是先调用后者函数,判断返回 imp 结果是否和 _objc_msgForward_impcache 相同,如果相同返回 nil,反之返回 imp。

需要注意的是在 lookUpImpOrNil 中并不会对 cls 进行初始化(initialize)或者是方法动态实现过程(resolver),若 lookUpImpOrNil 返回了nil,则会调用 _class_resolveInstanceMethod

这里以非元类来分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
// 如果类没有实现 +resolveInstanceMethod 方法则返回nil
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
return;
}

// 通过 objc_msgSend 来执行 resolveInstanceMethod 方法
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

// resolveInstanceMethod 执行过程中肯能会动态添加方法, lookUpImpOrNil 会缓存最新的imp(不管是否是开发者动态实现),
// 这样做可以下次方法调用的时候,不会再次执行动态方法解析的过程
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

// ......忽略相关日志代码
}

到此,消息转发前的逻辑已经全部走完,简单总结一下各个函数调用的顺序作用:

  1. 汇编入口 _objc_msgSend 为消息发送的入口
  2. 找不到方法则跳转到 __objc_msgSend_uncached_impcache ,对栈进行相关操作
  3. 跳转 _class_lookupMethodAndLoadCache3 (objc-runtime-new.mm)
  4. 第一次执行 lookUpImpOrForward, 对相关类进行 initialize 相关操作,忽略缓存列表去查找方法,如果找不到会进行 reslover 动态方法解析
  5. 步骤4会一直从本类到父类进行重复查找,如果都没有找到方法则调用 _class_resolveMethod 进行方法动态解析
  6. 如果是非元类,则直接跳转到 _class_resolveInstanceMethod ,函数内部会先调用 lookUpImpOrNil 来判断类有没有实现 +resolveInstanceMethod 方法,这里的查找结果也会缓存到 cache 中,内部查找也是通过 lookUpImpOrForward 来实现,根据返回的imp是否为 _objc_msgForward_impcache ,若是则返回 nil,然后 _class_resolveClassMethod 会直接return,结束动态解析过程
  7. +resolveClassMethod 被实现,则同过 objc_msgSend 来执行 +resolveClassMethod 方法;缓存结果,减少 _class_resolveClassMethod 过程调用

消息转发

在第一次执行 lookUpImpOrForward 过程中,动态解析方法完了之后,还没有找到方法,则放回 _objc_msgForward_impcache

__objc_msgSend_uncached_impcache 汇编代码会利用 br 指令跳转到 _objc_msgForward_impcache ,后者内部是通过 b 指令跳转到 __objc_msgForward,最后会调用 _objc_forward_handler 函数(objc-runtime.h)

_objc_msgSend_uncached_impcache 的默认实现为 objc_defaultForwardHandler

1
2
3
4
5
6
7
8
9
__attribute__((noreturn)) void 
objc_defaultForwardHandler(id self, SEL sel)
{
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

从代码实现中可以看到熟悉的报错日志:unrecognized selector sent to instance

要自定义转发过程则需要通过 objc_setForwardHandler 来重写 objc_defaultForwardHandler

1
2
3
4
5
6
7
void objc_setForwardHandler(void *fwd, void *fwd_stret)
{
_objc_forward_handler = fwd;
#if SUPPORT_STRET
_objc_forward_stret_handler = fwd_stret;
#endif
}

objc_setForwardHandler 的调用是在 Core Foundation 中实现,但在其开源代码中,苹果删除了件 __CFInitialize() 中调用 objc_setForwardHandler 的代码。具体可以参考文章Objective-C 消息发送与转发机制原理

消息转发的过程大概可以分为以下几步:

  1. 快速转发:开发者通过重写 forwardingTargetForSelector: 方法提供新的接受者(forwardingTarget)来重新执行 seletor;如果 forwardingTarget 和旧的接受者相同或者为nil,则进入下一步
  2. 完整消息转发:重写 methodSignatureForSelector: 方法获取方法签名并新建一个 NSInvocation 对象 invocation,invocation作为参数传入开发者重写的 forwardInvocation: 方法从而完成整个消息的转发
  3. 若步骤2没有完成转发则会调用 doesNotRecognizeSelector 方法,抛出异常

消息转发特性能做什么?

了解过消息转发的过程,那我们能利用这特性解决什么问题呢?

1. AOP

既然能接管消息转发的过程,很容易联想到通过消息转发在原有方法执行的过程中插入需要的代码逻辑,从而实现切面编程,具体可以参考forwardInvocation的例子

2.解决NSTimer强引用Target导致循环引用

跟第一个例子相似,也是通过 NSProxy 进行消息转发,在原有 NSTimer 和target 之间加入一层proxy解决循环引用问题

1
2
3
4
5
6
7
8
9
10
11
12
- (id)forwardingTargetForSelector:(SEL)aSelector {
return _weakObject;
}

- (void)forwardInvocation:(NSInvocation *)invocation {
void *null = NULL;
[invocation setReturnValue:&null];
}

- (BOOL)respondsToSelector:(SEL)aSelector {
return [_weakObject respondsToSelector:aSelector];
}

参考文章:

  1. iOS开发·runtime原理与实践: 消息转发篇(Message Forwarding)
  2. 从源代码看 ObjC 中消息的发送
  3. Objective-C 消息发送与转发机制原理