【iOS】—— 消息传递和消息转发

消息传递和消息转发


在寒假学小蓝书的时候,接触过这方面的知识,但是学习很浅,这部分内容还需要很大的精力去学。

在学习之前我们先来看一个问题:消息传递和消息转发的区别?
chat:
在 Objective-C 中,消息传递和消息转发都是实现动态方法派发的机制,但它们有着不同的作用。
消息传递指的是将一个消息发送给一个对象,在运行时确定该对象是否可以响应这个消息,并执行对应的方法。当一个对象接收到一个消息时,它会首先查找自己的方法列表,如果找到了对应的方法,就直接调用;如果没有找到,则会向它的父类去查找,一直沿着继承链向上查找,直到找到能够响应这个消息的方法或到达了 NSObject 类为止。如果还没有找到,则会进入消息转发流程。
消息转发是在无法通过消息传递找到对应方法的情况下,让对象有机会在运行时动态添加方法,或者将消息转发给其他对象来处理。Objective-C 会依次调用三个方法来执行消息转发过程,分别是 forwardingTargetForSelector:、methodSignatureForSelector: 和 forwardInvocation:。其中 forwardingTargetForSelector: 方法允许对象返回另一个对象,将消息转发给那个对象;methodSignatureForSelector: 方法用于创建一个方法签名,描述方法的参数类型和返回值类型;而 forwardInvocation: 方法则是真正执行方法调用的地方,允许对象对消息进行处理或者将其再次转发给其他对象。
因此,可以看出消息传递和消息转发的区别在于,消息传递是在对象自己的方法列表中查找方法并直接调用,而消息转发是在无法找到对应方法时通过一系列机制来动态生成方法或者将消息转发给其他对象

消息传递(方法调用)

在 Objective-C 中,消息直到运行时才绑定到方法实现上。编译器会将消息表达式转化为一个消息函数的调用。
OC中的消息表达式如下(方法调用):

id returnValue = [someObject messageName:parameter];

这里,someObject叫做接收者(receiver),messageName:叫做选择子(selector),选择子和参数合起来称为“消息”。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数叫做objc_msgSend,编译器看到上述这条消息会转换成一条标准的 C 语言函数调用:

id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);

objc_msgSend函数,这个函数将消息接收者和方法名作为主要参数,其原型如下所示:

objc_msgSend(receiver, selector)                    // 不带参数
objc_msgSend(receiver, selector, arg1, arg2,...)    // 带参数

objc_msgSend通过以下几个步骤实现了动态绑定机制:

  • 首先,获取selector指向的方法实现。由于相同的方法可能在不同的类中有着不同的实现,因此根据receiver所属的类进行判断。
  • 其次,传递receiver对象、方法指定的参数来调用方法实现。
  • 最后,返回方法实现的返回值。

  • 当消息传递给一个对象时,首先从运行时系统缓存objc_cache中进行查找。如果找到,则执行。否则,继续执行下面步骤。
  • objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法分发表methodLists中查找方法的selector。如果未找到,将沿着类的superclass找到其父类,并在父类的分发表methodLists中继续查找。
  • 以此类推,一直沿着类的继承链追溯至NSObject类。一旦找到selector,传入相应的参数来执行方法的具体实现,并将该方法加入缓存objc_cache。如果最后仍然没有找到selector,则会进入消息转发流程。

我们来在代码里试试:

//Dog.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Dog : NSObject
- (void)run;
@end

NS_ASSUME_NONNULL_END

//Dog.m
#import "Dog.h"

@implementation Dog
- (void)run {
    
    
    NSLog(@"run");
}
@end

//main.m
#import <Foundation/Foundation.h>
#import "Dog.h"
int main(int argc, const char * argv[]) {
    
    
    @autoreleasepool {
    
    
        Dog *dog = [[Dog alloc] init];
        [dog run];
    }
    return 0;
}

还是一样,将这个文件转成c++文件看看:

clang -rewrite-objc main.m

#pragma clang assume_nonnull end
int main(int argc, const char * argv[]) {
    
    
    /* @autoreleasepool */ {
    
     __AtAutoreleasePool __autoreleasepool; 
        Dog *dog = ((Dog *(*)(id, SEL))(void *)objc_msgSend)((id)((Dog *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Dog"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)dog, sel_registerName("run"));
    }
    return 0;
}
static struct IMAGE_INFO {
    
     unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = {
    
     0, 2 };

编译后的方法调用都是通过objc_msgSend发送的,证明方法的本质就是消息发送

  • objc_msgSend带有默认的2个隐式参数:消息的接收者id类型,消息的方法名SEL类型。
  • 开始的alloc方法是给类对象发消息objc_getClass(“Person”)
  • 如果消息接收者是实例对象,实例对象会通过isa找到类对象,从中找到实例方法。类方法同理,在元类对象中找到。

我们来看这个文件的顶部,发现objc_msgSend方法不止一种,有很多个:
在这里插入图片描述
以下方法依次代表发给当前类对象、父类对象、结构体、结构体父类、浮点类型。

__OBJC_RW_DLLIMPORT void objc_msgSend(void);
__OBJC_RW_DLLIMPORT void objc_msgSendSuper(void);
__OBJC_RW_DLLIMPORT void objc_msgSend_stret(void);
__OBJC_RW_DLLIMPORT void objc_msgSendSuper_stret(void);
__OBJC_RW_DLLIMPORT void objc_msgSend_fpret(void);

IMP指针

不同的类可以拥有相同的方法,不同类的实例对象执行相同的selector时会在各自的方法列表中去根据SEL去寻找自己类对应的IMP。

IMP本质就是一个函数指针,这个被指向的函数包含一个接收消息的对象id,调用方法的SEL,以及一些方法参数,并返回一个id。因此我们可以通过SEL获得它所对应的IMP,在取得了函数指针之后,也就意味着我们取得了需要执行方法的代码入口,这样我们就可以像普通的C语言函数调用一样使用这个函数指针。

还是一样我们来看一个问题:
oc消息传递中查找IMP指针作用是啥?
chat:
在 Objective-C 中,消息传递是一种动态绑定的方式,该方式使对象能够在运行时根据其类型和当前需要调用的方法来响应消息。当向一个对象发送消息时,Objective-C 运行时会查找与该消息名称相匹配的方法实现,并在找到合适的实现后调用它。

为了查找相应的方法实现,Objective-C 运行时使用 IMP(Implementation)来表示方法的实际代码。IMP 是一个指向函数指针的指针,它指向方法的实际代码实现。因此,在查找方法实现时,实际上是在查找对应的 IMP。

这种动态绑定的技术使得 Objective-C 具有很强的灵活性和可扩展性,可以在运行时添加、删除或替换方法实现。同时,它也使得 Objective-C 代码的执行效率较低,因为每次向对象发送消息时都要进行方法查找并调用相应的 IMP。

IMP与SEL的区别与联系

  • SEL:类方法的指针,相当于一种编号,区别与IMP
  • IMP:函数指针,保存了方法的地址

SEL是通过表取对应关系的IMP,进行方法的调用

每一个继承于NSObject的类都能自动获的runtime的支持,在这样的类中,有一个isa指针,指向该类定义的数据结构体,这个结构体是编译器编译时为类创建的.在这个结构体中包括了指向其父类类定义的指针及Dispatch table,Dispatch table 是一张SEL和IMP的对应表。也就是说方法编号SEL最后还要通过Dispatch table表找到对应的IMP,IMP是一个函数指针,然后去执行这个方法;

接下来我们来看看源码解析过程:
查找 IMP方式大致分为两种,快速查找和慢速查找,我们先来看看快速查找:

快速查找imp过程

objc_msgSend在不同架构下都有实现:以arm64为例,代码实现是汇编。

  • 为什么选用汇编来实现?速度更快,直接使用参数,免去大量参数的拷贝的开销。
  • 在函数和全局变量前面会加下划线“_”,防止符号冲突。

汇编代码查找过程

  • 首先从cmp p0,#0开始,这里p0是寄存器,存放的是消息接受者。当进入消息发送入口时,先判断消息接收者是否存在,不存在则重新执行objc_msgSend
  • b.le LNilOrTagged,b是跳转到的意思。le是如果p0小于等于0,总体意思是若p0小于等于0,则跳转到LNilOrTagged,执行b.eq LReturnZero直接退出这个函数
	//进入objc_msgSend流程
	ENTRY _objc_msgSend
    //流程开始,无需frame
	UNWIND _objc_msgSend, NoFrame

    //判断p0(消息接收者)是否存在,不存在则重新开始执行objc_msgSend
	cmp	p0, #0			// nil check and tagged pointer check
//如果支持小对象类型,返回小对象或空
#if SUPPORT_TAGGED_POINTERS
    //b是进行跳转,b.le是小于判断,也就是p0小于0的时候跳转到LNilOrTagged
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
    //等于,如果不支持小对象,就跳转至LReturnZero退出
	b.eq	LReturnZero
#endif
    //通过p13取isa
	ldr	p13, [x0]		// p13 = isa
    //通过isa取class并保存到p16寄存器中
	GetClassFromIsa_p16 p13, 1, x0	// p16 = class

  • 如果消息接受者不为nil,汇编继续跑,到CacheLookup NORMAL,在cache中查找imp,来看一下具体的实现
//在cache中通过sel查找imp的核心流程
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
	//
	// Restart protocol:
	//
	//   As soon as we're past the LLookupStart\Function label we may have
	//   loaded an invalid cache pointer or mask.
	//
	//   When task_restartable_ranges_synchronize() is called,
	//   (or when a signal hits us) before we're past LLookupEnd\Function,
	//   then our PC will be reset to LLookupRecover\Function which forcefully
	//   jumps to the cache-miss codepath which have the following
	//   requirements:
	//
	//   GETIMP:
	//     The cache-miss is just returning NULL (setting x0 to 0)
	//
	//   NORMAL and LOOKUP:
	//   - x0 contains the receiver
	//   - x1 contains the selector
	//   - x16 contains the isa
	//   - other registers are set as per calling conventions
	//

    //从x16中取出class移到x15中
	mov	x15, x16			// stash the original isa
//开始查找
LLookupStart\Function:
	// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    //ldr表示将一个值存入到p10寄存器中
    //x16表示p16寄存器存储的值,当前是Class
    //#数值 表示一个值,这里的CACHE经过全局搜索发现是2倍的指针地址,也就是16个字节
    //#define CACHE (2 * __SIZEOF_POINTER__)
    //经计算,p10就是cache
	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
//真机64位看这个
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    //CACHE 16字节,也就是通过isa内存平移获取cache,然后cache的首地址就是 (bucket_t *)
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
//获取buckets
#if __has_feature(ptrauth_calls)
	tbnz	p11, #0, LLookupPreopt\Function
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else
    //and表示与运算,将与上mask后的buckets值保存到p10寄存器
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
    //p11与#0比较,如果p11不存在,就走Function,如果存在走LLookupPreopt
	tbnz	p11, #0, LLookupPreopt\Function
#endif
    //按位右移7个单位,存到p12里面,p0是对象,p1是_cmd
	eor	p12, p1, p1, LSR #7
	and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
    //LSR表示逻辑向右偏移
    //p11, LSR #48表示cache偏移48位,拿到前16位,也就是得到mask
    //这个是哈希算法,p12存储的就是搜索下标(哈希地址)
    //整句表示_cmd & mask并保存到p12
	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

    //去除掩码后bucket的内存平移
    //PTRSHIFT经全局搜索发现是3
    //LSL #(1+PTRSHIFT)表示逻辑左移4位,也就是*16
    //通过bucket的首地址进行左平移下标的16倍数并与p12相与得到bucket,并存入到p13中
	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

						// do {
    
    
//ldp表示出栈,取出bucket中的imp和sel分别存放到p17和p9
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
    //cmp表示比较,对比p9和p1,如果相同就找到了对应的方法,返回对应imp,走CacheHit
	cmp	p9, p1				//     if (sel != _cmd) {
    
    
    //b.ne表示如果不相同则跳转到3f
	b.ne	3f				//         scan more
						//     } else {
    
    
2:	CacheHit \Mode				// hit:    call or return imp
						//     }
//向前查找下一个bucket,一直循环直到找到对应的方法,循环完都没有找到就调用_objc_msgSend_uncached
3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
    //通过p13和p10来判断是否是第一个bucket
	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
	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

通过 类对象/元类 (objc_class) 通过内存平移得到cache,获取buckets,通过内存平移的方式获取对应的方法(对比sel)。

在缓存中找到了方法那就直接调用,找到sel就会进入CacheHit,去return or call imp:返回或调用方法的实现(imp)。

如果从缓存中没有找到方法怎么办?

  • 如果没有找到缓存,查找下一个bucket,一直循环直到找到对应的方法,循环完都没有找到就调用__objc_msgSend_uncached

下面是上述判断跳转代码:

//LGetIsaDone是一个入口
LGetIsaDone:
	// calls imp or objc_msgSend_uncached
    //进入到缓存查找或者没有缓存查找方法的流程
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

__objc_msgSend_uncached源码汇编:

	STATIC_ENTRY __objc_msgSend_uncached
	UNWIND __objc_msgSend_uncached, FrameWithNoSaves

	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band p15 is the class to search
	
	MethodTableLookup
	TailCallFunctionPointer x17

	END_ENTRY __objc_msgSend_uncached

其中调用了MethodTableLookup宏: 从方法列表中去查找方法
看一下它的结构:

.macro MethodTableLookup
	
	SAVE_REGS MSGSEND

	// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
	// receiver and selector already in x0 and x1
	mov	x2, x16
	mov	x3, #3
	bl	_lookUpImpOrForward

	// IMP in x0
	mov	x17, x0

	RESTORE_REGS MSGSEND

.endmacro

其中bl表示调用了方法_lookUpImpOrForward,_lookUpImpOrForward在汇编里找不到,因为汇编的函数比C++的多一个下划线,需要去掉下划线,去找到lookUpImpOrForward方法实现

总结消息发送快速查找imp(汇编):

objc_msgSend(receiver, sel, …)

  • 1.检查消息接收者receiver是否存在,为nil则不做任何处理
  • 2.通过receiver的isa指针找到对应的class类对象
  • 3.找到class类对象进行内存平移,找到cache
  • 4.从cache中获取buckets
  • 5.从buckets中对比参数sel,看在缓存里有没有同名方法
  • 6.如果buckets中有对应的sel --> cacheHit --> 调用imp
  • 7.如果buckets中没有对应的sel --> _objc_msgSend_uncached -> _lookUpImpOrForward (c/c++慢速查找)

补充:

方法缓存

苹果认为如果一个方法被调用了,那个这个方法有更大的几率被再此调用,既然如此直接维护一个缓存列表,把调用过的方法加载到缓存列表中,再次调用该方法时,先去缓存列表中去查找,如果找不到再去方法列表查询。这样避免了每次调用方法都要去方法列表去查询,大大的提高了速率

慢速查找

NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    
    
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

    if (slowpath(!cls->isInitialized())) {
    
    
        ...省略部分

    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 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;
        }
    }

    // 未找到实现。请尝试一次方法解析器。

    if (slowpath(behavior & LOOKUP_RESOLVER)) {
    
    
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
    
    
#if CONFIG_USE_PREOPT_CACHES
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
    
    
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
 done_unlock:
    runtimeLock.unlock();
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
    
    
        return nil;
    }
    return imp;
}

1.检查类是否被初始化、是否是个已知的关系、确定继承关系等准备工作。

    for (unsigned attempts = unreasonableClassCount();;) {
    
    
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
    
    
            // 如果是常量优化缓存
            // 再一次从cache查找imp
            // 目的:防止多线程操作时,刚好调用函数,此时缓存进来了
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
    
    
            // curClass方法列表。
            method_t *meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
    
    
                imp = meth->imp(false);
                goto done;
            }
            // 每次判断都会把curClass的父类赋值给curClass
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
    
    
                // 没有找到实现,方法解析器没有帮助。
                // 使用转发。
                imp = forward_imp;
                break;
            }
        }

        // 如果超类链中存在循环,则停止。
        if (slowpath(--attempts == 0)) {
    
    
            _objc_fatal("Memory corruption in class list.");
        }

        // 超类缓存。
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
    
    
            // 在超类中找到forward::条目。
            // 停止搜索,但不要缓存;调用方法
            // 首先为这个类解析器。
            break;
        }
        if (fastpath(imp)) {
    
    
            // 在超类中找到方法。在这个类中缓存它。
            goto done;
        }
    }

进入了一个循环逻辑:

  • 从本类的method list查找imp(查找的方式是getMethodNoSuper_nolock,一会分析);
  • 从本类的父类的cache查找imp(cache_getImp汇编写的)
  • 从本类的父类的method list查找imp,…继承链遍历…(父类->…->根父类)
  • 若上面环节有任何一个环节查找到了imp,跳出循环,缓存方法到本类的cache(log_and_fill_cache);
  • 直到查找到nil,指定imp为消息转发,跳出循环。

跳出循环后的逻辑:

done:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
    
    
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
    
    
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
 done_unlock:
    runtimeLock.unlock();
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
    
    
        return nil;
    }
    return imp;

如果找到了imp,就会把imp缓存到本类cache里(log_and_fill_cache):(注意这里不管是本类还是本类的父类找到了imp,都会缓存到本类中去)。

static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
    
    
#if SUPPORT_MESSAGE_LOGGING
    if (slowpath(objcMsgLogEnabled && implementer)) {
    
    
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cls->cache.insert(sel, imp, receiver); // 插入缓存
}

2.getMethodNoSuper_nolock查找方式

tatic method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    
    
    runtimeLock.assertLocked();

    ASSERT(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?

    auto const methods = cls->data()->methods();
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         mlists != end;
         ++mlists)
    {
    
    
        // <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 nil;
}

search_method_list_inline里找到了method_t就会返回出去了(search_method_list_inline):

ALWAYS_INLINE static method_t *
search_method_list_inline(const method_list_t *mlist, SEL sel)
{
    
    
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->isExpectedSize();
    
    if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
    
    
        return findMethodInSortedMethodList(sel, mlist);
    } else {
    
    
        // Linear search of unsorted method list
        if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
            return m;
    }

#if DEBUG
    // sanity-check negative 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;
}

这里就是使用findMethodInSortedMethodListfindMethodInUnsortedMethodList通过sel找到method_t的。这两个函数的区别就是:
前者是排好序的,后者是未排好序的;前者方法中的查询方式是二分查找,后者则是普通查找。

总结慢速查找

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)

  • 1.从本类的 method list (二分查找/遍历查找)查找imp
  • 2.从本类的父类的cache查找imp(汇编)
  • 3.从本类的父类的method list (二分查找/遍历查找)查找imp
    …继承链遍历…(父类->…->根父类)里找cachemethod listimp
  • 4.若上面环节有任何一个环节查找到了imp,跳出循环,缓存方法到本类的cache,并返回imp
  • 5.直到查找到nil,指定imp为消息转发,跳出循环,执行动态方法解析resolveMethod_locked

这时候我们又会想到一个问题:
oc消息传递中查找IMP什么时候要用快速查找,什么时候要用慢速查找

chat:
在 Objective-C 中,消息传递中查找 IMP 的时候,通常采用快速查找的方式。这是因为 Objective-C 运行时内部会使用一些高效的数据结构来存储类和方法信息,从而加快查找 IMP 的速度。

具体来说,当向一个对象发送消息时,Objective-C 运行时会首先检查该对象所属的类是否重写了该方法,如果重写了,则直接调用重写后的方法实现。如果没有重写,则会在其父类中继续查找方法实现,直到找到合适的实现或者查找到根类为止。

在进行查找时,Objective-C 运行时会利用缓存机制来提高查找的速度。它会将最近使用过的方法实现存储在缓存中,下次再调用相同的方法时就可以直接从缓存中获取实现,避免了反复查找的过程。
前面说了很多,但是都没说到点子上,主要看后面一段

虽然快速查找方法实现的效率很高,但是如果出现了一些特殊情况,比如类的继承关系较为复杂或者存在大量的动态方法解析等操作,那么查找 IMP 的过程可能会变得相对缓慢。在这种情况下,可能需要使用慢速查找的方式,即通过线性搜索来逐个查找每个方法实现,以保证能够准确找到所需的实现。

再来看一个问题:
Objective-C中消息发送慢速查找和快速转发区别
chat:慢速查找是指当 Objective-C 对象接收到一个无法识别的消息时,它会调用 forwardInvocation: 方法,并在这个方法中进行一些处理,例如动态创建新的对象来响应该消息。由于这种方式需要进行额外的处理,所以消息传递的速度相对较慢。

快速转发是指当对象接收到一个无法识别的消息时,它会调用 methodSignatureForSelector: 方法来获取该消息的方法签名,然后返回给系统一个表示该消息的 NSMethodSignature 对象。然后系统会尝试使用 forwardingTargetForSelector: 方法将消息转发给其他对象进行响应。如果这个方法没有实现,那么会继续调用 forwardInvocation: 方法进行处理。由于这种方式避免了一些额外的处理,所以消息传递的速度相对较快。

大致是对上面内容的一个总结吧

消息转发

上面介绍到的是找到了消息,然后发送消息,那如果没有找到消息,该怎么处理呢?就需要消息转发。

学到这块就可以看看最开头的问题,发现那个问题问的蛮不专业的。我们接着来看消息转发:

  • 默认情况下,如果以[object message]的形式调用方法,如果object无法响应message消息时,编译器会报错。
  • 如果是以performSeletor:的形式调用方法,则需要等到运行时才能确定object是否能接收message消息。如果不能,则程序崩溃。

对于后者,当不确定一个对象是否能接收某个消息时,可以调用respondsToSelector: 来进行判断:

if ([self respondsToSelector:@selector(method)]) {
    
    
    [self performSelector:@selector(method)];
}

事实上,当一个对象无法接收某一消息时,就会启动所谓“消息转发(message forwarding)”机制。通过消息转发机制,我们可以告诉对象如何处理未知的消息。

消息转发机制大致可分为三个步骤:

  • 动态方法解析
  • 备援接收者
  • 完整消息转发

下图为消息转发过程的示意图:
在这里插入图片描述

动态决议

// No implementation found. Try method resolver once.
//未找到实现。尝试一次方法解析器
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
    
    
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

通过之前的源码发现,如果没找到方法则尝试调用resolveMethod_locked动态解析,只会执行一次:

/***********************************************************************
* resolveMethod_locked
* Call +resolveClassMethod or +resolveInstanceMethod.
*
* Called with the runtimeLock held to avoid pressure in the caller
* Tail calls into lookUpImpOrForward, also to avoid pressure in the callerb
**********************************************************************/
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    
    
    lockdebug::assert_locked(&runtimeLock);
    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);
}

主要用的的方法如下:

// 类方法未找到时调起,可以在此添加方法实现
+ (BOOL)resolveClassMethod:(SEL)sel;
// 对象方法未找到时调起,可以在此添加方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel;
//其中参数sel为未处理的方法

上述代码大致流程:

  • 先判断判断进行解析的是否是元类
  • 如果不是元类,则调用resolveInstanceMethod进行对象方法动态解析
  • 如果是元类,则调用resolveClassMethod进行类方法动态解析,完成类方法动态解析后,再次查询cls中的imp,如果没有找到,则进行一次对象方法动态解析。

而这两个方法resolveInstanceMethodresolveClassMethod则称为方法的动态决议。

执行完上述代码后返回lookUpImpOrForwardTryCache:

IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
    
    
    return _lookUpImpTryCache(inst, sel, cls, behavior);
}

这个方法调用的是_lookUpImpTryCache方法:

ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
    
    
    lockdebug::assert_unlocked(&runtimeLock);

    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)) {
    
    
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

done:
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
    
    
        return nil;
    }
    return imp;
}

进入_lookUpImpTryCache源码,可以看到这里有cache_getImp;也就是说在进行一次动态决议之后,还会通过cache_getImpcache里找一遍方法的sel

#endif
    if (slowpath(imp == NULL)) {
    
    
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

如果还是没找到(imp == NULL)?也就是无法通过动态添加方法的话,还会执行一次lookUpImpOrForward,这时候进lookUpImpOrForward方法,这里behavior传的值会发生变化。

第二次进入lookUpImpOrForward方法后,执行到if (slowpath(behavior & LOOKUP_RESOLVER))这个判断时:

// 这里就是消息转发机制第一层的入口
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
    
    
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

根据变化后的behavior值和LOOKUP_RESOLVER值之间的关系导致该if语句内部只能进入第一次,因此这个判断相当于单例。解释了为什么开头说的该动态解析resolveMethod_locked为什么只执行一次。

动态解析添加方法

在动态决议阶段可以为类添加方法,以保证程序正常运行

class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) 

@cls : 给哪个类对象添加方法
@name : SEL类型,给哪个方法名添加方法实现
@imp : IMP类型的,要把哪个方法实现添加给给定的方法名
@types : 就是表示返回值和参数类型的字符串

我们来看一个例子:

//Person.m
#import "Person.h"
#import <objc/runtime.h>
@interface Person : NSObject
- (void)print;
@end
@implementation Person
@end

可以看到print方法并未实现,所以在主函数中调用程序一定会崩溃,
然后我们将代码修改为下面这样:
在.m文件中增加这个方法:

+(BOOL)resolveInstanceMethod:(SEL)sel {
    
    
    NSLog(@"%s, sel = %@", __func__, NSStringFromSelector(sel));
    return [super resolveInstanceMethod:sel];
}

程序依然会崩溃,我们看看输出结果:
在这里插入图片描述
那么控制台为什么会打印两次信息呢也就是为什么会调用两次resolveInstanceMethod方法呢?这个最后来说。

我们看看程序崩溃的原因:
在这里插入图片描述
是因为找不到imp而崩溃,那么我们可以在这个方法里通过runtime的class_addMethod,给sel动态的生成imp。其中第四个参数是返回值类型,用void用字符串描述:“v@:”

- (void)addMethod {
    
    
    NSLog(@"%s", __func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    
    NSLog(@"%s, sel = %@", __func__, NSStringFromSelector(sel));
    if(sel == @selector(print)) {
    
    
        IMP imp = class_getMethodImplementation(self, @selector(addMethod));
        class_addMethod(self, sel, imp, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

在这里插入图片描述

快速转发

当cache没有找到imp,类的继承链里的方法列表都没有找到imp,并且resolveInstanceMethod / resolveClassMethod 返回NO就会进入消息转发。也就是所以如果本类没有能力去处理这个消息,那么就转发给其他的类,让其他类去处理。

done:
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
    
    
        return nil;
    }
    return imp;

imp == (IMP)_objc_msgForward_impcache进入消息转发机制。
查看一下这个方法:
竟然是汇编实现的这就又印证了汇编速度更快的结论:

	STATIC_ENTRY __objc_msgForward_impcache

	// No stret specialization.
	b	__objc_msgForward

	END_ENTRY __objc_msgForward_impcache

	
	ENTRY __objc_msgForward

	adrp	x17, __objc_forward_handler@PAGE
	ldr	p17, [x17, __objc_forward_handler@PAGEOFF]
	TailCallFunctionPointer x17
	
	END_ENTRY __objc_msgForward

快速转发测试

  • Person类中定义func1方法但是不实现,利用forwardingTargetForSelector:(SEL)aSelector 方法进行消息快速转发。
  • NiuBiPerson类中定义func1方法且实现:
//Person.h
@interface Person : NSObject
- (void)print;
@end
//Person.m
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    
    NSLog(@"%s, sel = %@", __func__, NSStringFromSelector(sel));
    if(sel == @selector(print)) {
    
    
        IMP imp = class_getMethodImplementation(self, @selector(addMethod));
        class_addMethod(self, sel, imp, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

//NiuBiPerson.h
@interface NiuBiPerson : NSObject
- (void)print;
@end
//NiuBiPerson.m
@implementation NiuBiPerson
- (void)print {
    
    
    NSLog(@"%s", __func__);
}
@end

在这里插入图片描述
转发的作用在于,如果当前对象无法响应消息,就将它转发给能响应的对象。

此时,方法缓冲在接收转发消息的对象。

慢速转发

如果消息的快速转发也没有找到方法;后面还有个methodSignatureForSelector方法,作用是方法有效性签名
将刚才使用快速转发forwardingTargetForSelector方法注释后,添加上methodSignatureForSelector方法后能否正常运行?

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    
    
    NSLog(@"%s, aSelector = %@",__func__, NSStringFromSelector(aSelector));
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

运行之后发现程序会崩溃,因为这个方法需要搭配forwardInvocation

forwardInvocation方法提供了一个入参,类型是NSInvocation;它提供了target和selector用于指定目标里查找方法实现。

- (void)forwardInvocation:(NSInvocation *)anInvocation;

在这里插入图片描述

总结

防止系统崩溃的三个救命稻草:动态解析快速转发、慢速转发。
OC方法调用的本质就是消息发送,消息发送是SEL-IMP的查找过程。

动态决议

实例方法:

+ (BOOL)resolveInstanceMethod:(SEL)sel;
// 系统通过该方法调用上面OC类里的实现
static void resolveInstanceMethod(id inst, SEL sel, Class cls) 

类方法:

+ (BOOL)resolveClassMethod:(SEL)sel;

消息转发

消息快速转发:

- (id)forwardingTargetForSelector:(SEL)aSelector;

消息慢速转发:

// 方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
// 正向调用
- (void)forwardInvocation:(NSInvocation *)anInvocation;

消息的三次拯救

  • 动态方法解析
  • 备援接收者
  • 完整消息转发

请添加图片描述

猜你喜欢

转载自blog.csdn.net/m0_62386635/article/details/130067666