【iOS】源码学习-消息流程分析
前言
在OC中,方法调用并不是直接调用函数,而是一次消息发送。底层被转换为objc_msgSend,其核心任务是根据消息接收者receiver和方法选择器SEL,找到真正的方法实现IMP。
Runtime介绍
区分一下运行时和编译时:
- 运行时:代码跑起来被装载到内存中的过程。如果此时出错,则程序崩溃,是一个动态阶段。
- 编译时:源代码翻译成机器能识别的代码的过程。主要对语言进行最基本的检查报错,即词法分析、语法分析等,是一个静态阶段。
Runtime称为运行时。
Runtime的使用方式有以下三种:
- 通过OC代码,例如
[person satHello] - 通过NSObject方法,例如
isKindOfClass - 通过Runtime API,例如
class_getInstanceSize
这三种方式底层的关系如下图:

compiler:编译器,即LLVM。
具体的说:OC的alloc对应底层的objc_alloc,Runtime System Libarary就是底层库。
动态特点
接着从以下三个方面理解为什么OC是动态语言。
动态类型
OC中的对象是动态类型的,这意味着我们可以在运行的时候发送信息给对象,对象可以根据接受到的类型执行方法。与静态语言类型不同,静态类型在编译时就必须要确定引用哪种对象,而动态类型更加泛化。
objc
// id是一个struct objc_object*类型指针
id obj = [[NSString alloc] initWithString:@"Hello world"];
obj = [[NSDate alloc] init];
动态绑定
动态绑定是指方法调用可以在运行时解析,而不是在编译时。这意味着OC对象需要在运行时决定要执行对象的哪个方法,而不是编译时。这种机制是通过消息传递实现的,使得我们可以在程序运行期间改变对象的调用方法。
动态语言
OC能被称为动态语言的核心原因就是消息转发机制,消息转发机制允许开发者截取并处理未被对象识别的信息。
方法本质
现在我们知道了Runtime是什么之后,我们就知道方法调用本质为什么是消息发送了。
具体实例看一下:
objc
[lyd sayHello];
clang编译后的底层实现:
cpp
((void (*)(id, SEL))(void *)objc_msgSend)((id)lyd, sel_registerName("sayHello"));
由此看出,方法调用的本质是objc_msgSend(结构体指针, SEL)消息发送。
SEL
objc
typedef struct objc_selector *SEL;
这是apple官方公示的SEL。它是一个不透明的容器类型,是代表方法的选择子。
其定义如下:
GNU OC中的objc_selector
objc
struct objc_selector {
void *sel_id;
const char *sel_types;
};
SEL其实就是一个方法选择器,告诉编译器当前我们想要调用哪个方法。在运行时,方法选择器用来表示方法的名字,一个方法选择器就是一个C字符串。在OC的运行时被注册,编译器生成选择器在类加载时由运行时自动映射。可以在运行时添加新的选择器,并使用sel_registerName函数检索现有的选择器。

这里OC会在编译时根据方法名生成一个唯一区分的ID,这个ID是SEL类型的。只要方法签名相同,SEL返回的就相同。在Runtime中维护了一个SEL的表,这个表按照NSSet来存储,只要SEL相同就会被看作同一个方法并加载到表中。因此在OC开发中要尽量避免方法重载。
方法重载:同一个类里,方法名完全一样,但参数不同。
IMP
objc
#if !OBJC_OLD_DISPATCH_PROTOTYPES // 新版
typedef void (*IMP)(void /* id, SEL, ... */ ); // 为了类型安全和ARC,隐藏参数声明
#else // 旧版
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
// p1:调用方法对象
// p2:方法名
// ...:任意可变参数
#endif
由此可见,IMP本质是OC方法对应的C函数指针,指向方法实现开始的位置。
由于默认的IMP定义为无参数无返回值,因此如果直接调用会报错:

正确的做法是根据方法的真实签名进行强制转换:
objc
MyClass *myClass = [[MyClass alloc] init];
// 获取IMP
IMP imp = [myClass methodForSelector:@selector(calculateSum:with:)];
// 定义正确的函数指针类型
typedef int (*FuncType)(id, SEL, int, int);
// 强制转换
FuncType func = (FuncType)imp;
// 调用
int result = func(myClass, @selector(calculateSum:with:), 1, 2);
Method
Method是一个结构体,包含SEL和是不透明的类型,表示类中定义的方法。
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; // 方法的类型,包含返回值和参数的类型
IMP _Nonnull method_imp OBJC2_UNAVAILABLE; // IMP类型,指向方法实现地址的指针
} OBJC2_UNAVAILABLE;
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 m1 = class_getInstanceMethod([MyClass class], @selector(m1));
Method m2 = class_getInstanceMethod([MyClass class], @selector(m2));
method_exchangeImplementations(m1, m2);
SEL、IMP、Method之间的关系是:
当向对象发送消息时,调用SEL在对象的类以及父类方法列表中进行查找Method,一旦找到对应的Method,直接调用其结构体中IMP去实现方法。
子类调用父类的方法
了解过普通方法的消息发送原理后,再看子类调用父类的方法。
通过一个实例:
objc
#import "LYDPerson.h"
@interface LYDTeather : LYDPerson
- (void)sayBye;
@end
@implementation LYDTeather
- (void)sayBye {
NSLog(@"self class = %@, %s", [self class], __func__);
NSLog(@"super class = %@, %s", [super class], __func__);
}
打印结果是:

我们可以看到两个方法打印出来是相同的。我们将其编译为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 class被编译为objc_msgSendSuper。
对此,苹果官方的解释是:
当遇到方法调用时,编译器会生成对以下函数之一的调用:
- objc_msgSend
- objc_msgSend_stret
- objc_msgSendSuper
- objc_msgSendSuper_stret
其中,除了发送到对象超类的消息使用objc_msgSendSuper发送,其他消息都使用objc_msgSend发送。以数据结构作为返回值的消息使用objc_msgSend_stret和objc_msgSendSuper_stret。
我们看objc_msgSendSuper源码,发现该方法需要两个参数:objc_super结构体和SEL。
objc
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
再看objc_super源码,同样需要两个参数:receiver和super_class。
objc
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
__unsafe_unretained _Nonnull Class super_class;
/* super_class is the first class to search */
};
#endif
super_class表示从父类对象开始查找,不代表接受者receiver就是父类对象。
cache
我们现在已经知道了方法调用其实底层就是在找SEL-IMP,而cache就是加速了这个查找。
cache结构
通过看cache_t的源码,我们发现分成了3个架构的处理:
CACHE_MASK_STORAGE_OUTLINED:表示运行的环境是模拟器或者macOSCACHE_MASK_STORAGE_HIGH_16:表示运行环境是64位真机CACHE_MASK_STORAGE_LOW_4:表示运行环境是非64位真机
其中真机的架构中,mask和bucket写在一起,目的是为了优化,可以通过各自掩码来获取相应的数据。
objc
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED // macOS、模拟器
explicit_atomic<struct bucket_t *> _buckets;
explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 // 64位真机
explicit_atomic<uintptr_t> _maskAndBuckets;//写在一起的目的是为了优化
mask_t _mask_unused;
// 掩码省略....
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 // 非64位真机
explicit_atomic<uintptr_t> _maskAndBuckets;
mask_t _mask_unused;
// 掩码省略....
#else
#error Unknown cache mask storage type.
#endif
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
//方法省略.....
}
再看bucket_t的源码,同样分为两个版本:真机和非真机。区别在于sel和imp的顺序不同。
objc
struct bucket_t {
private:
// IMP-first is better for arm64e ptrauth and no worse for arm64.
// SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__ // 真机
// explicit_atomic:加了原子性保护
explicit_atomic<uintptr_t> _imp;
explicit_atomic<SEL> _sel;
#else // 非真机
explicit_atomic<SEL> _sel;
explicit_atomic<uintptr_t> _imp;
#endif
因此,我们可知cache中缓存的是sel-imp 。整体结构如下:

cache探索
通过LLDB调试我们知道:
- cache属性的获取是通过首地址平移16字节,即首地址+0x10获取cache的地址
- sel-imp是在cache_t的_buckets属性中。
- 获取_buckets属性就可以通过相应获取方法sel()和imp()来获取sel-imp了。
然后看一下如何在cache中插入一个新缓存的方法:
objc
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
// 确保调用此函数时已经持有全局运行时锁
lockdebug::assert_locked(&runtimeLock.get());
// Never cache before +initialize is done
// 避免类还没有完成初始化而缓存
if (slowpath(!cls()->isInitialized())) {
return;
}
// 检查是否是预缓存优化
if (isConstantOptimizedCache()) {
_objc_fatal("cache_t::insert() called with a preoptimized cache for %s",
cls()->nameForLogging());
}
#if DEBUG_TASK_THREADS
return _collecting_in_critical();
#else
#if CONFIG_USE_CACHE_LOCK
mutex_locker_t lock(cacheUpdateLock); // 资源获取即初始化风格的锁守卫对象。在构造函数中自动获取互斥锁,析构函数中自动释放锁。专门保护缓存更新的锁可以减少全局锁的持有时间,提高并发性能。
#endif
ASSERT(sel != 0 && cls()->isInitialized());
// Use the cache as-is if until we exceed our expected fill ratio.
// 第一次insert的时候occupied是0,newOccupied是1
mask_t newOccupied = occupied() + 1;
// capacity的值就是buckets的长度
unsigned oldCapacity = capacity(), capacity = oldCapacity;
// 如果cache为空,则分配buckets(arm64下长度为2,x86_64下长度为4)
// reallocate中无需释放老buckets
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
// 给容量加上初始值,x86_64为4,arm64为2
if (!capacity) capacity = INIT_CACHE_SIZE;
reallocate(oldCapacity, capacity, /* freeOld */false);
}
// 在arm64下,缓存的大小<=buckets长度的7/8 ----> 不扩容
// 在86_64下,缓存的大小<=buckets长度的3/4 ----> 不扩容
else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
// Cache is less than 3/4 or 7/8 full. Use it as-is.
}
#if CACHE_ALLOW_FULL_UTILIZATION // 只有arm64才需要走这个判断
// 在arm64下,buckets的长度<=8 ----> 不扩容
else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
// Allow 100% cache utilization for small buckets. Use it as-is.
}
#endif
// 扩容逻辑
else {
// 对当前容量的2倍扩容。如果扩容后容量大小大于一个最大阈值,则设置这个为最大值
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
// 创建新的扩容后的buckets,释放旧的buckets
reallocate(oldCapacity, capacity, true);
}
bucket_t *b = buckets(); // 获取buckets数组指针
mask_t m = capacity - 1; // m是buckets的长度-1
mask_t begin = cache_hash(sel, m); // 通过hash计算出要插入的方法在buckets上的起始位置
mask_t i = begin;
// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot.
do {
// 当前hash计算出来的buckets在i的位置没有值
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
// 没值就去存方法
b[i].set<Atomic, Encoded>(b, sel, imp, cls());
return;
}
// 当前hash计算出来的buckets在i的位置sel有值且等于要存储的sel,说明该方法已有缓存
if (b[i].sel() == sel) {
// The entry was added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
return;
}
// 如果计算出来的起始位置i存在哈希冲突的话,就通过cache_next去改变i值(增大i)
} while (fastpath((i = cache_next(i, m)) != begin));
bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}
这个过程的整个逻辑顺序总结一下就是:
- 确保线程安全,确保类已经初始化成功,确保不是预编译的只读cache
- 判断是否需要扩容,获取当前已经缓存方法数量
- 第一次插入时需要先初始化缓存
- 判断是否需要扩容,扩容到对应方法(如果超出了最大的缓存大小,就把旧方法中所有缓存全部清除,用新的缓存替换)
- 找到哈希插入的位置
- 插入方法(如果 之前已经插入过就不继续插入,如果哈希冲突了就处理冲突逻辑问题,如果插入失败了就进入异常处理)
再具体看一下处理扩容逻辑:
获取到buckets数组指针,得到buckets最大的下标m,并且通过cache_hash方法计算出要缓存的方法在buckets中的起始位置。然后通过下面这个hash函数来处理它的下标值,该方法确保起始位置一定在buckets最大下标里。
objc
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
// 把SEL转化成unsigned long类型,一个很大的数字,然后通过哈希计算压缩成数组大小范围内的数
uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
value ^= value >> 7; // 位运算使得哈希分布更加均匀
#endif
// value是一个很大的数,mask=buckets长度-1
// 一个很大的数&一个小的数得到的结果是这个小的数
return (mask_t)(value & mask);
}
这里可能会出现哈希冲突,因此进入一个处理哈希冲突的函数:
objc
#if CACHE_END_MARKER
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}
#elif __arm64__
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;
}
#else
#error unexpected configuration
#endif
解决哈希冲突后,再去根据方法的SEL计算哈希下标,遍历哈希桶对应位置,判断该位置是否已经有缓存方法。若桶中已有相同的SEL,则说明方法已存在缓存,可以直接复用。
方法缓存不是连续存放的。这是因为底层采用buckets哈希桶结构,每个SEL的存储位置由哈希算法计算得出,并非按顺序线性分配。
一旦扩容之后,会先创建新的buckets,然后遍历旧数组每一个有效条目,重新计算旧数组的每一个方法的哈希然后插入新数组,最后将cache_t的指针指向新数组,最后释放旧内存。
消息发送流程
有了前面的铺垫,现在我们正式看一下消息发送的流程。
查找objc_msgSend源码,发现是汇编实现

objc
// 消息发送的汇编入口
// objc_msgSend主要是拿到接收者的isa信息
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
// 判断接收者是否存在,其中p0是objc_msgSend的第一个参数,即消息接收者receiver
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
//---- le小于 --支持taggedpointer(小对象类型)的流程
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
// p0为空的时候直接返回空,也就是接受者为nil的时候消息不会被做一个处理
b.eq LReturnZero
#endif
//取出isa指针
ldr p13, [x0] // p13 = isa
//---- 在64位架构下通过 p16 = isa(p13) & ISA_MASK,拿出shiftcls信息,得到class信息,获得isa中的一个类信息
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
//如果有isa.走到cacheLookup方法面也就我们这里的核心部分快速查找流程
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
//等于空返回空
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
objc_msgSend使用汇编实现的原因:
- 执行速度快,满足方法调用高频需求
- 直接操作寄存器,不破坏上层函数调用栈
- 跨版本、跨架构稳定,不依赖编译器优化
- 能精准控制寄存器,保证消息发送稳定
总结一下主要步骤:
- 检查receiver是否为nil/tagged pointer
- 如果是普通对象,从对象内存中取出isa
- 由isa还原出class(通过掩码、移位)
- 进入CacheLookup做SEL->IMP的快速缓存查找
寄存器:CPU内部高速临时存储单位,速度远快于内存,用来存放函数参数、内存地址、运算中间值,汇编底层依赖寄存器保证极致执行效率。
消息发送会先通过缓存进行查找方法实现,如果在缓存中没有找到方法实现,就会进入慢速查找过程,去类的方法列表以及父类链中进行循环查找。
快速查找CacheLookup
重点看快速查找部分的函数实现:
objc
.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
//
// 保存原始isa到x15,防止后续x16被修改丢失原值
mov x15, x16 // stash the original isa
LLookupStart\Function:
// p1 = SEL方法名,p16 = 类的isa指针
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
// 从isa偏移16字节(CACHE)读取cache结构体,存入p10
ldr p10, [x16, #CACHE] // p10 = mask|buckets
// p10右移48位,取出高16位mask存入p11
lsr p11, p10, #48 // p11 = mask
// p10低48位保留,得到buckets数组首地址
and p10, p10, #0xffffffffffff // p10 = buckets
// 哈希计算:sel & mask 得到查找下标存入w12
and w12, w1, w11 // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
// isa偏移16字节读取cache,p11 = mask(高16)+buckets(低48)
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
// 判断是否开启预优化缓存,是则跳转到LLookupPreopt
tbnz p11, #0, LLookupPreopt\Function
// 提取低48位作为buckets首地址
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
// 非ptrauth架构,提取buckets地址
and p10, p11, #0x0000fffffffffffe // p10 = buckets
// 判断预优化缓存标记位
tbnz p11, #0, LLookupPreopt\Function
#endif
// 优化哈希算法:_cmd ^ (_cmd >>7) 减少哈希冲突
eor p12, p1, p1, LSR #7
// 新哈希值 & mask 得到最终查找下标
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
// 非预优化缓存:提取buckets数组
and p10, p11, #0x0000ffffffffffff // p10 = buckets
// 标准哈希:sel & mask 得到下标
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
// 读取cache数据
ldr p11, [x16, #CACHE] // p11 = mask|buckets
// 提取buckets数组
and p10, p11, #~0xf // p10 = buckets
// 提取低4位mask偏移量
and p11, p11, #0xf // p11 = maskShift
mov p12, #0xffff
// 计算真实mask值
lsr p11, p12, p11 // p11 = mask = 0xffff >> p11
// sel & mask 计算下标
and p12, p1, p11 // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif
// 下标左移4位(1+PTRSHIFT=4) = 下标×16字节,定位bucket内存地址
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// do {
// 读取bucket的imp和sel,存入p17、p9,指针向前移动
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
// 比较bucket中的sel和传入的方法sel
cmp p9, p1 // if (sel != _cmd) {
// 不相等,继续遍历查找
b.ne 3f // scan more
// } else {
// 缓存命中!调用命中逻辑,执行方法
2: CacheHit \Mode // hit: call or return imp
// }
// 当前bucket的sel为空,说明缓存未命中,跳转慢速查找
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
// 判断是否遍历到buckets数组头部
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
// 计算buckets最后一个元素地址,实现循环查找
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
// 记录第一次查找的bucket位置,防止死循环
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = first probed bucket
// do {
// 尾部开始向前遍历读取bucket
4: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
// 比对sel是否匹配
cmp p9, p1 // if (sel == _cmd)
// 匹配则跳转到命中逻辑
b.eq 2b // goto hit
// 判断sel是否为空
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
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
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
快速查找流程主要步骤整理一下:
- 找到class类对象,通过首地址偏移16字节找到cache地址
- 从cache中获得buckets和mask
- 根据mask通过哈希算法计算哈希下标
- 根据下标和buckets首地址找到对应bucket
- 从bucket对比参数sel, 看在缓存有没有同名方法:如果有对应的sel,cacheHit->直接调用imp;反之,直接进入慢速查找
快速查找的本质就是:哈希定位 → 线性探测 → 回绕查找 → 命中/未命中分流。
慢速查找lookUpImpOrForward(C/C++)
在快速查找流程中,如果没有找到方法实现,无论是走到CheckMiss还是JumpMiss,最终都会走到__objc_msgSend_uncached汇编函数。
- CheckMiss:中途发现找不到,立刻退出快速查找。本质是:
objcif (bucket->sel == 0) { //再向前遍历查找bucket时,发现当前bucket的sel为0(即遇到空桶),直接跳转去慢速查找 }
- JumpMiss:整个缓存全部遍历完,还是没找到,最终退出。
在__objc_msgSend_uncached的汇编实现中,其核心的就是MethodTableLookup,即查询方法列表。
cpp
// p1:消息接收者
// p2:当前要找的方法选择子
// p3:从哪个类开始查找
// p4:控制查找行为的一组标志位
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
// 定义的消息转发,imp初始化为空,如果最终找不到方法就去执行消息转发,是一个兜底行为
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.
// 如果cls还未初始化完成,那么给behavior加禁止缓存
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);
// 确保这个类是realized+initialized状态
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
}
// 查找类的缓存
// unreasonableClassCount:安全上限数字,表示类的迭代上限
// 这是Runtime慢速查找的安全循环,防止死循环卡死
for (unsigned attempts = unreasonableClassCount();;) {
// 尝试查找方法
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
// 如果是常量优化缓存,再一次尝试从cache中查找imp,目的是防止多线程操作的时候,刚好调用函数缓存进来了
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// curClass method list.
// 找当前类,先不找父类,并且内部无锁
// 类方法列表采用二分查找,找到则返回,将方法缓存到cache中
method_t *meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
// 彻底找不到方法时,把imp指向转发函数,不再继续查找
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.
// 父类的缓存
// 从父类的cache中去找,看能不能找到imp
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.
// 如果在父类中找到了forward,则停止查找且不缓存,首先调用该类的方法解析器
break;
}
if (fastpath(imp)) {
// Found the method in a superclass. Cache it in this class.
// 如果父类中找到该方法,则将其存储到cache中
goto done;
}
}
// No implementation found. Try method resolver once.
// 第二次进入后,在第一次的时候behavior就被取反了,所以第二次进入的时候就不会进入,因此动态解析的过程是一个执行一次的单例操作
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
// 如果没有找到,就走resolveMethod
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
// 找到imp,存储到缓存
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;
}
慢速查找主要步骤整理一下:
- 检查类的合法性,确保类已经realized和initialized
- 加锁,保证查找和缓存线程安全,并且定义最大循环次数,防止父类链死循环
- 循环沿着继承链向上查找方法(先查当前类的方法列表,查不到就去父类缓存查找)
- 找到方法,缓存到本类cache,返回imp
- 如果遍历完父类链都找不到,则执行一次动态方法解析
- 解析如果依然失败,则最终设置imp为消息转发并返回
值得注意的是:无论是本类中还是父类中找到了imp,都会缓存到本类中去。
然后具体看一下在类和父类继承链中查找imp的过程:
cpp
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
lockdebug::assert_locked(&runtimeLock.get());
ASSERT(cls->isRealized());
// fixme nil cls?
// fixme nil sel?
auto alternates = cls->data()->methodAlternates();
if (auto *relativeList = alternates.relativeList)
return getMethodFromRelativeList(relativeList, sel);
if (alternates.list)
return getMethodFromListArray(&alternates.list, 1, sel);
if (auto *array = alternates.array) {
auto listAlternates = array->listAlternates();
if (listAlternates.oneList)
return getMethodFromListArray(&listAlternates.oneList, 1, sel);
if (auto innerArray = listAlternates.array)
return getMethodFromListArray(innerArray, listAlternates.arrayCount, sel);
if (auto *relativeList = listAlternates.listList)
return getMethodFromRelativeList(relativeList, sel);
}
return nil;
}
static method_t *getMethodFromRelativeList(relative_list_list_t<method_list_t> *list, SEL sel) {
// Relative lists never have a match for selectors outside the shared
// cache.
if (!objc::inSharedCache((uintptr_t)sel))
return nullptr;
for (auto mlists = list->beginLists(),
end = list->endLists();
mlists != end;
++mlists)
{
// <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest
// caller of search_method_list, inlining it turns
// getMethodNoSuper_nolock into a frame-less function and eliminates
// any store from this codepath.
method_t *m = search_method_list_inline(*mlists, sel);
if (m) return m;
}
return nullptr;
}
遍历所有方法列表,对一个方法列表调用search_method_list_inline,一旦找到就立即返回,全部没找到就返回nil。
cpp
// 查找函数具体实现
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 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;
}
这里重点看一下二分查找这里的逻辑:
cpp
// 方法类型分发器:判断方法列表是哪种格式,选择对应的比较方法,调用底层查找(不做查找,只做格式适配)
// 强制内联,减少函数调用开销,方法查询最频繁的路径
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
// 判断方法列表是哪种类型
switch (list->listKind()) {
// 小表(压缩格式,最常见)
case method_t::Kind::small:
// 如果是共享缓存内的小表
if (CONFIG_SHARED_CACHE_RELATIVE_DIRECT_SELECTORS && objc::inSharedCache((uintptr_t)list)) {
// 检查SEL也在共享缓存里
if (!objc::inSharedCache((uintptr_t)key))
return nil;
// 计算偏移量,用偏移量比较
uintptr_t keyOffset = (uintptr_t)key - sharedCacheRelativeMethodBase();
// 调用重载函数,传入小表+共享缓存的比较方式
return findMethodInSortedMethodList(key, list, [=](method_t &m) { return compare(keyOffset, (uintptr_t)m.getSmallNameAsSELOffset()); });
} else {
// 如果是普通小表,用普通SEL比较
return findMethodInSortedMethodList(key, list, [=](method_t &m) { return compare(key, m.getSmallNameAsSELRef()); });
}
// 大表(完整普通方法表)
case method_t::Kind::big:
return findMethodInSortedMethodList(key, list, [=](method_t &m) { return compare(key, m.big().name); });
// 带签名的大表
case method_t::Kind::bigSigned:
return findMethodInSortedMethodList(key, list, [=](method_t &m) { return compare(key, m.bigSigned().name); });
#if TARGET_OS_EXCLAVEKIT
// 特殊裁剪表(极少用)
case method_t::Kind::bigStripped:
return findMethodInSortedMethodList(key, list, [=](method_t &m) { return compare(key, m.bigStripped().name); });
#endif
}
}
cpp
// 真正执行查找的函数
// compare:传入的比较方式lamdba,由第一个函数根据表类型决定
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.
// 二分查找切换为线性查找的阈值,<=4个 元素时,线性更快
const uint32_t threshold = 4;
// 二分查找,缩小区间
for (count = list->count; count > threshold; count >>= 1) {
// 计算中间位置
auto probe = base + (count >> 1);
// 用传入的compare函数比较当前中间方法
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;
}
这两个函数同名但参数不同:第一个是分发器,用于决定怎么比较方法名;第二个是查找器,真正执行二分查找。

总结一下慢速查找流程:
- 从本类的method list查找imp
- 从本类的cache查找imp
- 从本类的父类的method list查找imp(继承链遍历)
- 如果上面任何一个环节中查询到了imp就跳出循环,缓存方法到本类的cache,并返回imp
- 直到查询到nil,指定imp为消息转发,跳出循环,执行动态方法解析resolveMethod_locked
动态方法决议&消息转发
当快速查找和慢速查找都没有找到对应方法时,apple官方给出了补救方法:
- 动态方法决议:慢速查找流程未找到之后,会执行一次动态方法决议
- 消息转发:如果动态方法决议仍然没有找到实现,则进行消息转发
动态方法决议
objc
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
// 进入这个方法前,Runtime正在一个受Runtime全局锁的保护的查找流程中
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);
}
objc
IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
return _lookUpImpTryCache(inst, sel, cls, behavior);
}
objc
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;
}
动态方法决议主要步骤如下:
- 如果是类,执行实例方法的动态方法决议resolveInstanceMethod。
- 如果是元类,执行类方法的的动态方法决议resolveClassMethod。
- 如果发现元类中没有找到或者为空,则在元类实例方法的动态方法决议resolveInstanceMethod中查找,这是因为类方法在元类方法中是实例方法。
- 如果动态方法决议中将其实现指向类其他方法,则需要继续查找指定的imp,即慢速查找流程

实例方法的动态方法决议
我们具体看一下执行实例方法的动态方法决议的类方法resolveInstanceMethod:
objc
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
// 检查运行时锁的状态
lockdebug::assert_unlocked(&runtimeLock.get());
ASSERT(cls->isRealized());
// 定义动态解析方法的选择器
SEL resolve_sel = @selector(resolveInstanceMethod:);
// lookUpImpOrNilTryCache: 安全查找类方法resolveInstanceMethod的实现
// 从元类继承链向上查找imp,如果本类实现这个方法,那么就可以在这个类的元类中找到imp。若未自定义实现,则可以在最终NSObject根元类中找到默认声明的imp。查找失败直接返回nil。该方法仅查询缓存+继承链,不触发动态解析,不触发消息转发
// 这里本质上是找resolveInstanceMethod自己的实现(找工具方法)
// p1:目标,类对象
// p2:要找的方法(resolveInstanceMethod
// p3:从元类开始找,因为是类方法
if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
// Resolver not implemented.
return;
}
// 主动发送resolveInstanceMethod消息给类对象,并绑定sel+imp
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
// 先到本类的cache中找到resolveInstanceMethod,再到本类的元类继承链中找resolveInstanceMethod的imp,若本类中没有实现就会调用NSObject默认实现,调用这个imp缓存到cache中
// 和前面方法一致,参数更换了,这里本质上是在动态添加方法后,找真正调用的那个方法的imp(找真正调用方法)
// p1:实例
// p2:真正要调用的方法
// p3:从类开始找
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);
}
objc
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;
}
总结一下实例方法动态方法决议主要步骤:
- 准备好resolveInstanceMethod方法的选择器。
- 通过lookUpImpOrNilTryCache检查本类是否实现resolveInstanceMethod。如果找到,有imp,那么不走消息转发;反之没有找到,则最终一定会走到NSObject根元类默认实现,解析失败,没有补上任何方法,进入消息转发。
- 回调objc_msgSend。主动发送resolveInstanceMethod消息给类对象,给开发者机会通过class_addMethod动态添加方法。
- 再次通过lookUpImpOrNilTryCache检查开发者是否把真正要调用的方法添加进来了,并且缓存起来。
- 最后通过_objc_inform打印日志,辅助测试。
类方法的动态方法决议
我们具体看一下执行类方法的动态方法决议的类方法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);
}
// 给对象发送resolveClassMethod消息,并绑定sel+imp
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));
}
}
}
总结一下类方法动态方法决议主要步骤:
- 从元类的继承链里向上查找找resolveClassMethod的imp。如果本类元类实现了,则直接返回;反之,则在最终根元类NSObject里默认实现。
- 获取这个类的元类后,进入getMaybeUnrealizedNonMetaClass方法,将nometa赋值成类对象,用于后续方法添加。
- 主动向类对象发送resolveClassMethod消息,尝试动态添加缺失的类方法。
- 无论resolveClassMethod是自定义实现还是NSObject默认实现,执行后重新查找目标类方法,并将结果缓存。
- 类方法在元类中是以实例方法的形式缓存的,所以查找类方法本质上是在元类中查找实例方法。
崩溃修改
这里我们模拟一下保底机制:
objc
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface LYDPerson : NSObject
- (void)methodFirst;
- (void)methodSecond;
- (void)methodThird;
@end
@implementation LYDPerson
- (void)methodFirst {
NSLog(@"methodFirst实现了");
}
- (void)methodSecond {
NSLog(@"methodSecond实现了");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(methodThird)) {
NSLog(@"%@", NSStringFromSelector(sel));
// 获取已实现方法的imp
IMP imp = class_getMethodImplementation(self, @selector(methodFirst));
// 获取已实现方法的方法签名
Method method = class_getInstanceMethod(self, @selector(methodFirst));
const char *type = method_getTypeEncoding(method);
// 动态给methodThird添加方法实现
return class_addMethod(self, sel, imp, type);
}
// 其他方法交给父类处理
return [super resolveInstanceMethod:sel];
}
@end
objc
int main(int argc, const char * argv[]) {
@autoreleasepool {
LYDPerson *lyd = [LYDPerson alloc];
[lyd methodFirst];
[lyd methodSecond];
[lyd methodThird];
}
return 0;
}

我们可以通过使用白名单/前缀匹配然后分流再来优化一下我们的处理方式:
objc
#import "NSObject+SafeGuard.h"
#import <objc/runtime.h>
@implementation NSObject (SafeGuard)
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selName = NSStringFromSelector(sel);
// 方法一:只拦截自己业务前缀的方法(避免影响系统方法)
if ([selName hasPrefix:@"LYD_"]) {
// 用block动态生成一个方法实现imp
IMP imp = imp_implementationWithBlock(^(id obj) {
NSLog(@"方法%@未实现,已拦截,防止崩溃", selName);
});
// 方法签名
// v@:表示返回值void+接受者id+SEL
const char *type = "v@:";
// 动态添加方法实现
BOOL success = class_addMethod(self, sel, imp, type);
if (success) {
NSLog(@"动态方法%@添加成功", selName);
}
return success;
}
if ([selName isEqualToString:@"dangerousMethod"]) {
NSLog(@"热修复:拦截危险方法%@,替换为安全实现", selName);
IMP imp = imp_implementationWithBlock(^(id obj){
NSLog(@"热修复成功:危险方法已安全执行");
});
const char *type = "v@:";
return class_addMethod(self, sel, imp, type);
}
return NO;
}
@end

消息转发
如果前面所有方法都没有找到方法实现,那么就不得不走消息转发流程了。消息转发分为快速转发和慢速转发,方法在未实现崩溃报错前会经历两遍动态方法决议和两边快速转发、两边慢速转发。
快速转发forwardingTargetForSelector
objc
#import "LYDPerson.h"
#import "LYDTeather.h"
@implementation LYDPerson
// 快速转发:找不到方法,直接转给别人处理
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(sayHello)) {
NSLog(@"快速转发:sayHello转给LYDTeacher");
return [LYDTeather new];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
慢速转发methodSignatureForSelector和forwradInvocation
快速转发之后如果还是没有实现的话,就进入最后的机会---慢速转发。
objc
#import "LYDStudent.h"
#import "LYDPerson.h"
@implementation LYDStudent
// 返回方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(sayHi)) {
NSLog(@"慢速转发:提供方法签名v@:");
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
// 处理调用
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = [anInvocation selector];
LYDPerson *person = [LYDPerson new];
if ([person respondsToSelector:sel]) {
NSLog(@"消息转发给person");
// 如果person可以处理,交给它处理
[anInvocation invokeWithTarget:person];
} else {
// 不可以就交给系统处理
[super forwardInvocation:anInvocation];
}
}
@end
第一个函数要返回方法签名,得到签名后,运行时系统创建一个NSInvocaion对象,并调用forwardInvocation方法。在forwardInvocation方法里,我们就可以自定义消息的处理方式了(例如转发给另一个对象,或者直接忽略这个消息)。

总结
OC方法调用的本质就是消息发送,消息发送就是SEL-IMP的查找过程。
- 实例方法查找父类链:当前类->父类->根类->nil
- 类方法查找父类链:当前元类->根元类->根类->nil
完整的消息发送流程是:
- 快速查找(缓存查找)
- 慢速查找(循环递归找父类)
- 快速慢速都找不到,则进入动态方法决议(resolveInstanceMethod/resolveClassMethod)
- 动态方法决议没有处理,则进入快速消息转发(forwardingTargetForSelector)
- 快速消息转发没有处理,则进入慢速消息转发(methodSignatureForSelector和forwradInvocation)
- 快速慢速消息转发都没处理,直接崩溃