【iOS】源码学习-消息流程分析

【iOS】源码学习-消息流程分析

前言

在OC中,方法调用并不是直接调用函数,而是一次消息发送。底层被转换为objc_msgSend,其核心任务是根据消息接收者receiver和方法选择器SEL,找到真正的方法实现IMP。

Runtime介绍

区分一下运行时和编译时:

  • 运行时:代码跑起来被装载到内存中的过程。如果此时出错,则程序崩溃,是一个动态阶段。
  • 编译时:源代码翻译成机器能识别的代码的过程。主要对语言进行最基本的检查报错,即词法分析、语法分析等,是一个静态阶段。

Runtime称为运行时

Runtime的使用方式有以下三种:

  1. 通过OC代码,例如[person satHello]
  2. 通过NSObject方法,例如isKindOfClass
  3. 通过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:表示运行的环境是模拟器或者macOS
  • CACHE_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
}

这个过程的整个逻辑顺序总结一下就是:

  1. 确保线程安全,确保类已经初始化成功,确保不是预编译的只读cache
  2. 判断是否需要扩容,获取当前已经缓存方法数量
  3. 第一次插入时需要先初始化缓存
  4. 判断是否需要扩容,扩容到对应方法(如果超出了最大的缓存大小,就把旧方法中所有缓存全部清除,用新的缓存替换
  5. 找到哈希插入的位置
  6. 插入方法(如果 之前已经插入过就不继续插入,如果哈希冲突了就处理冲突逻辑问题,如果插入失败了就进入异常处理)

再具体看一下处理扩容逻辑:

获取到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使用汇编实现的原因:

  • 执行速度快,满足方法调用高频需求
  • 直接操作寄存器,不破坏上层函数调用栈
  • 跨版本、跨架构稳定,不依赖编译器优化
  • 能精准控制寄存器,保证消息发送稳定

总结一下主要步骤:

  1. 检查receiver是否为nil/tagged pointer
  2. 如果是普通对象,从对象内存中取出isa
  3. 由isa还原出class(通过掩码、移位)
  4. 进入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

快速查找流程主要步骤整理一下:

  1. 找到class类对象,通过首地址偏移16字节找到cache地址
  2. 从cache中获得buckets和mask
  3. 根据mask通过哈希算法计算哈希下标
  4. 根据下标和buckets首地址找到对应bucket
  5. 从bucket对比参数sel, 看在缓存有没有同名方法:如果有对应的sel,cacheHit->直接调用imp;反之,直接进入慢速查找

快速查找的本质就是:哈希定位 → 线性探测 → 回绕查找 → 命中/未命中分流

慢速查找lookUpImpOrForward(C/C++)

在快速查找流程中,如果没有找到方法实现,无论是走到CheckMiss还是JumpMiss,最终都会走到__objc_msgSend_uncached汇编函数。

  • CheckMiss:中途发现找不到,立刻退出快速查找。本质是:
objc 复制代码
if (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;
}

慢速查找主要步骤整理一下:

  1. 检查类的合法性,确保类已经realized和initialized
  2. 加锁,保证查找和缓存线程安全,并且定义最大循环次数,防止父类链死循环
  3. 循环沿着继承链向上查找方法(先查当前类的方法列表,查不到就去父类缓存查找)
  4. 找到方法,缓存到本类cache,返回imp
  5. 如果遍历完父类链都找不到,则执行一次动态方法解析
  6. 解析如果依然失败,则最终设置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;
}

这两个函数同名但参数不同:第一个是分发器,用于决定怎么比较方法名;第二个是查找器,真正执行二分查找。

总结一下慢速查找流程:

  1. 从本类的method list查找imp
  2. 从本类的cache查找imp
  3. 从本类的父类的method list查找imp(继承链遍历)
  4. 如果上面任何一个环节中查询到了imp就跳出循环,缓存方法到本类的cache,并返回imp
  5. 直到查询到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;
}

动态方法决议主要步骤如下:

  1. 如果是类,执行实例方法的动态方法决议resolveInstanceMethod。
  2. 如果是元类,执行类方法的的动态方法决议resolveClassMethod。
  3. 如果发现元类中没有找到或者为空,则在元类实例方法的动态方法决议resolveInstanceMethod中查找,这是因为类方法在元类方法中是实例方法。
  4. 如果动态方法决议中将其实现指向类其他方法,则需要继续查找指定的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;
}

总结一下实例方法动态方法决议主要步骤:

  1. 准备好resolveInstanceMethod方法的选择器。
  2. 通过lookUpImpOrNilTryCache检查本类是否实现resolveInstanceMethod。如果找到,有imp,那么不走消息转发;反之没有找到,则最终一定会走到NSObject根元类默认实现,解析失败,没有补上任何方法,进入消息转发。
  3. 回调objc_msgSend。主动发送resolveInstanceMethod消息给类对象,给开发者机会通过class_addMethod动态添加方法。
  4. 再次通过lookUpImpOrNilTryCache检查开发者是否把真正要调用的方法添加进来了,并且缓存起来。
  5. 最后通过_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));
        }
    }
}

总结一下类方法动态方法决议主要步骤:

  1. 从元类的继承链里向上查找找resolveClassMethod的imp。如果本类元类实现了,则直接返回;反之,则在最终根元类NSObject里默认实现。
  2. 获取这个类的元类后,进入getMaybeUnrealizedNonMetaClass方法,将nometa赋值成类对象,用于后续方法添加。
  3. 主动向类对象发送resolveClassMethod消息,尝试动态添加缺失的类方法。
  4. 无论resolveClassMethod是自定义实现还是NSObject默认实现,执行后重新查找目标类方法,并将结果缓存。
  5. 类方法在元类中是以实例方法的形式缓存的,所以查找类方法本质上是在元类中查找实例方法。

崩溃修改

这里我们模拟一下保底机制:

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

完整的消息发送流程是:

  1. 快速查找(缓存查找)
  2. 慢速查找(循环递归找父类)
  3. 快速慢速都找不到,则进入动态方法决议(resolveInstanceMethod/resolveClassMethod)
  4. 动态方法决议没有处理,则进入快速消息转发(forwardingTargetForSelector)
  5. 快速消息转发没有处理,则进入慢速消息转发(methodSignatureForSelector和forwradInvocation)
  6. 快速慢速消息转发都没处理,直接崩溃
相关推荐
2501_915918411 小时前
iOS性能数据监控:从概念到工具实践,让应用运行更流畅
android·macos·ios·小程序·uni-app·cocoa·iphone
承渊政道1 小时前
【贪心算法】(经典实战应用解析(二):最⻓递增⼦序列、递增的三元⼦序列、最⻓连续递增序列、买卖股票的最佳时机、买卖股票的最佳时机II)
数据结构·c++·学习·算法·leetcode·贪心算法·哈希算法
li星野1 小时前
动态规划十题通关:从爬楼梯到编辑距离(Python + C++)
c++·python·学习·算法·动态规划
Tutankaaa1 小时前
学校知识竞赛怎么组织?从班级到年级的进阶方案
经验分享·学习·算法·职场和发展
li星野2 小时前
二叉树十题通关:从层序遍历到序列化(Python + C++)
开发语言·c++·python·学习
南境十里·墨染春水2 小时前
线程池学习(一) 理解 进程 线程 协程及上下文切换
java·开发语言·学习
Mr.H01272 小时前
C语言MQTT学习系列(3篇):第一篇:从零开始学MQTT(C语言版):入门必看,跑通最简Demo
c语言·网络·学习
Cat_Rocky11 小时前
k8s-持久化存储,粗浅学习
java·学习·kubernetes
AOwhisky12 小时前
虚拟化技术学习笔记
linux·运维·笔记·学习·虚拟化技术