在 Objective-C 语言中,实例对象执行方法,而执行方法的过程也可以称为给实例对象发送消息。
当你随便写下一段函数调用的代码后,
[receiver message];
都会被编译器转化为
id objc_msgSend(id self, SEL op, ...);
本文将分析objc_msgSend
在 objc4-756.2
版objc-msg-arm64.s
文件的汇编实现
objc_msgSend
objc_msgSend
虽然开源但是是用汇编实现的,好在有详细对的注释。对于不熟悉汇编的人通过注释也能够看得懂
1 | ENTRY _objc_msgSend |
这段代码主要做了两个事情
- 校验tagged是否为空。如果为空则
return
。 - 如果不为空则跳转到
CacheLookup
处。参数为NORMAL
CacheLookup
.macro CacheLookup
// p1 = SEL, p16 = isa
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
and w12, w1, w11 // x12 = _cmd & mask
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
add p12, p12, w11, UXTW #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // double wrap
JumpMiss $0
.endmacro
代码逻辑主要为从当前类对象缓存中寻找函数。
1.找到则调用。
2.未找到则跳转到CheckMiss
CheckMiss
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
由于CacheLookup
的入参$0
是NORMAL
,所以此处CheckMiss
的入参也是NORMAL
。所以回跳转到__objc_msgSend_uncached
处
__objc_msgSend_uncached
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
主要逻辑是调用MethodTableLookup
寻找函数地址并将其存放在x17
寄存器中调用。
MethodTableLookup
1 | .macro MethodTableLookup |
前后一堆操作地址可以忽略。主要是看__class_lookupMethodAndLoadCache3
这个调用,这个函数式使用C语言实现的,可以不用看汇编了。
_class_lookupMethodAndLoadCache3
1 | /*********************************************************************** |
_class_lookupMethodAndLoadCache3
主要的逻辑是调用lookUpImpOrForward
函数,并且通过传参可以看出cache
是为NO,因为之前在汇编代码CacheLookup
中已经使用过缓查找。接下来主要分析lookUpImpOrForward
的实现。
lookUpImpOrForward
这个函数主要做了有三件事情
- 查找函数地址
- 如果未找到函数地址,进入方法决议流程。
- 如果前面两部都没有成功,进入消息转发流程。
1 |
|
由于代码很长,就不逐行分析了,之写下大致流程。
- 查找函数地址
1.1 如果类对象第一次接受到消息,会调用initialize
函数
1.2 从当前的类对象缓存中查找函数(如果是类方法则从当前的元类对象缓存查找,以下同理)
1.3 上面未找到,则从当前类对象的函数列表中查找,如果找到,将其放入当前类对象的缓存中。
1.4 上面未找到,则逐级从父类的缓存和方法列表中查找,如果找到,如果找到,将其放入当前类对象的缓存中。
1.5 上面未找到,跳入重定向流程 - 方法决议
2.1 如果当前类是类对象,同时有实现+ resolveInstanceMethod
。则调用resolveInstanceMethod
,然后跳转到1.1
2.2 如果当前类是元类类对象,同时有实现+ resolveClassMethod
。则调用resolveClassMethod
,然后跳转到1.1
2.3 如果经过上面步骤还是无法查找到函数地址则进入消息转发阶段。 - 消息转发
3.1 由于消息转发的实现_objc_msgForward_impcache
是闭源的,目前无法查看,但是大致做的事情有很多博客有说明。
3.2 调用[+ -] forwardingTargetForSelector
尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非 nil 对象。否则返回 nil ,继续下面的动作
3.3 调用[+ -] methodSignatureForSelector:
方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector
抛出异常。如果能获取,则返回非nil:创建一个NSlnvocation
并传给[+ -]forwardInvocation:
。
3.4 调用[+ -] forwardInvocation:
方法,将第3步获取到的方法签名包装成Invocation
传入,接下来如何操作由程序员自行处理。
3.5 如果上面都符合的话就会抛出doesNotRecognizeSelector
异常。