【iOS】消息传递和消息转发机制
消息传递机制
在OC语言中,调用对象的方法被叫做消息传递。消息有名称和选择子(selector),可以接受参数,还可能有返回值。
在Objective-C中,如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的C语言函数,然而对象收到消息之后,究竟该调用那个方法则完全运行期决定,甚至可以在程序运行时改变,这些特性使得Objective-C成为一门真正的动态语言。
例:OC消息表达式:
id returnValue = [someObject messageName:parameter];
这一段会被编译器处理成:
id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);
someObject称为接受者(receiver),messageName称为“选择子”,选择子和参数一起称为“消息”,编译器看到这条消息会转换成一条标准的 C 语言函数调用。
选择子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
这样的机制大大的增加了我们的程序的灵活性,我们可以通过给一个方法传递SEL参数,让这个方法动态的执行某一个方法;我们也可以通过配置文件指定需要执行的方法,程序读取配置文件之后把方法的字符串翻译成为SEL变量然后给相应的对象发送这个消息。
从效率的角度上来说,执行的时候不是通过方法名字而是方法ID也就是一个整数来查找方法,由于整数的查找和匹配比字符串要快得多,所以这样可以在某种程度上提高执行的效率
我们需要注意,@selector等同于是把方法名翻译成SEL方法名,其仅仅关心方法名和参数个数,并不关心返回值与参数类型
生成SEL的过程是固定的,因为它只是一个表明方法的ID,不管是在哪个类写这个dayin方法,SEL值都是固定一个
在Runtime中维护了一个SEL的表,这个表存储SEL不按照类来存储,只要相同的SEL就会被看做一个,并存储到表中。在项目加载时,会将所有方法都加载到这个表中,而动态生成的方法也会被加载到表中。
那么不同的类可以拥有相同的方法,不同类的实例对象执行相同的selector时会在各自的方法列表中去根据SEL去寻找自己类对应的IMP。
IMP本质就是一个函数指针,这个被指向的函数包含一个接收消息的对象id,调用方法的SEL,以及一些方法参数,并返回一个id。因此我们可以通过SEL获得它所对应的IMP,在取得了函数指针之后,也就意味着我们取得了需要执行方法的代码入口,这样我们就可以像普通的C语言函数调用一样使用这个函数指针。
objc_msgSend
我们可以看到转换中,使用到了objc_msgSend 函数,这个函数将消息接收者和方法名作为主要参数,如下所示:
objc_msgSend(receiver, selector) // 不带参数
objc_msgSend(receiver, selector, arg1, arg2,...) // 带参数
objc_msgSend 通过以下几个步骤实现了动态绑定机制:
- 首先,获取 selector 指向的方法实现。由于相同的方法可能在不同的类中有着不同的实现,因此根据 receiver 所属的类进行判断。
- 其次,传递 receiver 对象、方法指定的参数来调用方法实现。
- 最后,返回方法实现的返回值。
消息传递的关键在于objc_class结构体,其有三个关键的字段:
- isa:指向类的指针。
- superclass:指向父类的指针。
- methodLists:类的方法分发表(dispatch table)。
当创建一个新对象时,先为其分配内存,并初始化其成员变量。其中 isa 指针也会被初始化,让对象可以访问类及类的继承链。
下图所示为消息传递过程的示意图:

- 当消息传递给一个对象时,首先从运行时系统缓存objc_cache中进行查找。如果找到,则执行。否则,继续执行下面步骤。
- objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法分发表methodLists中查找方法的selector。如果未找到,将沿着类的superclass找到其父类,并在父类的分发表methodLists中继续查找。
- 以此类推,一直沿着类的继承链追溯至NSObject类。一旦找到selector,传入相应的参数来执行方法的具体实现,并将该方法加入缓存objc_cache。如果最后仍然没有找到selector,则会进入消息转发流程。
源码解析
快速查找
objc_msgSend在不同架构下都有实现:以arm64为例,代码实现是汇编。为什么选用汇编来实现?速度更快,直接使用参数,免去大量参数的拷贝的开销。在函数和全局变量前面会加下划线“_”,防止符号冲突。
汇编过程:
//进入objc_msgSend流程ENTRY _objc_msgSend//流程开始,无需frameUNWIND _objc_msgSend, NoFrame//判断p0(消息接收者)是否存在,不存在则重新开始执行objc_msgSendcmp p0, #0 // nil check and tagged pointer check//如果支持小对象类型,返回小对象或空
#if SUPPORT_TAGGED_POINTERS//b是进行跳转,b.le是小于判断,也就是p0小于0的时候跳转到LNilOrTaggedb.le LNilOrTagged // (MSB tagged pointer looks negative)
#else//等于,如果不支持小对象,就跳转至LReturnZero退出b.eq LReturnZero
#endif//通过p13取isaldr p13, [x0] // p13 = isa//通过isa取class并保存到p16寄存器中GetClassFromIsa_p16 p13, 1, x0 // p16 = class
- 首先从cmp p0,#0开始,这里p0是寄存器,存放的是消息接受者。当进入消息发送入口时,先判断消息接收者是否存在,不存在则重新执行objc_msgSend
- b.le LNilOrTagged,b是跳转到的意思。le是如果p0小于等于0,总体意思是若p0小于等于0,则跳转到LNilOrTagged,执行b.eq LReturnZero直接退出这个函数
- 如果消息接受者不为nil,汇编继续跑,到CacheLookup NORMAL,在cache中查找imp。
来看一下具体的实现:
//在cache中通过sel查找imp的核心流程
.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////从x16中取出class移到x15中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表示p16寄存器存储的值,当前是Class//#数值 表示一个值,这里的CACHE经过全局搜索发现是2倍的指针地址,也就是16个字节//#define CACHE (2 * __SIZEOF_POINTER__)//经计算,p10就是cacheldr p10, [x16, #CACHE] // p10 = mask|bucketslsr p11, p10, #48 // p11 = maskand p10, p10, #0xffffffffffff // p10 = bucketsand w12, w1, w11 // x12 = _cmd & mask
//真机64位看这个
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16//CACHE 16字节,也就是通过isa内存平移获取cache,然后cache的首地址就是 (bucket_t *)ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
//获取buckets
#if __has_feature(ptrauth_calls)tbnz p11, #0, LLookupPreopt\Functionand p10, p11, #0x0000ffffffffffff // p10 = buckets
#else//and表示与运算,将与上mask后的buckets值保存到p10寄存器and p10, p11, #0x0000fffffffffffe // p10 = buckets//p11与#0比较,如果p11不存在,就走Function,如果存在走LLookupPreopttbnz p11, #0, LLookupPreopt\Function
#endif//按位右移7个单位,存到p12里面,p0是对象,p1是_cmdeor p12, p1, p1, LSR #7and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#elseand p10, p11, #0x0000ffffffffffff // p10 = buckets//LSR表示逻辑向右偏移//p11, LSR #48表示cache偏移48位,拿到前16位,也就是得到mask//这个是哈希算法,p12存储的就是搜索下标(哈希地址)//整句表示_cmd & mask并保存到p12and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4ldr p11, [x16, #CACHE] // p11 = mask|bucketsand p10, p11, #~0xf // p10 = bucketsand p11, p11, #0xf // p11 = maskShiftmov p12, #0xfffflsr p11, p12, p11 // p11 = mask = 0xffff >> p11and p12, p1, p11 // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif//去除掩码后bucket的内存平移//PTRSHIFT经全局搜索发现是3//LSL #(1+PTRSHIFT)表示逻辑左移4位,也就是*16//通过bucket的首地址进行左平移下标的16倍数并与p12相与得到bucket,并存入到p13中add p13, p10, p12, LSL #(1+PTRSHIFT)// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))// do {
//ldp表示出栈,取出bucket中的imp和sel分别存放到p17和p9
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--//cmp表示比较,对比p9和p1,如果相同就找到了对应的方法,返回对应imp,走CacheHitcmp p9, p1 // if (sel != _cmd) {//b.ne表示如果不相同则跳转到3fb.ne 3f // scan more// } else {
2: CacheHit \Mode // hit: call or return imp// }
//向前查找下一个bucket,一直循环直到找到对应的方法,循环完都没有找到就调用_objc_msgSend_uncached
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;//通过p13和p10来判断是否是第一个bucketcmp 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_ADDRSadd p13, p10, w11, UXTW #(1+PTRSHIFT)// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))// p13 = buckets + (mask << 1+PTRSHIFT)// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4add p13, p10, p11, LSL #(1+PTRSHIFT)// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endifadd 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 hitcmp p9, #0 // } while (sel != 0 &&ccmp p13, p12, #0, ne // bucket > first_probed)b.hi 4bLLookupEnd\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 = bucketsautdb x10, x16 // auth as early as possible
#endif// x12 = (_cmd - first_shared_cache_sel)adrp x9, _MagicSelRef@PAGEldr 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_shiftlsr x17, x11, #55 // w17 = (hash_shift, ...)lsr w9, w12, w17 // >>= shiftlsr x17, x11, #60 // w17 = mask_bitsmov x11, #0x7ffflsr 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_shiftlsr x17, x11, #48 // w17 = (hash_shift, hash_mask)lsr w9, w12, w17 // >>= shiftand x9, x9, x11, LSR #53 // &= mask
#endif// sel_offs is 26 bits because it needs to address a 64 MB buffer (~ 20 MB as of writing)// keep the remaining 38 bits for the IMP offset, which may need to reach// across the shared cache. This offset needs to be shifted << 2. We did this// to give it even more reach, given the alignment of source (the class data)// and destination (the IMP)ldr x17, [x10, x9, LSL #3] // x17 == (sel_offs << 38) | imp_offscmp x12, x17, LSR #38.if \Mode == GETIMPb.ne \MissLabelConstant // cache misssbfiz x17, x17, #2, #38 // imp_offs = combined_imp_and_sel[0..37] << 2sub x0, x16, x17 // imp = isa - imp_offsSignAsImp x0ret
.elseb.ne 5f // cache misssbfiz x17, x17, #2, #38 // imp_offs = combined_imp_and_sel[0..37] << 2sub x17, x16, x17 // imp = isa - imp_offs
.if \Mode == NORMALbr x17
.elseif \Mode == LOOKUPorr x16, x16, #3 // for instrumentation, note that we hit a constant cacheSignAsImp x17ret
.else
.abort unhandled mode \Mode
.endif5: ldursw x9, [x10, #-8] // offset -8 is the fallback offsetadd x16, x16, x9 // compute the fallback isab LLookupStart\Function // lookup again with a new isa
.endif
#endif // CONFIG_USE_PREOPT_CACHES.endmacro
1. 流程:
- 获取到指向 cache 和 _bucketsAndMaybeMask;
- 从 _bucketsAndMaybeMask 中分别取出 buckets 和 mask,并由 mask 根据哈希算法计算出哈希下标;
- 根据所得的哈希下标 begin 和 buckets 首地址,取出哈希下标对应的 bucket;
- 进入 do-while 循环,根据 bucket 中的 sel 查找;
通过内存平移获得cache 和 _bucketsAndMaybeMask, _bucketsAndMaybeMask中高16位存mask,低48位存buckets(高16位 | 低48位 = mask | buckets), 即_bucketsAndMaybeMask = mask(高位16) + buckets指针(低48位)。
将objc_msgSend的参数p1(即第二个参数_sel)& mask,通过哈希算法,得到需要查找存储sel-imp的bucket下标begin,即p12 = begin = _sel & mask,因为在存储sel-imp时,也是通过同样哈希算法计算哈希下标进行存储。
static inline mask_t cache_hash(SEL sel, mask_t mask)
{return (mask_t)(uintptr_t)sel & mask;
}
根据计算的哈希下标begin 乘以单个bucket占用的内存大小,得到buckets首地址距离begin下标指向的bucket在实际内存中的偏移量。通过首地址 + 实际偏移量,获取哈希下标begin对应的bucket。bucket是有sel和imp两个属性组成,每个属性都是8个字节的大小,所以bucket的大小是16
在do-while循环中:
- 第一次do-while循环,从begin —> 0 查找一遍,如果没命中,p9不为nil,开始第二次do-while循环;
- 第二次do-while循环,从mask —> 0再次查找一遍;
- 依然如此,则执行__objc_msgSend_uncached —> MethodTableLookup —> _lookUpImpOrForward开始查找方法列表。
在缓存中找到了方法那就直接调用,找到sel就会进入CacheHit,去return or call imp:返回或调用方法的实现(imp)。
2. CacheHit的内容:上图的Mode代表走下面的NORMAL流程,authenticate and call imp意思验证并调用方法实现。
#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2
// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMALTailCallCachedImp x17, x10, x1, x16 // authenticate and call imp//调用imp
.elseif $0 == GETIMPmov p0, p17cbz p0, 9f // don't ptrauth a nil impAuthAndResignAsIMP x0, x10, x1, x16 // authenticate imp and re-sign as IMP
9: ret // return IMP//返回imp
.elseif $0 == LOOKUP// 执行__objc_msgSend_uncached,开始方法列表查找// No nil check for ptrauth: the caller would crash anyway when they// jump to a nil IMP. We don't care if that jump also fails ptrauth.AuthAndResignAsIMP x17, x10, x1, x16 // authenticate imp and re-sign as IMPcmp x16, x15cinc x16, x16, ne // x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)ret // return imp via x17
.else
.abort oops
.endif
.endmacro
3. 缓存中没有找到方法
如果没有找到缓存,查找下一个bucket,一直循环直到找到对应的方法,循环完都没有找到就调用__objc_msgSend_uncached
下面是上述判断跳转代码:
//LGetIsaDone是一个入口
LGetIsaDone:// calls imp or objc_msgSend_uncached//进入到缓存查找或者没有缓存查找方法的流程CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
__objc_msgSend_uncached源码汇编:
STATIC_ENTRY __objc_msgSend_uncachedUNWIND __objc_msgSend_uncached, FrameWithNoSaves// THIS IS NOT A CALLABLE C FUNCTION// Out-of-band p15 is the class to searchMethodTableLookupTailCallFunctionPointer x17END_ENTRY __objc_msgSend_uncached
其中调用了MethodTableLookup宏: 从方法列表中去查找方法。
看一下它的结构:
.macro MethodTableLookupSAVE_REGS MSGSEND// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)// receiver and selector already in x0 and x1mov x2, x16mov x3, #3bl _lookUpImpOrForward// IMP in x0mov x17, x0RESTORE_REGS MSGSEND.endmacro
其中bl表示调用了方法_lookUpImpOrForward,_lookUpImpOrForward在汇编里找不到,因为汇编的函数比C++的多一个下划线,需要去掉下划线,去找到lookUpImpOrForward方法实现
至此快速查找imp汇编部分就结束了,接下来到了慢速查找过程:c/c++环节。
总结消息发送快速查找imp(汇编):
objc_msgSend(receiver, sel, …)
- 检查消息接收者receiver是否存在,为nil则不做任何处理
- 通过receiver的isa指针找到对应的class类对象
- 找到class类对象进行内存平移,找到cache
- 从cache中获取buckets
- 从buckets中对比参数sel,看在缓存里有没有同名方法
- 如果buckets中有对应的sel --> cacheHit --> 调用imp
- 如果buckets中没有对应的sel --> _objc_msgSend_uncached -> _lookUpImpOrForward (c/c++慢速查找)
慢速查找
方法缓冲
苹果认为如果一个方法被调用了,那个这个方法有更大的几率被再此调用,既然如此直接维护一个缓存列表,把调用过的方法加载到缓存列表中,再次调用该方法时,先去缓存列表中去查找,如果找不到再去方法列表查询。这样避免了每次调用方法都要去方法列表去查询,大大的提高了速率。
查找过程
先看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 againruntimeLock.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)) {// 如果是常量优化缓存// 再一次从cache查找imp// 目的:防止多线程操作时,刚好调用函数,此时缓存进来了
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下imp = cache_getImp(curClass, sel); //cache中找IMPif (imp) goto done_unlock; //找到就直接返回了curClass = curClass->cache.preoptFallbackClass();
#endif} else { //如果不是常量优化缓存// 当前类的方法列表。method_t *meth = getMethodNoSuper_nolock(curClass, sel);if (meth) {imp = meth->imp(false);goto done;}// 每次判断都会把curClass的父类赋值给curClassif (slowpath((curClass = curClass->getSuperclass()) == nil)) {// No implementation found, and method resolver didn't help.// Use forwarding.imp = forward_imp;break;}}// 如果超类链中存在循环,则停止。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)) {// 在超类中找到方法。在这个类中缓存它。goto done;}}// 没有实现,尝试一次方法解析器。// 这里就是消息转发机制第一层的入口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 // iOS操作系统且真机的情况下while (cls->cache.isConstantOptimizedCache(/* strict */true)) {cls = cls->cache.preoptFallbackClass();}
#endiflog_and_fill_cache(cls, imp, sel, inst, curClass);}done_unlock:runtimeLock.unlock();if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {return nil;}return imp;
}
方法首先是定义一个消息的转发forward_imp;接着判断类的初始化、加锁、检查是否已知的类…等等,先不管这些。重点在于接下来的for循环:
// unreasonableClassCount()表示循环的上限;for (unsigned attempts = unreasonableClassCount();;) {if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {// 如果是常量优化缓存// 再一次从cache查找imp// 目的:防止多线程操作时,刚好调用函数,此时缓存进来了
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下imp = cache_getImp(curClass, sel);if (imp) goto done_unlock;curClass = curClass->cache.preoptFallbackClass();
#endif} else {// curClass方法列表。method_t *meth = getMethodNoSuper_nolock(curClass, sel);if (meth) {imp = meth->imp(false);goto done;}// 每次判断都会把curClass的父类赋值给curClassif (slowpath((curClass = curClass->getSuperclass()) == nil)) {// 没有找到实现,方法解析器没有帮助。// 使用转发。imp = forward_imp;break;}}// 如果超类链中存在循环,则停止。if (slowpath(--attempts == 0)) {_objc_fatal("Memory corruption in class list.");}// 超类缓存。imp = cache_getImp(curClass, sel);if (slowpath(imp == forward_imp)) {// 在超类中找到forward::条目。// 停止搜索,但不要缓存;调用方法// 首先为这个类解析器。break;}if (fastpath(imp)) {// 在超类中找到方法。在这个类中缓存它。goto done;}}
进入了一个循环逻辑:
- 从本类的method list查找imp(查找的方式是getMethodNoSuper_nolock,一会分析);
- 从本类的父类的cache查找imp(cache_getImp汇编写的)
- 从本类的父类的method list查找imp
- …继承链遍历…(父类->…->根父类)
- 若上面环节有任何一个环节查找到了imp,跳出循环,缓存方法到本类的cache(log_and_fill_cache);
直到查找到nil,指定imp为消息转发,跳出循环。
查找方式
看看在类和父类继承链中查找imp是个什么样的查找方式的(getMethodNoSuper_nolock):
/************************************************************************ getMethodNoSuper_nolock* fixme* Locking: runtimeLock must be read- or write-locked by the caller**********************************************************************/
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;
}
跳转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();// 已排序的二分查找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 resultsif (mlist->isFixedUp()) {for (auto& meth : *mlist) {if (meth.name() == sel) {_objc_fatal("linear search worked when binary search did not");}}}
#endifreturn nil;
}
fastpath()代表大概会走的路径,以下是两种情况的查找。
- findMethodInSortedMethodList:从Sorted可知从已排序的方法列表里查找,采用二分查找。
- findMethodInUnsortedMethodList:从Unsorted可知从未排序方法列表用的线性查找,通过for循环遍历一个个对比sel从而取出method_t:。
看一下findMethodInSortedMethodList函数,跳转findMethodInSortedMethodList,ALWAYS_INLINE代表这是始终内联的。
// 方法内联
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list)
{if (list->isSmallList()) {if (CONFIG_SHARED_CACHE_RELATIVE_DIRECT_SELECTORS && objc::inSharedCache((uintptr_t)list)) {return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.getSmallNameAsSEL(); });} else {return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.getSmallNameAsSELRef(); });}} else {return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.big().name; });}
}
编译后走的是以下流程,这是通过二分查找进行方法查找的。
/************************************************************************ search_method_list_inline**********************************************************************/
template<class getNameFunc>
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{ASSERT(list);// 二分查找// auto 代表自动匹配类型;auto first = list->begin();auto base = first;// decltype: declare type,译为声明类型。这里获取表达式类型;decltype(first) probe;uintptr_t keyValue = (uintptr_t)key;uint32_t count;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;
}
函数获取 probe 的方法名称,并将其与传入的 key 进行比较。如果相等,表示找到了匹配的方法。然后,函数会回溯查找该方法名称在方法列表中的第一个出现位置,以处理类别(Category)对方法的重写。
跳出循环后
done:if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下while (cls->cache.isConstantOptimizedCache(/* strict */true)) {cls = cls->cache.preoptFallbackClass();}
#endiflog_and_fill_cache(cls, imp, sel, inst, curClass);}done_unlock:runtimeLock.unlock();if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {return nil;}return imp;
如果找到了imp,就会把imp缓存到本类cache里(log_and_fill_cache)。(注意这里不管是本类还是本类的父类找到了imp,都会缓存到本类中去)
跳转 log_and_fill_cache :
/***********************************************************************
* log_and_fill_cache
* Log this method call. If the logger permits it, fill the method cache.
* cls is the method whose cache should be filled.
* implementer is the class that owns the implementation in question.
**********************************************************************/
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGINGif (slowpath(objcMsgLogEnabled && implementer)) {bool cacheIt = logMessageSend(implementer->isMetaClass(), cls->nameForLogging(),implementer->nameForLogging(), sel);if (!cacheIt) return;}
#endifcls->cache.insert(sel, imp, receiver);
}
这段代码用于在方法调用时记录消息日志,并将频繁调用的方法缓存起来,以加快后续对该方法的调用速度。
总结消息发送慢速查找imp(c/c++): IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
- 从本类的 method list (二分查找/遍历查找)查找imp;
- 从本类的父类的cache查找imp(汇编);
- 从本类的父类的method list (二分查找/遍历查找)查找imp;
- …继承链遍历…(父类->…->根父类)里找cache和method list的imp;
- 若上面环节有任何一个环节查找到了imp,跳出循环,缓存方法到本类的cache,并返回imp;
- 直到查找到nil,指定imp为消息转发,跳出循环,执行动态方法解析resolveMethod_locked。
消息转发
动态决议
上面介绍了方法调用的本质是消息发送。那如果经过查找后,没有找到方法,系统会怎么处理?这就是接下来介绍的方法动态决议和消息转发。
动态决议过程
当本类和本类继承链下的cache和method list都查找不到imp,imp被赋值成了_objc_msgForward_impcache但是它没有调用,会进入动态方法解析流程,并且只会执行一次。
resolveMethod_locked的源码声明:
/***********************************************************************
* resolveMethod_locked
* Call +resolveClassMethod or +resolveInstanceMethod.
*
* Called with the runtimeLock held to avoid pressure in the caller
* Tail calls into lookUpImpOrForward, also to avoid pressure in the callerb
**********************************************************************/
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); // inst:类对象 cls: 元类if (!lookUpImpOrNilTryCache(inst, sel, cls)) {resolveInstanceMethod(inst, sel, cls);}}// chances are that calling the resolver have populated the cache// so attempt using itreturn lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
这段代码是用于解析方法的函数 resolveMethod_locked。它根据类的类型(元类或非元类)来进行动态方法解析,并尝试从缓存中查找方法实现。
函数的主要逻辑如下:
- 首先,函数会断言运行时锁
runtimeLock已经被持有,以避免在调用过程中产生竞争。 - 接着,函数检查类是否已经被实例化
(realized)。如果类还没有被实例化,那么它不会有任何方法可以解析,直接返回。 - 然后,函数释放运行时锁,以允许其他线程访问运行时。
- 根据类的类型,进行不同类型方法的动态解析:
- 如果类不是元类,表示解析实例方法。调用
[cls resolveInstanceMethod:sel]尝试解析方法。 - 如果类是元类,表示解析类方法。首先调用
[nonMetaClass resolveClassMethod:sel]尝试解析方法,如果找不到,则继续调用[cls resolveInstanceMethod:sel]尝试解析实例方法。
- 如果类不是元类,表示解析实例方法。调用
- 解析完方法后,函数尝试从缓存中查找方法实现。如果能够从缓存中找到方法实现,则直接返回该实现。否则,函数会根据给定的行为
behavior进行进一步处理,可能进行方法转发或查找父类方法实现。
两个方法:resolveInstanceMethod和resolveClassMethod。也称为方法的动态决议。
执行完上述代码后返回lookUpImpOrForwardTryCache:
IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{return _lookUpImpTryCache(inst, sel, cls, behavior);
}
这个方法调用的是_lookUpImpTryCache方法:
ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{runtimeLock.assertUnlocked();if (slowpath(!cls->isInitialized())) {// see comment in lookUpImpOrForwardreturn lookUpImpOrForward(inst, sel, cls, behavior);}IMP imp = cache_getImp(cls, sel);if (imp != NULL) goto done;
#if CONFIG_USE_PREOPT_CACHESif (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) {imp = cache_getImp(cls->cache.preoptFallbackClass(), sel);}
#endifif (slowpath(imp == NULL)) {return lookUpImpOrForward(inst, sel, cls, behavior);}done:if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {return nil;}return imp;
}
进入_lookUpImpTryCache源码,可以看到这里有cache_getImp;也就是说在进行一次动态决议之后,还会通过cache_getImp从cache里找一遍方法的sel。
如果还是没找到(imp == NULL)?也就是无法通过动态添加方法的话,还会执行一次lookUpImpOrForward,这时候进lookUpImpOrForward方法,这里behavior传的值会发生变化。
第二次进入lookUpImpOrForward方法后,执行到if (slowpath(behavior & LOOKUP_RESOLVER))这个判断时:
// 这里就是消息转发机制第一层的入口if (slowpath(behavior & LOOKUP_RESOLVER)) {behavior ^= LOOKUP_RESOLVER;return resolveMethod_locked(inst, sel, cls, behavior);}
根据变化后的behavior值和LOOKUP_RESOLVER值之间的关系导致该if语句内部只能进入第一次,因此这个判断相当于单例。解释了为什么开头说的该动态解析resolveMethod_locked为什么只执行一次。
动态解析测试
resolveClassMethod:默认返回值是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文件中声明一个方法,但在.m文件中并没有实现这个方法。在外部调用这个方法就会导致程序崩溃
原因:
- 第一步查找方法中,在自己的类对象以及父类的类对象中都没有找到这个方法的实现
- 所以转向动态方法解析,动态方法解析我们什么也没做,
- 所以进行第三步,转向消息转发,消息转发我们也什么都没做,最后产生崩溃
此时我们在动态方法解析这一步补救它:
- 当调用的是对象方法时,动态方法解析是在
resolveInstanceMethod方法中实现的 - 当调用的是类方法时,动态方法解析是在
resolveClassMethod中实现的
利用动态方法解析和runtime,我们可以给一个没有实现的方法添加方法实现。
#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface person : NSObject
- (void)test;
@endNS_ASSUME_NONNULL_END#import "person.h"
#import <objc/message.h>
#import <objc/runtime.h>@implementation person
+ (BOOL)resolveInstanceMethod:(SEL)sel {NSLog(@"%s, self = %@", __func__, NSStringFromSelector(sel));return [super resolveInstanceMethod:sel];
}
@end
运行如下:

可以看到为什么会有2次执行呢?放到最后再讲。类方法也是如此。
既然是因为找不到imp而崩溃,那么我们可以在这个方法里通过runtime的class_addMethod,给sel动态的生成imp。其中第四个参数是返回值类型,用void用字符串描述:“v@:”
BOOL
class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{if (!cls) return NO;mutex_locker_t lock(runtimeLock);return ! addMethod(cls, name, imp, types ?: "", NO);
}
方法修改:
+ (BOOL)resolveInstanceMethod:(SEL)sel {NSLog(@"%s, self = %@", __func__, NSStringFromSelector(sel));if (sel == @selector(test)) {IMP imp = class_getMethodImplementation(self.class, @selector(addMethod));class_addMethod(self.class, sel, imp, "v@:");}return [super resolveInstanceMethod:sel];
}- (void)addMethod {NSLog(@"%s", __func__);
}
可以看到运行正常:

消息转发
如果系统在动态决议阶段没有找到实现,就会进入消息转发阶段。
消息快速转发
当cache没有找到imp,类的继承链里的方法列表都没有找到imp,并且resolveInstanceMethod / resolveClassMethod 返回NO就会进入消息转发。
我们在 lookUpImpOrForward 的时候就看到 imp 被指定成了_objc_msgForward_impcache。
//如果上述在类对象和父类对象中没有查到方法//我们就进入动态方法解析if (resolver && !triedResolver) {//triedResolver用来判断是否曾经进行过动态方法解析,如果没有那就进入动态方法解析,如果进行过,就跳过runtimeLock.unlock();_class_resolveMethod(cls, sel, inst); //动态方法解析函数runtimeLock.lock();// Don't cache the result; we don't hold the lock so it may have // changed already. Re-do the search from scratch instead.triedResolver = YES; //进行过动态方法解析就把这个标识为设置为YESgoto retry;//retry是前面的发送消息的过程}//如果动态方法解析失败,就进入消息转发imp = (IMP)_objc_msgForward_impcache; //由这一步进入消息转发cache_fill(cls, sel, imp, inst);
//如果消息转发失败,程序崩溃done:runtimeLock.unlock();
所以如果本类没有能力去处理这个消息,那么就转发给其他的类,让其他类去处理。
看一下进行消息转发的函数__objc_msgForward_impcache的具体实现, 它就是消息转发的流程;又到了我们的源码汇编阶段:
STATIC_ENTRY __objc_msgForward_impcache// Method cache version// THIS IS NOT A CALLABLE C FUNCTION// Out-of-band condition register is NE for stret, EQ otherwise.jne __objc_msgForward_stretjmp __objc_msgForwardEND_ENTRY __objc_msgForward_impcacheENTRY __objc_msgForward// Non-stret versionmovq __objc_forward_handler(%rip), %r11jmp *%r11END_ENTRY __objc_msgForward
但__objc_forward_handler并没有开源。
消息快速转发测试
- Person类中定义func1方法但是不实现,利用-(id)forwardingTargetForSelector:(SEL)aSelector 方法进行消息快速转发
- Blank类中定义func1方法且实现
#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface Person : NSObject
- (void)func1;
@endNS_ASSUME_NONNULL_END#import "Person.h"
#import "Blank.h"
#import <objc/runtime.h>
#import <objc/message.h>@implementation Person- (id)forwardingTargetForSelector:(SEL)aSelector {NSLog(@"%s, aSelector = %@", __func__, NSStringFromSelector(aSelector));if (aSelector == @selector(func1)) {return [Blank alloc];}return [super forwardingTargetForSelector:aSelector];
}
@end#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface Blank : NSObject
- (void)func1;
@endNS_ASSUME_NONNULL_END#import "Blank.h"@implementation Blank
- (void)func1 {NSLog(@"%s", __func__);
}
@end
main.m文件,新建person对象并调用func1方法
#import <Foundation/Foundation.h>
#import "Person.h"
#import <objc/runtime.h>int main(int argc, const char * argv[]) {@autoreleasepool {Person* person = [[Person alloc] init];[person func1];}return 0;
}
运行如下:

转发的作用在于,如果当前对象无法响应消息,就将它转发给能响应的对象。
这时候方法缓存在哪?接收转发消息的对象
应用场景:专门搞一个类,来处理这些无法响应的消息。方法找不到时的crash收集。
演示的是实例方法,如果是类方法,只需要将 - 改成 + ;
消息的慢速转发
如果消息的快速转发也没有找到方法;后面还有个methodSignatureForSelector方法,作用是方法有效性签名。它还需要搭配另一个方法:forwardInvocation
forwardInvocation方法提供了一个入参,类型是NSInvocation;它提供了target和selector用于指定目标里查找方法实现。
将刚才使用快速转发forwardingTargetForSelector方法注释后,添加上methodSignatureForSelector方法和forwardInvocation方法。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {NSLog(@"%s, aSelector = %@", __func__, NSStringFromSelector(aSelector));return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}- (void)forwardInvocation:(NSInvocation *)anInvocation {}
运行正常:

总结
OC方法调用的本质就是消息发送,消息发送是SEL-IMP的查找过程
动态决议
- 通过消息发送机制也找不到方法,系统在进入消息转发前,还会进行动态决议。
实例方法的动态决议
+ (BOOL)resolveInstanceMethod:(SEL)sel;
// 系统通过该方法调用上面OC类里的实现
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
类方法的动态决议
+ (BOOL)resolveClassMethod:(SEL)sel;
消息转发
- 动态决议也找不到方法,才真正进入消息转发环节。
- 动态决议、快速转发、慢速转发合称为三个救命稻草,用于防止方法查找导致的系统崩溃。
消息快速转发
- (id)forwardingTargetForSelector:(SEL)aSelector;
消息慢速转发
// 方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
// 正向调用
- (void)forwardInvocation:(NSInvocation *)anInvocation;
消息转发机制基本上分为三个步骤,也被称为消息的三次拯救:
- 动态方法解析
- 备援接收者
- 完整消息转发
我们可以通过控制这三个步骤其中一环来解决这一个问题
特别注意:如果是正常类的消息,是不会走到这三个步骤的。所以走到这三个不步骤的前提条件已经确定该消息为未知消息
流程图

一些问题
runtime是如何通过selector找到对应的IMP地址的?
缓存查找–>当前类查找–>父类逐级查找
如果子类调用父类方法,缓存在哪个类?
当子类调用父类的方法时,Objective-C runtime 会首先检查子类的方法缓存。如果父类的方法已经存在于子类的方法缓存中,则直接从缓存中获取方法的实现并调用。这样可以提高方法查找的速度,避免了每次调用都需要进行完整的方法查找。
如果子类的方法缓存中不存在父类的方法,则会继续向上查找方法实现。首先会查找父类的缓存,然后是父类的父类,依此类推,直到找到方法的实现或者到达继承链的顶端。
需要注意的是,如果子类重写了父类的方法并且在子类中调用了 super 关键字来调用父类的方法,那么这个方法调用不会使用缓存,而是通过完整的方法查找来确定方法的实现。
总结起来,子类调用父类的方法时,缓存是存储在子类的方法缓存中的,但如果子类重写了父类的方法并使用 super 调用父类的方法,则不会使用缓存。
两次动态决议的原因
打断点测试一下:

运行后,lldb输入指令bt可以看到打印的信息
第一次进入该断点输入bt显示如下:

第二次进入该断点输入bt显示如下:

调用了___forwarding___符号,还有熟悉的慢速转发methodSignatureForSelector方法 ,可知第二次是消息转发;
在消息的第一次动态决议和快速转发都没找到方法后,进入到慢速转发。过程中,runtime还会调用一次lookUpImpOrForward,这个方法里包含了动态决议,这才造成了二次动态决议。
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
