Runtime 二:msg_msgSend执行流程
在Runtime 一: OC 方法的底层数据结构和存储机制我们知道了OC
类的底层结构,类方法的存储位置以及方法缓存的策略.今天我们搞清楚OC
方法调用的本质:消息发送机制 - msg_Send
.
我们随便写一个方法[person test];
,转成C++
代码,发现它的底层是这样的:

OC
方法调用的本质就是给对象发送消息:objc_msgSend()
,这个流程可以分为三个阶段:
- 消息发送
- 动态方法解析
-
消息转发
下面我们将从源码上分析这三个过程的具体实现.
一:消息发送代码分析:
查找源码步骤:
打开runtime
源码 -> 搜索objc_msgSend
-> 找到objc-msg-arm64.s
文件 -> 找到ENTRY _objc_msgSend
(方法入口):

我们用伪代码写一下上图的逻辑:
objc_msgSend(person , sel){
if (person == nil) return;
// 如果不为 nil ,从缓存中查找...
}
从上图中可以看到,如果reserver
不为nil
,就执行CacheLookup
从缓存中查找,所以我们搜索CacheLookup
方法:

搜索
CheckMiss
:
搜索
__objc_msgLookup_uncached
:
搜索
MethodTableLookup
:
搜索
__class_lookupMethodAndLoadCache3
,咦~一搜索发现这个方法就这一处存在,搜索不到了.既然汇编里面没有了,那会不会是用C语言
实现的这个方法呢?我们去掉一个下划线_
然后搜索(为什么要去掉一个下划线呢?因为C语言在编译成汇编语言是,会默认在方法前面加一个下划线_,所以我们在C语言中搜索时要去掉一个_
),发现还真搜索到了这个方法:
进入
lookUpImpOrForward
:
从上图中可以很清楚的看到看到方法的查找过程,注释把每一步都写的很清楚了.我们继续进入
getMethodNoSuper_nolock(cls, sel)
方法,看看它的内部是如何从类中查找方法的:
进入search_method_list(mlist, sel)
看看是如何查找的:

ok,到现在我们就从源码层面搞清楚了
objc_msgSend()
第一阶段消息发送
的步骤,我们用一张图总结一下:
二:动态方法解析:
从lookUpImpOrForward
方法实现中可以看到,如果在消息发送阶段始终没有找到方法,那么就会进入动态方法解析:

我们用代码验证一下.在
Person.h
中声明一个- (void)test
方法,但是不实现这个方法.然后我们重写+ (BOOL)resolveInstanceMethod:(SEL)sel
这个方法,然后运行代码:
发现的确进入到
resolveInstanceMethod
这个方法,那我们在这个方法内部改如何操作呢?
其实上图的
Method
就是我们之前讲过的method_t
,我们可以用method_t
替换一下上面的写法:
我们用一张图捋一下动态方法解析的步骤:

关于动态方法解析的注意点:
- 1:通过
class_addMethod
动态添加的方法是添加到class_rw_t
中的method_list_t
中的,我们从源码中也可以看到,动态添加方法的实现后,进入goto retry
,重新进入消息发送阶段,从类的cache_t
或者class_rw_t
中查找 - 2:动态添加方法的实现后,会重新进入消息发送阶段,重新查找方法
三:消息转发:
如果在消息发送阶段没有找到方法,并且在动态方法解析阶段没有实现resolveInstanceMethod / resolveClassMethod
或者在实现了这两个方法但是没有在这两个方法中添加方法,那么就会进入到第三步:消息转发阶段
进入消息转发阶段会进入- (id)forwardingTargetForSelector:(SEL)aSelector
方法,这个方法返回一个id
类型的对象,表示把这个消息转发给其他对象来处理,比如我们可以这样:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
Person *person = [[Person alloc]init];
//在 main 函数中调用 test 方法
[person test];
}
return 0;
}
// 把 消息 转发 给 student 对象来处理
- (id)forwardingTargetForSelector:(SEL)aSelector{
return [[Student alloc]init];
}
//最后执行的结果:
2019-12-02 15:17:02.837927+0800 msg_SendTest[9942:3757488] student 的 test 方法
forwardingTargetForSelector
这个方法内部是如何处理的呢?我们还是从源码上找答案:

我们进入
_objc_msgForward_impcache
内部会发现没有别的线索了,只是一个声明:
其实,进入到消息转发阶段,相关的源代码就没有开源了.所以要想研究消息转发阶段到底做了什么,只能通过逆向了分析,可惜我又不会逆向,😆,好尴尬😳.辛亏有国外的大神把消息转发的流程通过伪代码写了出来:
int __forwarding__(void *frameStackPointer, int isStret) {
id receiver = *(id *)frameStackPointer;
SEL sel = *(SEL *)(frameStackPointer + 8);
const char *selName = sel_getName(sel);
Class receiverClass = object_getClass(receiver);
// 调用 forwardingTargetForSelector:
if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
id forwardingTarget = [receiver forwardingTargetForSelector:sel];
if (forwardingTarget && forwardingTarget != receiver) {
return objc_msgSend(forwardingTarget, sel, ...);
}
}
// 调用 methodSignatureForSelector 获取方法签名后再调用 forwardInvocation
if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
if (methodSignature && class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];
[receiver forwardInvocation:invocation];
void *returnValue = NULL;
[invocation getReturnValue:&value];
return returnValue;
}
}
if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
[receiver doesNotRecognizeSelector:sel];
}
// The point of no return.
kill(getpid(), 9);
}
为什么是__forwarding__
这个方法呢?我们把forwardingTargetForSelector
方法注释掉,看看系统会报什么错误:

所以进入消息转发阶段后,就会进入
__forwarding__
方法处理.我么重点研究这个方法.上面已经把伪代码的核心部分提炼出来了,下面我通过截图标注的形式说明:
我们用代码验证一下:

其实我们可以在
forwardInvocation
中做任何事情,因为NSInvocation
内部有我们想要的任何东西:
为所欲为:

以上都是以实力方法为例,下面我们把-(void)test;
更改为+(void)test
:
更改为类方法后一运行报错,这是为什么呢?我们仔细看看伪代码:

所以,如果是类方法,我们就要这样写:

到这里我们就搞清楚了消息转发的所有流程和细节,我们用一张图捋一捋:

ok,终于把消息发送的全部流程写完了,说实话,写文章挺费时间的,加油坚持吧.