目录:
参考的博客:
Objective-C 消息发送与转发机制原理
[iOS开发]消息传递和消息转发机制
iOS八股文(六)objc_msgSend之方法查找源码解析
iOS八股文(七)objc_msgSend之动态解析和消息转发
消息传递机制的学习
之前学习过这个机制的一些内容:对象、消息、运行期
在对象上调用方法,术语就叫做传递消息,消息有名称和选择器(方法),可以接受参数,还可能有返回值。
在很多语言,比如 C ,调用一个方法其实就是跳到内存中的某一点并开始执行一段代码。没有任何动态的特性,因为这在编译时就决定好了。
而在 Objective-C 中,[object foo] 语法并不会立即执行 foo这个方法的代码。它是在运行时给object 发送一条叫 foo 的消息。这个消息,也许会由 object 来处理,也许会被转发给另一个对象,或者不予理睬假装没收到这个消息。多条不同的消息也可以对应同一个方法实现。这些都是在程序运行的时候决定的。
消息传递机制的学习其实就是理解OC是怎么样进行调用方法的。
id returnValue = [someObject messageName:parameter];
这样一条代码编译器会将其处理成
id returnValue = objc_msgSend(someObject, @selectro(messageName:), parameter);
下面介绍两个概念:
选择子SEL
OC在编译时会根据方法的名字(包括参数序列),生成一个用来区分这个办法的唯一的一个ID,这个ID就是SEL类型的。我们需要注意的是,只要方法的名字(包括参数序列)相同,那么他们的ID就是相同的。所以不管是父类还是子类,名字相同那么ID就是一样的。
SEL sell1 = @selector(eat:);
NSLog(@"sell1:%p", sell1);
SEL sell2 = @selector(eat);
NSLog(@"sell2:%p", sell2);
//sell1:0x100000f63
//sell2:0x100000f68
其中需要注意的是:@selector等于是把方法名翻译成SEL方法名。其仅仅关心方法名和参数个数,并不关心返回值与参数类型
生成SEL的过程是固定的,因为它只是一个表明方法的ID,不管是在哪个类写这个eat方法,SEL值都是固定一个
在Runtime中维护了一个SEL的表,这个表存储SEL不按照类来存储,只要相同的SEL就会被看做一个,并存储到表中。在项目加载时,会将所有方法都加载到这个表中,而动态生成的方法也会被加载到表中。
不同的类可以拥有相同的方法,不同类的实例对象执行相同的selector时会在各自的方法列表中去根据SEL去寻找自己类对应的IMP。
IMP本质就是一个函数指针,这个被指向的函数包含一个接收消息的对象id,调用方法的SEL,以及一些方法参数,并返回一个id。因此我们可以通过SEL获得它所对应的IMP,在取得了函数指针之后,也就意味着我们取得了需要执行方法的代码入口,这样我们就可以像普通的C语言函数调用一样使用这个函数指针。
小的总结:
SEL : 类成员方法的指针,但不同于C语言中的函数指针,函数指针直接保存了方法的地址,但SEL只是方法编号。**IMP:**一个函数指针,保存了方法的地址
IMP和SEL关系
每一个继承于NSObject的类都能自动获得runtime的支持。在这样的一个类中,有一个isa指针,指向该类定义的数据结构体,这个结构体是由编译器编译时为类(需继承于NSObject)创建的.在这个结构体中有包括了指向其父类类定义的指针以及 Dispatch table. Dispatch table是一张SEL和IMP的对应表。也就是说方法编号SEL最后还是要通过Dispatch table表寻找到对应的IMP,IMP就是一个函数指针,然后执行这个方法
1.通过方法获得方法的编号:SEL methodId=@selector(methodName);
或者SEL methodId = NSSelectorFromString(methodName);
2.通过方法编号执行该编号的方法: [self performSelector:methodId withObject:nil];
3.通过方法编号获取该编号的方法名 NSString*methodName = NSStringFromSelector(methodId);
4.通过方法编号获得IMP IMP methodPoint = [self methodForSelector:methodId];
5.执行IMP void (*func)(id, SEL, id) = (void *)imp; func(self, methodName,param);
**注意分析:**如果方法没有传入参数时:void (*func)(id, SEL) = (void *)imp; func(self, methodName);
如果方法传入一个参数时:void (*func)(id, SEL,id) = (void *)imp; func(self, methodName,param);
如果方法传入俩个参数时:void (*func)(id, SEL,id,id) = (void *)imp; func(self, methodName,param1,param2);
objc_msgSend()的执行流程
- 消息发送阶段:负责从类及父类的缓存列表及方法列表查找方法
- 动态解析阶段:如果消息发送阶段没有找到方法,则会进入动态解析阶段:负责动态地添加方法实现
- 消息转发阶段:如果也没有实现动态解析方法,则会进行消息转发阶段,将消息转发给可以处理消息的接受者来处理
消息发送和转发流程可以概括为:消息发送(Messaging)是 Runtime 通过 selector 快速查找 IMP 的过程,有了函数指针就可以执行对应的方法实现;消息转发(Message Forwarding)是在查找 IMP 失败后执行一系列转发流程的慢速通道,如果不作转发处理,则会打日志和抛出异常。
在学习消息发送和转发流程的前提是: 对OC Runtime已经有一定的了解,消息,Class的结构,selector、IMP、元类等等
objc_msgSend
此函数是消息发送必经之路,但只要一提 objc_msgSend
,都会说它的伪代码如下或类似的逻辑,反正就是获取 IMP 并调用:
id objc_msgSend(id self, SEL _cmd, ...) {
Class class = object_getClass(self);
IMP imp = class_getMethodImplementation(class, _cmd);
return imp ? imp(self, _cmd, ...) : 0;
}
objc_msgSend参数
objc_msgSend
在调用的时候有两个默认参数,第一个参数是消息的接收者,第二个参数是方法名。
这一点可以通过oc代码重写成cpp代码来证明。
int object_c_source_m() {
OSTestObject1 *obj1 = [[OSTestObject1 alloc] init];
[obj1 print];
return 0;
}
重写后:
int object_c_source_m() {
OSTestObject1 *obj1 = ((OSTestObject1 *(*)(id, SEL))(void *)objc_msgSend)((id)((OSTestObject1 *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("OSTestObject1"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)obj1, sel_registerName("print"));
return 0;
}
可以看到print
的调用转化成了objc_msgSend
调用并传入 objc1
和 print
。如果方法本身有参数,会把本身的参数拼接到这两个参数后面。
源码解析
用伪代码的原因就是objc_msgSend
是用汇编语言写的,针对不同架构有不同的实现。苹果为什么objc_msgSend这部分代码要使用汇编来编写呢?答案很简单–效率。汇编的效率是比c/c++更快的,因为汇编大多是直接对寄存器的读写,相比较对内存的操作更底层,效率也更高。另外苹果在所有的汇编方法命值钱都会用下划线开头,目的是为了防止符号冲突。
下方就是arm64结构下的源码:
ENTRY _objc_msgSend//进入消息转发
UNWIND _objc_msgSend, NoFrame
//p0寄存器,消息接收者
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)//b是跳转,le是小于等于,也就是p0小于等于0时,跳转到LNilOrTagged
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
//缓存查找
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged://如果接收者为nil,跳转至此
b.eq LReturnZero // nil check如果消息接受者为空,直接退出这个函数
GetTaggedClass
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY _objc_msgSend//结束
- 首先从
cmp p0,#0
开始,这里p0是寄存器,存放的是消息接受者。b.le LNilOrTagged
,b是跳转到的意思。le是如果p0小于等于0,总体意思是若p0小于等于0,则跳转到LNilOrTagged
,执行b.eq LReturnZero
直接退出这个函数 - 如果消息接受者不为nil,汇编继续跑,到
CacheLookup NORMAL
,CacheLookup
这个宏是在类的缓存中查找 selector 对应的 IMP(放到p10
)并执行。如果缓存没中,那就得到 Class 的方法表中查找了来看一下具体的实现
其实只需要看注释就能知道大概流程。
这部分其实是objc_msgSend
开始到找类对像cache
方法结束的流程。
首先判断receiver
是否存在,以及是否是taggedPointer
类型的指针,如果不是taggedPointer
类型,我们就取出对象的isa
指针(x13寄存器中),通过isa
指针找到类对象(x16寄存器),然后通过CacheLookup
,在类对象的cache
中查找是否有方法缓存,如果有就调用,如果没有走objc_msg_uncached
分支。
在cache中快速查找
下面就是CacheLookup
的源码:
//objc_msgSend开始找到类对象cache方法结束的流程中的 CacheLookup 方法的源码如下:
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
//
// Restart protocol:
//
// As soon as we're past the LLookupStart\Function label we may have
// loaded an invalid cache pointer or mask.
//
// When task_restartable_ranges_synchronize() is called,
// (or when a signal hits us) before we're past LLookupEnd\Function,
// then our PC will be reset to LLookupRecover\Function which forcefully
// jumps to the cache-miss codepath which have the following
// requirements:
//
// GETIMP:
// The cache-miss is just returning NULL (setting x0 to 0)
//
// NORMAL and LOOKUP:
// - x0 contains the receiver
// - x1 contains the selector
// - x16 contains the isa
// - other registers are set as per calling conventions
//
mov x15, x16 // stash the original isa
LLookupStart\Function:
// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
ldr p10, [x16, #CACHE] // p10 = mask|buckets
lsr p11, p10, #48 // p11 = mask
and p10, p10, #0xffffffffffff // p10 = buckets
and w12, w1, w11 // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
tbnz p11, #0, LLookupPreopt\Function
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
and p10, p11, #0x0000fffffffffffe // p10 = buckets
tbnz p11, #0, LLookupPreopt\Function
#endif
eor p12, p1, p1, LSR #7
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
ldr p11, [x16, #CACHE] // p11 = mask|buckets
and p10, p11, #~0xf // p10 = buckets
and p11, p11, #0xf // p11 = maskShift
mov p12, #0xffff
lsr p11, p12, p11 // p11 = mask = 0xffff >> p11
and p12, p1, p11 // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// do {
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // if (sel != _cmd) {
b.ne 3f // scan more
// } else {
2: CacheHit \Mode // hit: call or return imp
// }
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
cmp p13, p10 // } while (bucket >= buckets)
b.hs 1b
// wrap-around:
// p10 = first bucket
// p11 = mask (and maybe other bits on LP64)
// p12 = _cmd & mask
//
// A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
// So stop when we circle back to the first probed bucket
// rather than when hitting the first bucket again.
//
// Note that we might probe the initial bucket twice
// when the first probed slot is the last entry.
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
add p13, p10, w11, UXTW #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
// p13 = buckets + (mask << 1+PTRSHIFT)
// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
add p13, p10, p11, LSL #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = first probed bucket
// do {
4: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // if (sel == _cmd)
b.eq 2b // goto hit
cmp p9, #0 // } while (sel != 0 &&
ccmp p13, p12, #0, ne // bucket > first_probed)
b.hi 4b
LLookupEnd\Function:
LLookupRecover\Function:
b \MissLabelDynamic
#if CONFIG_USE_PREOPT_CACHES
#if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16
#error config unsupported
#endif
LLookupPreopt\Function:
#if __has_feature(ptrauth_calls)
and p10, p11, #0x007ffffffffffffe // p10 = buckets
autdb x10, x16 // auth as early as possible
#endif
// x12 = (_cmd - first_shared_cache_sel)
adrp x9, _MagicSelRef@PAGE
ldr p9, [x9, _MagicSelRef@PAGEOFF]
sub p12, p1, p9
// w9 = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
#if __has_feature(ptrauth_calls)
// bits 63..60 of x11 are the number of bits in hash_mask
// bits 59..55 of x11 is hash_shift
lsr x17, x11, #55 // w17 = (hash_shift, ...)
lsr w9, w12, w17 // >>= shift
lsr x17, x11, #60 // w17 = mask_bits
mov x11, #0x7fff
lsr x11, x11, x17 // p11 = mask (0x7fff >> mask_bits)
and x9, x9, x11 // &= mask
#else
// bits 63..53 of x11 is hash_mask
// bits 52..48 of x11 is hash_shift
lsr x17, x11, #48 // w17 = (hash_shift, hash_mask)
lsr w9, w12, w17 // >>= shift
and x9, x9, x11, LSR #53 // &= mask
#endif
ldr x17, [x10, x9, LSL #3] // x17 == sel_offs | (imp_offs << 32)
cmp x12, w17, uxtw
.if \Mode == GETIMP
b.ne \MissLabelConstant // cache miss
sub x0, x16, x17, LSR #32 // imp = isa - imp_offs
SignAsImp x0
ret
.else
b.ne 5f // cache miss
sub x17, x16, x17, LSR #32 // imp = isa - imp_offs
.if \Mode == NORMAL
br x17
.elseif \Mode == LOOKUP
orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
SignAsImp x17
ret
.else
.abort unhandled mode \Mode
.endif
5: ldursw x9, [x10, #-8] // offset -8 is the fallback offset
add x16, x16, x9 // compute the fallback isa
b LLookupStart\Function // lookup again with a new isa
.endif
#endif // CONFIG_USE_PREOPT_CACHES
.endmacro
大致看看注释,不用深究汇编代码逻辑,大概应该是通过类对象内存平移找到cache
,然后再获取buckets
,然后再查找方法。 注释解释:如果没有找到返回NULL,查找的时候x0存放方法接收者,x1存放方法名,x16存放isa指针。
如果没有找到,直接走_objc_msgSend_uncached
流程(走方法列表查询流程)
方法类表中查找
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p15 is the class to search
//下面两行代码是关键
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
从上方代码中我们发现执行了MethodTableLookup
方法进行方法列表查询,该方法如下:
.macro MethodTableLookup
SAVE_REGS MSGSEND
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
bl _lookUpImpOrForward//此处调用了loolUpImpOrForward方法
// IMP in x0
mov x17, x0
RESTORE_REGS MSGSEND
.endmacro
搜索lookUpImpOrForward
方法如下:
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
runtimeLock.assertUnlocked();
if (slowpath(!cls->isInitialized())) {
// The first message sent to a class is often +new or +alloc, or +self
// which goes through objc_opt_* or various optimized entry points.
//
// However, the class isn't realized/initialized yet at this point,
// and the optimized entry points fall down through objc_msgSend,
// which ends up here.
//
// We really want to avoid caching these, as it can cause IMP caches
// to be made with a single entry forever.
//
// Note that this check is racy as several threads might try to
// message a given class for the first time at the same time,
// in which case we might cache anyway.
behavior |= LOOKUP_NOCACHE;
}
// runtimeLock is held during isRealized and isInitialized checking
// to prevent races against concurrent realization.
// runtimeLock is held during method search to make
// method-lookup + cache-fill atomic with respect to method addition.
// Otherwise, a category could be added but ignored indefinitely because
// the cache was re-filled with the old value after the cache flush on
// behalf of the category.
runtimeLock.lock();
// We don't want people to be able to craft a binary blob that looks like
// a class but really isn't one and do a CFI attack.
//
// To make these harder we want to make sure this is a class that was
// either built into the binary or legitimately registered through
// objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
checkIsKnownClass(cls);
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
// runtimeLock may have been dropped but is now locked again
runtimeLock.assertLocked();
curClass = cls;
// The code used to lookup the class's cache again right after
// we take the lock but for the vast majority of the cases
// evidence shows this is a miss most of the time, hence a time loss.
//
// The only codepath calling into this without having performed some
// kind of cache lookup is class_getInstanceMethod().
for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
//cache缓存中查找
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// curClass method list.
//方法列表中查询
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
//找不到实现,方法解析器也没有帮助。
//使用转发。
imp = forward_imp;//由这一步进入消息转发
break;
}
}
// Halt if there is a cycle in the superclass chain.
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
if (fastpath(imp)) {
// Found the method in a superclass. Cache it in this class.
goto done;
}
}
// No implementation found. Try method resolver once.
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
done_unlock:
runtimeLock.unlock();
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
其中关键代码如下:
for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
//cache缓存中查找
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// curClass method list.
//方法列表中查询
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
//找不到实现,方法解析器也没有帮助。
//使用转发。
imp = forward_imp;//由这一步进入消息转发
break;
}
}
这段代码是非常厉害的,支持缓存的话要先从缓存当中查找,否则从方法列表中查找。这里有一个疑问,之前不是站在cache
中查找过了么?为什么还要再查找一次,首先是因为多线程同步问题,还有就是注意在这个循环里面curClass
要继续从superClass
中去找,这样逻辑统一,也需要在superClass
的cache
中查找。
如何在方法列表中寻找的呢,就是在getMethodNoSuper_nolock
方法里,下面是其实现:
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
// fixme nil cls?
// fixme nil sel?
auto const methods = cls->data()->methods();
for (auto mlists = methods.beginLists(),
end = methods.endLists();
mlists != end;
++mlists)
{
// <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest
// caller of search_method_list, inlining it turns
// getMethodNoSuper_nolock into a frame-less function and eliminates
// any store from this codepath.
method_t *m = search_method_list_inline(*mlists, sel);//可以看到这个方法是查找方法列表的关键
if (m) return m;
}
return nil;
}
上方的method_t *m = search_method_list_inline(*mlists, sel);
是查找方法列表的关键,我们现在来看一下search_method_list_inline
的实现:
ALWAYS_INLINE static method_t *
search_method_list_inline(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->isExpectedSize();
//fastpath大概率走这里
if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
//从排过序的方法列表中查找方法
return findMethodInSortedMethodList(sel, mlist);
} else {
// Linear search of unsorted method list
if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
return m;
}
#if DEBUG
// sanity-check negative results
//健全性检查阴性结果
if (mlist->isFixedUp()) {
for (auto& meth : *mlist) {
if (meth.name() == sel) {
_objc_fatal("linear search worked when binary search did not");
}
}
}
#endif
return nil;
}
这里可以看fastpath
代表的是大概率走这边,然后再看findMethodInSortedMethodList
的这个方法名,关键字InSorted
这里面可以大胆从字面翻译一下从排过序的方法类表中查找方法。
findMethodInSortedMethodList
的话有两个对应的方法实现,是因为C/C++可以通过参数来区分方法,这两个方法一个是两个参数,一个是三个参数,两个参数的这个方法最终也是调用三个参数的方法的。所以直接看三个参数的实现就可以:
template<class getNameFunc>
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
ASSERT(list);
auto first = list->begin();
auto base = first;
decltype(first) probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
//下方的count>>1=1相当于count = count / 2,所以这个是二分法查找
for (count = list->count; count != 0; count >>= 1) {
probe = base + (count >> 1);
uintptr_t probeValue = (uintptr_t)getName(probe);
if (keyValue == probeValue) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
probe--;
}
return &*probe;
}
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
注意看for
循环中的count
的变化。count>>=1
相当与 count = count / 2
。再结合之前的InSorted
,可以看到这是明显的二分法查找。
在找到方法之后需要缓存找到的方法,缓存找到的方法的流程如下:
接着我们先回到loolUpImpOrForward
的关键实现部分中:
for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
//cache缓存中查找
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// curClass method list.
//方法列表中查询
Method meth = getMethodNoSuper_nolock(curClass, sel);
//如果找到了方法,就进行缓存操作
if (meth) {
imp = meth->imp(false);
goto done;
}
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
//找不到实现,方法解析器也没有帮助。
//使用转发。(开始消息转发流程)
imp = forward_imp;//由这一步进入消息转发
break;
}
}
其中的:
//如果找到了方法,就进行缓存操作
if (meth) {
imp = meth->imp(false);
goto done;
}
将找到的方法保存然后去了done
done:
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
//很显然这个方法就是缓存找到方法的关键操作
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
我们看到缓存找到方法的关键操作是:log_and_fill_cache
方法,下面是其实现:
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
if (slowpath(objcMsgLogEnabled && implementer)) {
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
//用cache的insert将其写入缓存
cls->cache.insert(sel, imp, receiver);
}
我们发现其是调用cache的insert
方法写入到缓存中的,要注意的是cls
传的的是msgSend receiver 的isa指向的类对象/元类对象
。也就是说,即使方法是从父类的类对象/元类对象中找到的,这个方法缓存也是存再自己的cache中
的。
总结一下缓存查找和方法列表查找
用一张图总结:
上面我们了解了objc_msgSend
过程中的消息查找的流程,其中有个查找方法的函数名叫lookUpImpOrForward
,字面翻译查找方法或者转发。那么本文就来记录下objc_msgSend
的其他两个过程。
resolveMethod动态解析(动态决意)
lookUpImpOrForward
方法中有一段代码如下:
// No implementation found. Try method resolver once.
//没有发现方法实现,尝试一次动态解析
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
其中注释:没有找到方法实现,尝试一次方法解析。
继续来看里面resolveMethod_locked
的实现:
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
这里对cls进行了是否是元类的判断,如果是元类,说明是类方法的调用,则调用resolveClassMethod
,如果是类,说明是对象方法的调用,则调用resolveInstanceMethod
resolveInstanceMethod
方法代码如下:
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
//下方的resolveInstanceMethod:方法需要程序员自行编写实现
SEL resolve_sel = @selector(resolveInstanceMethod:);
if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
//缓存结果(好或坏),以便解析器下次不会触发。
//+resolveInstanceMethod添加到self a.k.a.cls
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
resolveClassMethod
方法源码如下:
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
// Resolver not implemented.
//未实现解析程序。
return;
}
Class nonmeta;
{
mutex_locker_t lock(runtimeLock);
//下面这个方法是用来获取元类对应的类对象的,然后再对类对象发送消息(相当于调用类方法)
nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
// +initialize path should have realized nonmeta already
//+初始化路径应已实现非meta
if (!nonmeta->isRealized()) {
_objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
nonmeta->nameForLogging(), nonmeta);
}
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
//下方的resolveClassMethod:方法需要程序员自行编写实现
bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
//缓存结果(好或坏),以便解析程序下次不会触发。
//+resolveClassMethod添加到self->ISA()也称cls
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
其中resolveClassMethod
resolveInstanceMethod
是通过msgSend
去调用类方法resolveClassMethod:
和resolveInstanceMethod:
的。其中resolveClassMethod
中的cls本来是元类
,通过getMaybeUnrealizedNonMetaClass
来获取元类对应的类对象
,然后再对类对象发送消息(相当于调用类方法)。
getMaybeUnrealizedNonMetaClass
方法的注释如下方所示:
可以看到其主要功能为返回该类或元类的普通类。
resolveClassMethod:
和resolveClassMethod:
的实现就交给了开发者,开发者可以再这里动态添加方法实现。
// 第一根稻草,使用动态解析,动态添加方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(methodOne)) {
Method methodNormal = class_getInstanceMethod(self, @selector(methodNormal));
class_addMethod(self,
@selector(methodOne),
method_getImplementation(methodNormal),
method_getTypeEncoding(methodNormal));
return YES
}
return [super resolveInstanceMethod:sel];
}
这样methodOne
的调用就可以使用methodNormal
的实现了。
注:resolveClassMethod:
和resolveInstanceMethod:
默认返回值是NO,如果你想在这个函数里添加方法实现,你需要借助class_addMethod
:
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
@cls : 给哪个类对象添加方法
@name : SEL类型,给哪个方法名添加方法实现
@imp : IMP类型的,要把哪个方法实现添加给给定的方法名
@types : 就是表示返回值和参数类型的字符串
下面我们进行一个动态测试的例子:
实现一个类,类在.h文件中声明一个实例方法methodOne
,但在.m文件中并没有实现这个方法
我们在外部调用这个方法就会导致程序崩溃
这个很容易理解:
- 第一步方法查找中,在自己的类对象以及父类的类对象中都没有找到这个方法的实现
- 所以转向动态方法解析,动态方法解析我们什么也没做
- 就会进行第三步,转向消息转发,消息转发我们也什么都没做,最后产生崩溃
但是如果我们对动态解析函数resolveInstanceMethod:进行一个编写,下方代码与上方动态方法解析例子相同:
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(methodOne)) {
Method methodNormal = class_getInstanceMethod(self, @selector(methodNormal));
class_addMethod(self,
@selector(methodOne),
method_getImplementation(methodNormal),
method_getTypeEncoding(methodNormal));
return YES
}
return [super resolveInstanceMethod:sel];
}
这样methodOne
的调用就可以使用methodNormal
的实现了,所以当第一步中示例方法methodOne
查找失败时,我们就会开始动态方法解析,在动态方法解析的resolveInstanceMethod:
函数中我们为methodOne
方法动态添加了methodNormal
方法的实现,那么走到这一步时,就会返回YES,并成功执行methodOne
方法。
消息转发
如果动态解析(动态决意)阶段还是没有对应的方法,那么就会来到消息转发阶段。消息转发阶段分为两部分,替换消息接收者阶段,也有叫快速转发,和完全消息转发阶段。
lookUpImpOrForward
方法的关键代码中:
for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
//cache缓存中查找
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// curClass method list.
//方法列表中查询
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
//找不到实现,方法解析器也没有帮助。
//使用转发。
imp = forward_imp;//由这一步进入消息转发
break;
}
}
我们可以看到imp = forward_imp;//由这一步进入消息转发
由这一步进入了消息转发,这里的原理是,如果已经进行过一次动态方法解析了,但是还是没有找到对应方法的实现,那么lookUpImpOrForward
方法就需要将_objc_msgForward_impcache
作为结果并写入缓存,然后将_objc_msgForward_impcache
标记结果返回到lookUpImpOrNil
方法后会经过判断继续向class_getMethodImplementation
方法返回nil
,然后接受到nil的class_getMethodImplementation
方法就会返回_objc_msgForward
正式开始消息转发流程,整个过程的代码如下:
__attribute__((flatten))
IMP class_getMethodImplementation(Class cls, SEL sel)
{
IMP imp;
if (!cls || !sel) return nil;
lockdebug_assert_no_locks_locked_except({ &loadMethodLock });
imp = lookUpImpOrNilTryCache(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);
// Translate forwarding function to C-callable external version
//如果imp为nil就开始返回_objc_msgForward正式进入消息转发
if (!imp) {
return _objc_msgForward;
}
return imp;
}
lookUpImpOrNilTryCache
函数获取不到 IMP
时就返回 _objc_msgForward
,lookUpImpOrNilTryCache
跟 lookUpImpOrForward
的功能很相似,只是将 lookUpImpOrForward
实现中的 _objc_msgForward_impcache
替换成了 nil
:
IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
{
return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}
我们发现lookUpImpOrNilTryCache
方法调用了_lookUpImpTryCache
方法,接下来我们就看一看_lookUpImpTryCache
的实现:
ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertUnlocked();
if (slowpath(!cls->isInitialized())) {
// see comment in lookUpImpOrForward
return lookUpImpOrForward(inst, sel, cls, behavior);
}
IMP imp = cache_getImp(cls, sel);
//如果缓存的位置不是NULL,就跳去执行done
if (imp != NULL) goto done;
#if CONFIG_USE_PREOPT_CACHES
if (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) {
imp = cache_getImp(cls->cache.preoptFallbackClass(), sel);
}
#endif
if (slowpath(imp == NULL)) {
return lookUpImpOrForward(inst, sel, cls, behavior);
}
done:
//缓存位置如果是_objc_msgForward_impcache的话,就返回nil
if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
return nil;
}
return imp;
}
到这里我们就理解了刚开始消息转发部分的流程了
由于最后返回的是_objc_msgForward
,那_objc_msgForward
到底是什么呢,其实它是一个入口,_objc_msgForward*
系列本质都是函数指针,都用汇编语言实现,都可以与 IMP
类型的值作比较,除_objc_msgForward
外,还有 _objc_msgForward_stret
,看后缀我们就知道_objc_msgForward_stret
的返回值肯定是一个结构体,接下来我们来看一下_objc_msgForward
的汇编实现:
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
发现里面调用了__objc_forward_handler
,但是很可惜__objc_forward_handler
并没有开源,但是其中其实是调用了objc_setForwardHandler
来为_objc_forward_handler
或_objc_forward_handler_stret
赋值,而赋值的参数则为汇编的__CF_forwarding_prep_0
和___forwarding_prep_1___
,而__CF_forwarding_prep_0
和 ___forwarding_prep_1___
函数都调用了 ___forwarding___
,而___forwarding___
又调用了 ___invoking___
函数,消息转发的逻辑几乎都写在 ___forwarding___
函数中了,所以我们看一下找到的 **___forwarding___
**函数的伪代码进行学习(这小段内容详见:Objective-C 消息发送与转发机制原理):
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 && forwarding != receiver) {
if (isStret == 1) {
int ret;
objc_msgSend_stret(&ret,forwardingTarget, sel, ...);
return ret;
}
return objc_msgSend(forwardingTarget, sel, ...);
}
}
// 僵尸对象
const char *className = class_getName(receiverClass);
const char *zombiePrefix = "_NSZombie_";
size_t prefixLen = strlen(zombiePrefix); // 0xa
if (strncmp(className, zombiePrefix, prefixLen) == 0) {
CFLog(kCFLogLevelError,
@"*** -[%s %s]: message sent to deallocated instance %p",
className + prefixLen,
selName,
receiver);
<breakpoint-interrupt>
}
// 调用 methodSignatureForSelector 获取方法签名后再调用 forwardInvocation
if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
if (methodSignature) {
BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct;
if (signatureIsStret != isStret) {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'. Signature thinks it does%s return a struct, and compiler thinks it does%s.",
selName,
signatureIsStret ? "" : not,
isStret ? "" : not);
}
if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];
[receiver forwardInvocation:invocation];
void *returnValue = NULL;
[invocation getReturnValue:&value];
return returnValue;
} else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message",
receiver,
className);
return 0;
}
}
}
SEL *registeredSel = sel_getUid(selName);
// selector 是否已经在 Runtime 注册过
if (sel != registeredSel) {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort",
sel,
selName,
registeredSel);
} // doesNotRecognizeSelector
else if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
[receiver doesNotRecognizeSelector:sel];
}
else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort",
receiver,
className);
}
// The point of no return.
kill(getpid(), 9);
}
这么一大坨代码就是整个消息转发路径的逻辑,概括如下:
- 先调用
forwardingTargetForSelector
方法获取新的target
作为receiver
重新执行selector
,如果返回的内容不合法(为nil
或者跟旧receiver
一样),那就进入第二步调用methodSignatureForSelector
。 - 调用
methodSignatureForSelector
获取方法签名后,判断返回类型信息是否正确,再调用forwardInvocation
执行NSInvocation
对象,并将结果返回。如果对象没实现methodSignatureForSelector
方法,进入第三步调用doesNotRecognizeSelector
。 - 调用
doesNotRecognizeSelector
方法,如果实现了doesNotRecognizeSelector
方法,就先执行该方法,再打印日志抛出异常,如果没有实现该方法,那么就直接去打印日志抛出异常。
自此,我们全部的消息转发的过程就全部讲完了,下面我们详细讲解一下消息转发主要用到的流程。
消息接收者替换
所以如果本类没有能力去处理这个消息,那么就转发给其他的类,让其他类去处理。
我们可以实现forwardingTargetForSelector
方法,并返回替换的对象:
//第二根稻草,使用快速消息转发,找其他对象来实现方法
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(methodTwo)) {
//也就是本类中的其他对象,此处选取的对象是forwardObject
return self.forwardObject;
}
return nil;
}
候就会去_forwardObject
里面去找methodTwo
了。_forwardObject
的类实现如下:
@implementation OSMsgSendForwardObject
//从消息转发而来
- (void)methodTwo {
NSLog(@"%s__ %@",__FUNCTION__,[self class]);
}
@end
完全消息转发
如果forwardingTargetForSelector
方法返回的是nil
,那么我们还有最后一根稻草可以抓住完全消息转发。相比于快速转发,不仅可以替换消息接受者,还能替换方法:
//第三根稻草,使用完全消息转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(methodThree)) {
NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"v@:"];
return sig;
}
return nil;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
//选择一个函数去替换
anInvocation.selector = @selector(methodNormal);
//选择一个消息接收者(对象)去替换
anInvocation.target = self.forwardObject;
[anInvocation invoke];
}
这里有两个类,NSMethodSignature
和 NSInvocation
。其中NSMethodSignature
是方法签名,可以通过方法的字符来实例化。NSInvocation
是方法调用实体,其中有target
和selector
和参数
构成。
这第三个救命稻草的逻辑就是: 先判断methodSignatureForSelector
有没有被实现且返回值不为nil
,如果已经实现且返回值不为nil
,那么就进行下一步判断forwardInvocation
有没有被实现,如果forwardInvocation
已经实现那么就使用方法签名生成NSInvocation
对象并调用forwardInvocation
方法,最后返回forwardInvocation
执行的结果,如果forwardInvocation
方法没有被实现,那就直接调用doesNotRecognizeSelector
方法打印日志抛出异常。如果methodSignatureForSelector
没有被实现或返回值为nil
,那么就直接调用doesNotRecognizeSelector
方法打印日志抛出异常。
附件(methodSignatureForSelector
方法中方法签名用到的字符编码):
为了协助运行时系统,编译器用字符串为每个方法的返回值和参数类型和方法选择器编码。使用的编码方案在其他情况下也很有用,所以它是public 的,可用于@encode() 编译器指令。当给定一个类型参数,返回一个编码类型字符串。类型可以是一个基本类型如int,指针,结构或联合标记,或任何类型的类名,事实上,都可以作为C sizeof() 运算符的参数。这个机制也是为了提高Runtime的效率,编码表如下:
由于调用一个方法我们需要知道方法调用者,方法名,方法参数。而methodSignatureForSelector
方法中我们创建方法签名的原因就是缺一个返回值和参数类型,所以方法签名中都是由上表中的字符构成的。
总结与思考
这张图很好地总结了消息转发三次拯救的过程:
我们首先明白OC方法调用的本质就是消息发送,消息发送是SEL-IMP的查找过程
我们先进行正常的消息发送,等到一个函数找不到时,OC运行时提供了三种方式去补救(三根救命稻草):
- 调用resolveInstanceMethod或者resolveClassMethod方法,尝试给一个没有实现的方法添加实现(动态解析)
- 调用forwardingTargetForSelector尝试让本类中其他的对象去执行这个函数(快速地消息转发)
- 如果没有进行快速转发,可以用methodSignatureForSeletor 和 frowardInvocation 来进行完全消息转发,不仅可以替换消息接受者,还能替换方法。(完整的消息转发)
最后附上一张大佬的完整版消息发送与转发流程图:
大佬的详细解释详见:Objective-C 消息发送与转发机制原理