[OC 底层] (三) 方法缓存与消息发送机制
文章目录
-
- [[OC 底层] (三) 方法缓存与消息发送机制](#[OC 底层] (三) 方法缓存与消息发送机制)
- 前言
- 探索方法的本质
-
- SEL
- IMP
- Method
- [SEL, IMP, Method](#SEL, IMP, Method)
- 子类调用父类方法
- objc_msgSend快速查找流程分析CacheLookup
- 慢速查找lookUpImpOrForward
- 动态方法解析
- 消息转发
-
- [快速转发 `forwardingTargetForSelector`](#快速转发
forwardingTargetForSelector) - 慢速转发 (methodSignatureForSelector: / forwardInvocation:)
- [快速转发 `forwardingTargetForSelector`](#快速转发
- 总结
前言
这篇文章的脉络我其实想从消息发送机制开始讲起,在后面我会补充方法缓存相关的内容。
探索方法的本质
在这之前我们要知道什么是消息发送,就比如我们调用了一个方法[person sayHello],但底层并不是像C/C++ 直接静态调用函数,而是会被编译成类似的以下形式:
objc
objc_msgSend(person, @selector(sayHello));
这个本质上就是消息发送。
objc
[person sayHello];
[person sayBye];
[person sayNB];
[person sayMaster];
//clang编译后的底层实现
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayBye"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayMaster"));
通过上面的c 底层代码可以看出,方法调用本质是消息发送,这里我们看到objc_msgSend() 的两个参数:(结构体, sel)。在这里我们要说三个有关消息发送的关键概念就是 消息接收者(要消息接受的对象)、方法选择器(SEL ,表示要调用的方法)和方法实现(本质上是一个函数指针,指向真正执行代码的地址)。
SEL
我们先看Apple公示的 SEL
objc
typedef struct objc_selector *SEL;
SEL是一个不透明的类型,代表方法的选择子,定义如下
objc
// GNU OC 中的 objc_selector
struct objc_selector {
void *sel_id;
const char *sel_types;
};
SEL是一个方法选择器,告诉编译器我们当前需要调用哪一个方法。

定义了三个 SEL 类型的变量:selA、selB、selC,它们都表示同一个方法选择器:sayBaGa。
在这里 oc 会在编译的时候根据方法名生成唯一一个区分的 ID,这个 ID 是 SEL 类型的,只要方法名字相同,SEL 返回的就相同。
在Runtime中维护了一个SEL的表,这个表按NSSet来存储 ,只要相同的SEL就会被看作是同一个方法并被加载到表中,因此OC中要尽量避免方法重载。
IMP
指向方法实现的首地址的指针,从这里可以看出它的一个定义
objc
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
IMP 本质是一个指向函数实现地址的指针。
Method
这同样也是一个不透明的类型,表示类中定义的方法:
objc
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE; //表示方法名的字符串
char * _Nullable method_types OBJC2_UNAVAILABLE; //char* 类型的,表示方法的类型,包含返回值和参数的类型
IMP _Nonnull method_imp OBJC2_UNAVAILABLE; //IMP类型,指向方法实现地址的指针
} OBJC2_UNAVAILABLE;
从中就可以看出 Method 是一个结构体类型指针。
Method操作函数如下
objc
方法操作主要有以下函数:
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types); // 添加方法
Method class_getInstanceMethod(Class cls, SEL name); // 获取实例方法
Method class_getClassMethod(Class cls, SEL name); // 获取类方法
Method *class_copyMethodList(Class cls, unsigned int *outCount); // 获取所有方法的数组
// 替代方法的实现
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types);
// 交换两个方法的实现
method_exchangeImplementations(Method m1, Method m2)
SEL, IMP, Method
objc
Class
└── method list
├── Method 1: SEL + types + IMP
├── Method 2: SEL + types + IMP
└── Method 3: SEL + types + IMP
上面就是一个很直观的 SEL ,Method, IMP 的关系图。
SEL方法选择器,实际上就是一个字符串的名字,编译器生成选择器在类加载时由运行时自动映射。可以理解为书的一个标题
IMP指向方法实现首地址的指针,理解为书的页码
Method是一个结构体,包含上述两者,同时包含char*表示函数的类型(包含返回值和参数类型),理解为书哪一章节的具体内容
SEL、IMP、Method之间的关系
当向对象发送消息时,调用SEL在对象的类以及父类方法列表中进行查找Method,因为Method结构体中包含IMP指针,因此一旦找到对应的Method就直接调用IMP去实现方法
子类调用父类方法
定义子类和父类
首先我们先定义两个类,一个类继承自另一个类
objc
- (void) sayBye {
NSLog(@"%@ say: %s", [self class], __func__);
NSLog(@"%@ say: %s", [super class], __func__);
}
这里的打印结果是


这里我们可以发现为什么我们使用了 superclass 打印出来还是Person。
编译成 c 以后我们可以发现
objc
NSLog((NSString *)&__NSConstantStringImpl__var_folders_rb_0ts7dvhs3zg9fc6kk2nvtlr00000gn_T_CJLPerson_d760c5_mi_2, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("CJLPerson"))}, sel_registerName("class")), __func__);
这里我们可以看到第二条的super变成了objc_msgSendSuper。
苹果官方文档对其方法解释为:当遇到方法调用时,编译器会生成对以下函数之一的调用:objc_msgSend、objc_msgSend_stret、objc_msgSendSuper或objc_msgSendSuper_stret。发送到对象超类的消息(使用super关键字)使用objc_msgSendSuper发送;其他消息使用objc_msgSend发送。使用objc_msgSendSuper_stret和objc_msgSend_stret发送以数据结构作为返回值的方法。
在这里我们可以重新认识一下objc_super数据结构的指针,传递值,标志消息发送的上下文,包括要接受消息的类的实例和要开始搜索方法的实现
objc
((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Person"))}
方法的接受和查找不一定是同一个人,方法的接受者是self,方法的查找是在super中去查找的。
super只是关键字,结构体中的super_class 等于父类,代表从父类对象开始查找;不代表接收者receiver是父类对象,objc_msgSendSuper的区别在于找方法的初始位置不一样。
objc_msgSend快速查找流程分析CacheLookup
下面是源码
objc
MSG_ENTRY _objc_msgSend
// 定义 _objc_msgSend 函数入口。
// Objective-C 中普通方法调用最终都会走到这里,例如:
// [person sayNB] -> objc_msgSend(person, @selector(sayNB))
UNWIND _objc_msgSend, NoFrame
// 栈展开相关信息。
// NoFrame 表示这个函数没有创建标准栈帧。
// objc_msgSend 是高频调用函数,使用汇编实现,尽量减少额外开销。
cmp p0, #0
// 判断消息接收者 receiver 是否为 nil。
// objc_msgSend 的第一个参数是 receiver,通常在 x0 / p0 寄存器中。
//
// 例如:
// objc_msgSend(person, @selector(sayNB))
//
// 此时:
// p0 / x0 = person
// p1 / x1 = @selector(sayNB)
//
// 这里就是判断 person 是否为空。
//
// 同时,这里也和 tagged pointer 的判断有关。
// tagged pointer 是一种特殊的小对象优化,比如某些 NSNumber、NSString 可能不是真正的堆对象。
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged
// 如果支持 tagged pointer:
//
// b.le 表示 less or equal,小于等于时跳转。
// 这里包含两种情况:
//
// 1. receiver == nil
// 2. receiver 是 tagged pointer
//
// 如果满足条件,就跳转到 LNilOrTagged 处理。
//
// 注意:在某些架构下 tagged pointer 的最高位有特殊标记,
// 所以可以通过这个比较判断是否需要进入 tagged pointer 处理流程。
#else
b.eq LReturnZero
// 如果不支持 tagged pointer:
//
// b.eq 表示 equal,也就是 receiver == nil 时跳转。
// 如果 receiver 是 nil,就直接跳转到 LReturnZero。
//
// Objective-C 里向 nil 发送消息不会崩溃,
// 而是直接返回 0 / nil / 空值。
#endif
ldr p14, [x0]
// receiver 不为空,也不是 tagged pointer,就继续执行。
//
// 这里从 receiver 对象内存的起始位置读取 isa。
//
// Objective-C 对象的第一个成员通常就是 isa。
// 所以:
//
// x0 = receiver
// [x0] = receiver->isa
//
// 执行后:
// p14 = raw isa
//
// raw isa 表示原始 isa,里面可能不只是单纯的 Class 指针,
// 还可能包含一些标志位,需要进一步处理。
GetClassFromIsa_p16 p14, 1, x0
// 从 raw isa 中提取真正的 Class。
//
// 因为现代 Objective-C 使用 non-pointer isa,
// isa 里面不一定直接就是类对象地址,
// 它可能还包含引用计数、标志位等信息。
//
// 所以这里需要通过掩码等方式,从 raw isa 里取出真正的 class。
//
// 执行后:
// p16 = class
//
// 也就是:
// receiver -> isa -> Class
//
// 到这里,Runtime 已经知道应该从哪个类开始查找方法了。
LGetIsaDone:
// 标签:表示 isa / class 获取完成。
//
// 普通对象会从上面的 GetClassFromIsa_p16 走到这里。
// tagged pointer 对象经过 GetTaggedClass 后,也会跳到这里。
//
// 换句话说,到这个位置时,p16 里应该已经有了 class。
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
// 进入方法缓存查找流程。
//
// 这是 objc_msgSend 的核心逻辑之一。
//
// 当前关键寄存器大概是:
//
// x0 / p0 = receiver
// x1 / p1 = selector,也就是 _cmd / SEL
// x16 / p16 = class
//
// CacheLookup 会去 class 的 cache 里查找:
//
// SEL -> IMP
//
// 如果缓存命中:
// 直接跳转到对应 IMP 执行方法实现。
//
// 如果缓存未命中:
// 跳转到 __objc_msgSend_uncached,
// 进入慢速查找流程。
//
// 慢速查找大致会去:
//
// class -> data -> method list
//
// 里用 SEL 找 Method,再从 Method 中取出 IMP。
// 如果本类没有,就沿着 superclass 往父类查找。
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
// 这里处理两种情况:
//
// 1. receiver == nil
// 2. receiver 是 tagged pointer
b.eq LReturnZero
// 如果 receiver == nil,跳转到 LReturnZero。
//
// 也就是说,向 nil 发送消息,直接返回 0。
//
// 例如:
//
// Person *p = nil;
// [p sayNB];
//
// 不会崩溃,而是直接返回。
GetTaggedClass
// 如果不是 nil,那就说明它是 tagged pointer。
//
// tagged pointer 不是真正的普通对象,
// 不能像普通对象那样通过 [x0] 读取 isa。
//
// 所以这里要通过 tagged pointer 的编码规则,
// 获取它对应的 Class。
//
// 执行后,同样会得到 class 信息。
b LGetIsaDone
// tagged pointer 的 class 获取完成后,
// 跳回 LGetIsaDone。
//
// 后续流程和普通对象一样:
//
// class -> cache -> SEL 查 IMP
// SUPPORT_TAGGED_POINTERS
#endif // SUPPORT_TAGGED_POINTERS
LReturnZero:
// 处理 receiver == nil 的返回逻辑。
//
// Objective-C 允许向 nil 发送消息。
// 这种情况下,不查方法、不走消息转发,直接返回空值。
//
// 根据返回值类型不同,可能需要清空不同的寄存器。
// x0 is already zero
// x0 本身已经是 0。
//
// 如果返回对象指针、整数、BOOL 等,
// x0 = 0 就表示 nil / 0 / NO。
mov x1, #0
// 把 x1 也清零。
// 某些返回场景可能需要多个通用寄存器配合返回。
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
// 把浮点 / 向量返回寄存器清零。
//
// 如果方法返回的是 float、double、结构体中的浮点相关值,
// 这些寄存器可能会被用作返回值。
//
// 向 nil 发送消息时,也应该返回 0 值,
// 所以这里统一清空。
ret
// 返回调用方。
//
// receiver 为 nil 的消息发送流程到这里结束。
END_ENTRY _objc_msgSend
// _objc_msgSend 函数结束。
这里的主要流程是这些
objc_msgSend(receiver, SEL)
|
v
判断 receiver 是否为 nil
|
|-- nil -> 直接返回 0
|
v
判断是否 tagged pointer
|
|-- tagged pointer -> 通过特殊规则拿到 Class
|
v
普通对象 -> 从 receiver 的 isa 中取出 Class
|
v
拿 Class 和 SEL 去 cache 里查 IMP
|
|-- cache 命中 -> 直接执行 IMP
|
|-- cache 未命中 -> 进入 __objc_msgSend_uncached 慢速查找
- 在这块有一个很重要的一点也就是第一部分,就是怎么拿到Class,这也就是 objc_msgSend 的前半段准备工作,我们需要先判断 receiver 是否为 nil 或 tagged pointer;如果是 nil 就直接返回,如果是 tagged pointer 就特殊获取 Class;如果是普通对象,就从 receiver 的 isa 中解析出 Class,为后面的 cache 查找方法实现做准备。
- 然后下面是第二部分,也就是快速查找的函数实现部分:
objc
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
// 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_offs
cmp x12, x17, LSR #38
.if \Mode == GETIMP
b.ne \MissLabelConstant // cache miss
sbfiz x17, x17, #2, #38 // imp_offs = combined_imp_and_sel[0..37] << 2
sub x0, x16, x17 // imp = isa - imp_offs
SignAsImp x0, x17
ret
.else
b.ne 5f // cache miss
sbfiz x17, x17, #2, #38 // imp_offs = combined_imp_and_sel[0..37] << 2
sub x17, x16, x17 // 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, x10
ret
.else
.abort unhandled mode \Mode
.endif
5: ldur x9, [x10, #-16] // offset -16 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
这个方法最核心的流程就是
Class -> cache -> buckets -> 用 SEL 匹配 bucket -> 找到 IMP -> 执行
里面的内容也就是,从 Class 中取出 cache 的 buckets 和 mask,用 SEL 计算 bucket 下标,然后在 buckets 里按开放寻址规则查找匹配的 SEL;如果找到,就取出对应 IMP 并直接执行;如果遇到空 bucket 或完整扫描后仍未找到,就跳到慢速查找。
慢速查找lookUpImpOrForward
这个慢速查找就是在如果没有存放在缓存中的话,就会到慢速查找中。下面是这个的相关函数
objc
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;
lockdebug::assert_unlocked(&runtimeLock.get());
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
lockdebug::assert_locked(&runtimeLock.get());
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().
// Has this class been disabled? Act like a message to nil.
if (!cls || !cls->ISA()) {
#if __arm64__
imp = _objc_returnNil;
goto done;
#elif __x86_64
if (behavior & LOOKUP_FPRET)
imp = _objc_msgNil_fpret;
else if (behavior & LOOKUP_FP2RET)
imp = _objc_msgNil_fp2ret;
else
imp = _objc_msgNil;
// We can't cache these on x86, in case some other caller tries sending
// this selector with a different return type. If we con't cache then we
// always come back here, and always choose the correct IMP for the
// caller's expected return type.
behavior |= LOOKUP_NOCACHE;
goto done;
#else
#error Don't know how to handle messages to disabled classes on this target.
#endif
}
for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// curClass method list.
method_t *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);
}
#if CONFIG_USE_PREOPT_CACHES
done_unlock:
#endif
runtimeLock.unlock();
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
下面是完整的流程图
objc
objc_msgSend
|
v
CacheLookup 快速查找
|
|-- cache 命中 -> 直接执行 IMP
|
|-- cache 未命中
|
v
__objc_msgSend_uncached
|
v
lookUpImpOrForward
|
v
检查类状态 / 加锁 / realize / initialize
|
v
curClass = cls
|
v
查 curClass 的 method list
|
|-- 找到 Method -> 取 IMP -> done
|
|-- 没找到
|
v
curClass = curClass->superclass
|
|-- nil -> imp = forward_imp -> break
|
v
查父类 cache
|
|-- 命中 IMP -> done
|
|-- 命中 forward_imp -> break
|
|-- 未命中 -> 下一轮查父类 method list
|
v
如果整个继承链都没找到
|
v
动态方法解析 resolveMethod_locked
|
v
如果还是没处理 -> 消息转发
|
v
找到 IMP 后写入 cls.cache
|
v
返回 IMP
在这里我们在类和父类继承链中查找IMP的时候,有一个重要的函数逻辑,就是查找的相关方法
getMethodNoSuper_nolock方法
这块有方法调用链
objc
getMethodNoSuper_nolock(curClass, sel)
-> search_method_list_inline(method_list, sel)
-> findMethodInSortedMethodList(sel, method_list) // 有序:二分查找
-> findMethodInUnsortedMethodList(sel, method_list) // 无序:线性遍历
-> method_t
-> meth->imp(false)
这个二分查找只是查找一个已经有序表的调用的函数
objc
template<class compareFunc>
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const compareFunc &compare)
{
ASSERT(list);
auto first = list->begin();
auto base = first;
uint32_t count;
// When to stop the binary search and move to a linear search.
const uint32_t threshold = 4;
for (count = list->count; count > threshold; count >>= 1) {
auto probe = base + (count >> 1);
int comparison = compare(probe);
if (comparison == 0) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
while (probe > first && compare(probe - 1) == 0) {
probe--;
}
return &*probe;
}
if (comparison > 0) {
base = probe + 1;
count--;
}
}
// Once we've shrunk the range enough, it's faster to do a linear search.
while (count-- > 0) {
auto comparison = compare(base);
if (comparison == 0)
return &*base;
if (comparison < 0)
return nil;
base++;
}
return nil;
}
下面有个图可以直接来解说这个比较重要的查找函数

我们可以给出一个较为完整的流程来去看一下所有函数调用的流程,
在当前类 curClass 中,根据 sel 查找方法;
具体查找时,会遍历当前类的方法列表;
如果某个方法列表是有序的,就用二分查找;
如果方法列表是无序的,就用线性遍历;
找到后得到 method_t;
再从 method_t 中取出 IMP;
最后执行这个 IMP`
动态方法解析
前面在提到查找流程的时候,我们如果在前两次都没找到对应的方法,这个时候还有一个补救方法就是动态方法解析。
下面是相关源码
objc
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
lockdebug::assert_locked(&runtimeLock.get());
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);
}
这块有几个重要内容
- 如果是类,执行实例方法的动态决议方法resolveInstanceMethod
- 如果是元类,执行类方法的动态决议方法resolveClassMethod,如果在元类中没有找到或者未空,则在元类的实例方法的动态决议方法resolveInstanceMethod(因为类反复该在元类中是实例方法),所以还需要查找元类中实例方法的动态决议.
- 如果动态方法决议中,将其指向了其他方法,则要继续查找对应的一个imp,就是慢速查找流程

实例方法
下面是实例方法的源码
objc
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
lockdebug::assert_unlocked(&runtimeLock.get());
ASSERT(cls->isRealized());
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
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));
}
}
}
在这里他多次调用了lookUpImpOrNilTryCache,我们可以看一下这个函数的逻辑:
objc
IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
{
return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
lockdebug::assert_unlocked(&runtimeLock.get());
if (slowpath(!cls->isInitialized())) {
// see comment in lookUpImpOrForward
return lookUpImpOrForward(inst, sel, cls, behavior);
}
IMP imp = cache_getImp(cls, sel);
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)) {
// dtrace probe
OBJC_RUNTIME_CACHE_MISS(inst, sel, cls);
return lookUpImpOrForward(inst, sel, cls, behavior);
}
done:
if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
return nil;
}
return imp;
}
我们可以总结一下里面的流程:
- 确认没加锁
- 确认 cls 已 realized
- 准备
resolve_sel = @selector(resolveInstanceMethod:) - 查找 cls 的元类链里有没有
+resolveInstanceMethod: - 如果没有,直接 return
- 如果有,主动调用
[cls resolveInstanceMethod:sel] - 开发者可以在这个方法里
class_addMethod - 调用完成后,重新查找原来的 sel
- 如果找到 IMP,说明动态解析成功
- 如果没找到,后续会进入消息转发
类方法
resolveClassMethod: 是在类方法找不到的时候用
objc
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
lockdebug::assert_unlocked(&runtimeLock.get());
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);
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
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
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));
}
}
}
下面是 class 相关的流程
objc
[Person sayHello]
|
v
Person 元类 cache 查找失败
|
v
Person 元类 method list 查找失败
|
v
父元类链查找失败
|
v
进入 resolveClassMethod
|
v
查找 +resolveClassMethod: 是否存在
|
v
调用 [Person resolveClassMethod:@selector(sayHello)]
|
v
class_addMethod(object_getClass(self), @selector(sayHello), imp, types)
|
v
重新查找 Person 元类中的 sayHello
|
v
找到 IMP
|
v
动态解析成功
|
v
返回 IMP
|
v
执行 IMP
流程小结:
- 会从元类的继承链里找resolveClassMethod的imp;
如果本类里实现了+(Bool)resolveClassMethod方法,则在元类里能找到imp;
如果本类里没有实现它,则最终在根元类NSObject里找imp,因为NSObject类里默认声明了+(Bool)resolveClassMethod方法实现。 - 获得这个类的元类,进入这个getMaybeUnrealizedNonMetaClass方法里面,将这个nonmeta赋值成类对象
- 主动发送resolveClassMethod,不管是在那一个层级找到的都会把它赋值到我们的本类的一个cache中
- 获取之前发送的一个imp.
- 这个resolveClassMethod不生效之后,再去在元类中查找那个实例方法resolveInstanceMethod(因为类方法在元类中是以实例方法的样式来存储的)
崩溃修改
我们其实可以实现一个保底机制
objc
#import <Foundation/Foundation.h>
#import "LKPerson.h"
#import "LKPerson2.h"
@interface Person : NSObject
@property (nonatomic, copy) NSString *LK_Name;
@property (nonatomic, strong) NSString *nickName;
- (void) sayHello;
- (void) sayCode;
- (void) sayMaster;
- (void) sayBaGa;
- (void) sayBye;
@end
@implementation Person
//- (void) sayHello {
// NSLog(@"Person say : %s", __func__);
//}
- (void)sayCode {
NSLog(@"Person say : %s", __func__);
}
- (void)sayBaGa {
NSLog(@"Person say : %s", __func__);
}
- (void)sayMaster {
NSLog(@"Person say : %s", __func__);
}
//- (void) sayBye {
// NSLog(@"%@ say: %s", [self class], __func__);
// NSLog(@"%@ say: %s", [super class], __func__);
//}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"%@, 保底机制", NSStringFromSelector(sel));
// if (sel == @selector(sayHello)) {
// IMP imp = class_getMethodImplementation(self, @selector(sayNB));
// Method sayMethod = class_getInstanceMethod(self, @selector(sayNB));
// const char* type = method_getTypeEncoding(sayMethod);
// return class_addMethod(self, sel, imp, type);
// }
return [super resolveInstanceMethod:sel];
}
@end
@interface Teacher : Person
@end
@implementation Teacher
@end
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// Person *p = [[Person alloc] init];
// NSObject *obj1 = [NSObject alloc];
// LKPerson2 *obj3 = [LKPerson2 alloc];
// NSLog(@"Hello, World!");
// NSObject *obj1 = [NSObject alloc];
// LKPerson *obj2 = [LKPerson alloc];
// LKPerson *obj4 = [LKPerson alloc];
// Person *person = [Person alloc];
// NSLog(@"%@", person);
// NSLog(@"hello world %@ - %@", obj1, obj2);
Person *p = [Person alloc];
Class pClass = [Person class];
[p sayBye];
[p sayHello];
// [p sayHello];
// [p sayCode];
// [p sayMaster];
// NSLog(@"%@", pClass);
// SEL selA = @selector(sayBaGa);
// SEL selB = sel_registerName("sayBaGa");
// SEL selC = NSSelectorFromString(@"sayBaGa");
// NSLog(@"123");
// [p sayBaGa];
}
return 0;
}
动态方法添加函数
objc
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
我们先解释一下这个的用意,给 cls 这个类添加一个叫 name 的方法这个方法执行 imp 指向的代码
方法的返回值和参数类型由 types 描述
然后我们来介绍一下这个核心方法,我们先从参数开始,
Class cls:给哪个类添加方法
SEL name:要添加的方法名
IMP imp:方法的真实实现
const char *types:方法类型编码
最后我们看返回值,返回 YES 表示添加成功。返回 NO 通常表示这个类中已经有这个方法了,添加失败。
这时我们回过头来看一下,我们把 +resolveInstanceMethod:(SEL)sel 这个方法这么写
objc
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"%@, 保底机制", NSStringFromSelector(sel));
if (sel == @selector(sayHello)) {
IMP imp = class_getMethodImplementation(self, @selector(sayBaGa));
Method sayMethod = class_getInstanceMethod(self, @selector(sayBaGa));
const char* type = method_getTypeEncoding(sayMethod);
return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod:sel];
}
就可以真正意义上实现一个保底的效果:

消息转发
快速转发 forwardingTargetForSelector
我们先看下快速转发的入口在哪里
objc
- (id)forwardingTargetForSelector:(SEL)aSelector;
快速转发,它的本质也就是把当前的消息交转给另一个对象处理
objc
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(sayHello)) {
return [Teacher new]; // 返回其他类中实现过这个方法的方法.
}
return [super forwardingTargetForSelector:aSelector];
}
这样就可以让 class1 转发到 class2 了。
慢速转发 (methodSignatureForSelector: / forwardInvocation:)
这个是最后一次挽救的机会,那么如何会进入一个慢速转发呢?如果原对象的快速转发没有返回有效的新接收者,那么原对象就会进入慢速转发。
如果是如下情况就会进入慢速转发
objc
- (id)forwardingTargetForSelector:(SEL)aSelector {
return [super forwardingTargetForSelector:aSelector];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil;
}
下面是慢速转发需要实现的方法
objc
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
上面的这两个方法是都需要实现的,如果需要实现慢速转发的话
- 第一个方法 这个方法的作用是:告诉 Runtime,这个未知方法的参数和返回值长什么样。
- 第二个是建立在第一个以上如果
methodSignatureForSelector:返回了有效的方法签名,Runtime 就会把原来的消息调用封装成一个NSInvocation对象,然后调用:- (void)forwardInvocation:(NSInvocation *)anInvocation;这个NSInvocation里面包含了这次调用的完整信息,比如:
objc
target:原始接收者
selector:原始方法名
arguments:参数
return value:返回值位置
method signature:方法签名
总结
其实一句话就可以概括完消息转发就是先查缓存和方法列表,找不到就动态解析,再不行就消息转发,最后仍没人处理才崩溃。
最终最终的本质就是一个消息发送,SEL-IMP 的查找过程。
最后拿两张图来结尾
