

深入了解Objective-C消息发送与转发过程
source link: https://chipengliu.github.io/2019/06/02/objc-msgSend-forward/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

深入了解Objective-C消息发送与转发过程
2019-06-022020-10-24iOS
在 Objective-C 语言中,对象/类(其实类也是一个对象) 执行方法最后会转化成给对象发送消息:
objc_msgSend(receiver, @selector(message))
如果 reveiver
中没有找到对应方法 message
, 则会开始消息转发的过程,也就是过程:
- 动态方法解析 Method Resolution
- 快速转发 Fast Rorwarding
- 完整消息转发 Normal Forwarding
接下来通过OC的源码来分析以上几个步骤具体的调用过程
当在 OC 中给对象执行方法,如 [object foo]
,会被翻译为 objc_msgSend(object, @selector(foo))
,@seletor
会将 foo 方法生成对应的选择子(SEL),选择子只跟方法名有关系,不同的类之间可以存在相同的方法选择子,但是同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也不行,也就是说 OC 中不支持像 C++ 那样的函数重载。
当编译器遇到一个方法调用时,它会将方法的调用翻译成以下函数中的一个
objc_msgSend
、objc_msgSend_stret
、objc_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;
}
整个方法查找的过程,可以简单的概括为以下几个步骤
- 实现、初始化对应的类
- 根据是否支持垃圾回收机制(GC)判断是否忽略当前的方法调用
- 从cache中查找方法
- cache中没有找到对应的方法,则到方法列表中查,查到则缓存
- 如果本类中查询到没有结果,则遍历所有父类重复上面的查找过程
- 最后都没有找到的方法的话,则执行
_class_resolveMethod
让调用者动态添加方法,并重复一轮查询方法的过程 - 若第六步没有完成动态添加方法,则把 _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;
}
lookUpImpOrNil
和 lookUpImpOrForward
类似,前者内部是先调用后者函数,判断返回 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*/);
// ......忽略相关日志代码
}
到此,消息转发前的逻辑已经全部走完,简单总结一下各个函数调用的顺序作用:
- 汇编入口
_objc_msgSend
为消息发送的入口 - 找不到方法则跳转到
__objc_msgSend_uncached_impcache
,对栈进行相关操作 - 跳转
_class_lookupMethodAndLoadCache3
(objc-runtime-new.mm) - 第一次执行
lookUpImpOrForward
, 对相关类进行 initialize 相关操作,忽略缓存列表去查找方法,如果找不到会进行 reslover 动态方法解析 - 步骤4会一直从本类到父类进行重复查找,如果都没有找到方法则调用
_class_resolveMethod
进行方法动态解析 - 如果是非元类,则直接跳转到
_class_resolveInstanceMethod
,函数内部会先调用lookUpImpOrNil
来判断类有没有实现+resolveInstanceMethod
方法,这里的查找结果也会缓存到 cache 中,内部查找也是通过lookUpImpOrForward
来实现,根据返回的imp是否为_objc_msgForward_impcache
,若是则返回 nil,然后_class_resolveClassMethod
会直接return,结束动态解析过程 - 若
+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 消息发送与转发机制原理
消息转发的过程大概可以分为以下几步:
- 快速转发:开发者通过重写
forwardingTargetForSelector:
方法提供新的接受者(forwardingTarget)来重新执行 seletor;如果 forwardingTarget 和旧的接受者相同或者为nil,则进入下一步 - 完整消息转发:重写
methodSignatureForSelector:
方法获取方法签名并新建一个 NSInvocation 对象 invocation,invocation作为参数传入开发者重写的forwardInvocation:
方法从而完成整个消息的转发 - 若步骤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];
}
参考文章:
Recommend
-
63
注:文章中使用的dubbo源码版本为2.5.4 零、文章目录 Consumer发送请求 Provider接收请求并发送响应 Consumer接收响应 一、Consumer发送请求 1.1 代码入口 在 dubbo剖析:二 服务引用 中讲到,服务引用方根据引用接口DemoService,使用dubbo的代理工厂类JavassistP...
-
65
-
10
对接C++socket打包发送与解析,打包解包一律使用网络字节序(大端)java该怎么实现? ...
-
14
在前一篇文章中给大家已经介绍过
-
6
技术之前,先读诗书:春江潮水连海平,海上明月共潮生。 之前讲解了Spring Cloud Stream整合RabbitMQ和GCP Pubsub,都是非常简单,而且代码没什么区别的。本文讲解Spring Cloud Str...
-
8
Linux网络源代码学习——数据包的发送与接收 | 李乾坤的博客 linux网络编程中,各层有各层的struct,但有一个struct是各层通用的,这就是描述接收和发送...
-
6
麻省理工科技评论-有望用于药物递送与癌症诊疗,宁夏大学团队联合同济大学实现棒状胶束活性横向生长有望用于药物递送与癌症诊疗,宁夏大学团队联合同济大学实现棒状胶束活性横向生长“我们相信该成果在将来能够用于精确调控柱...
-
3
集齐饿了么、达达快送与顺丰同城 抖音本地生活再进击 记者/王郁彪 编辑/刘雪梅 1...
-
8
闪送与抖音生活服务达成合作 为后者提供团购配送服务 ...
-
12
数据:BUSD发送与接收地址数创两年来新低 Neken 2023-05-03 16:00 摘要: 数据:BUSD发送与接收地址数创两年来新低 5月3日TuoniaoX...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK