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);
下面介绍两个概念:
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);
消息发送和转发流程可以概括为:消息发送(Messaging)是 Runtime 通过 selector 快速查找 IMP 的过程,有了函数指针就可以执行对应的方法实现;消息转发(Message Forwarding)是在查找 IMP 失败后执行一系列转发流程的慢速通道,如果不作转发处理,则会打日志和抛出异常。
在学习消息发送和转发流程的前提是: 对OC Runtime已经有一定的了解,消息,Class的结构,selector、IMP、元类等等
此函数是消息发送必经之路,但只要一提 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
直接退出这个函数CacheLookup NORMAL
,CacheLookup
这个宏是在类的缓存中查找 selector 对应的 IMP(放到 p10
)并执行。如果缓存没中,那就得到 Class 的方法表中查找了来看一下具体的实现其实只需要看注释就能知道大概流程。
这部分其实是objc_msgSend
开始到找类对像cache
方法结束的流程。
首先判断receiver
是否存在,以及是否是taggedPointer
类型的指针,如果不是taggedPointer
类型,我们就取出对象的isa
指针(x13寄存器中),通过isa
指针找到类对象(x16寄存器),然后通过CacheLookup
,在类对象的cache
中查找是否有方法缓存,如果有就调用,如果没有走objc_msg_uncached
分支。
下面就是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)
{
// 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
的其他两个过程。
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运行时提供了三种方式去补救(三根救命稻草):