【iOS】消息传递与消息转发

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

Objective-C是一门非常动态的语言以至于确定调用哪个方法被推迟到了运行时而非编译时。与之相反C语言使用静态绑定也就是说在编译期就能决定程序运行时所应该调用的函数所以在C语言中 如果某个函数没有实现编译时是不能通过的。而Objective-C是相对动态的语言运行时还可以向类中动态添加方法所以编译时并不能确定方法到底有没有对应的实现编译器在编译期间也就不能报错。

在对象上调用方法在Objective-C中非常普遍。用Objective-C的术语来讲 叫做“给某个对象发送某条消息"。消息有 “名称”或“选择子” (selector)之说。消息可以接受参数而且还可以有返回值。

消息传递之机制

这是发送消息的基本格式

id returnValue = [someObject messageName:parameter];

本例中someObject叫做方法调用者也叫做接受者(receiver) 。messageName:是方法名也叫做选择子(selector) 。选择子与参数合起来叫做“消息”(message) 。在运行时编译器会把上面这个格式的方法调用转化为一条标准的C语言函数调用该函数就是objc_ msgSend()该函数是消息objc里在运行时传递机制中的核心函数其原型如下:

void objc_msgSend(id self, SEL cmd, ....

显而易见该函数参数可变第一个参数代表接受者第二个参数代表选择子方法名后面就是消息中的参数一一对应
而在经过编译器的处理代码会被处理为

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

SEL选择子

OC在编译时会根据方法的名字包括参数序列生成一个用来区分这个办法的唯一的一个ID这个ID就是SEL类型的。我们需要注意的是只要方法的名字包括参数序列相同那么他们的ID就是相同的。所以不管是父类还是子类名字相同那么ID就是一样的。

	SEL sell1 = @selector(eat:);
    NSLog(@"sell1:%p", sell1);
    SEL sell2 = @selector(eat);
    NSLog(@"sell2:%p", sell2);
    //sell1:0x100000f63
	//sell2:0x100000f68

其中需要注意的是@selector等于是把方法名翻译成SEL方法名。其仅仅关心方法名和参数个数并不关心返回值与参数类型

生成SEL的过程是固定的因为它只是一个表明方法的ID不管是在哪个类写这个eat方法SEL值都是固定一个

在Runtime中维护了一个SEL的表这个表存储SEL不按照类来存储只要相同的SEL就会被看做一个并存储到表中。在项目加载时会将所有方法都加载到这个表中而动态生成的方法也会被加载到表中。

在OC中要得到方法SEL 可以直接使用:

  1. @ selector指示符号SEL act = @selector(setAge:);
  2. 也可是函数NSSelectorFromString(NSString *)
  3. (NSString *)NSStringFromSeletor (SEL)

IMP

IMP: 一个函数指针,保存了方法地址
它是OC方法实现代码块的地址通过他可以直接访问任意一个方法。免去发送消息的代码IMP声明:

typedef id (&IMP)(id,SEL,...);

IMP 是一个函数指针这个被只想的函数包含一个接收消息的对象id(self 指针)调用方法的选标SEL方法名以及不定个数的方法参数并返回一个id.

IMP与SEL的区别与联系

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

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

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

消息发送

objc_msgSend

以下demo为例定义一个Person类实现study方法并调用该方法。

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject
- (void)study;

@end

NS_ASSUME_NONNULL_END

#import "Person.h"

@implementation Person
- (void)study {
    NSLog(@"%s",__func__);
}
@end

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

打开终端在项目目录下通过clang指令讲main.m文件编译成后缀.cpp的c++类型文件

clang -rewrite-objc main.m

打开找到main函数编译后的方法调用都是通过objc_msgSend发送的证明方法的本质就是消息发送。

#pragma clang assume_nonnull end
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        Person* person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("study"));

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

来到cpp文件的顶部可以看到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);

objc_msgSendSuper

  • 父类Person
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject
- (void)testInstancePrint;

@end

NS_ASSUME_NONNULL_END

#import "Person.h"

@implementation Person

- (void)testInstancePrint {
    NSLog(@"%s", __func__);
}
@end
  • 子类Man
#import "Person.h"

NS_ASSUME_NONNULL_BEGIN

@interface Man : Person
- (void)testInstancePrint;
@end

NS_ASSUME_NONNULL_END

#import "Man.h"
#import <objc/runtime.h>
#import <objc/message.h>
#import <malloc/malloc.h>

@implementation Man
- (instancetype)init {
    if (self = [super init]) {
        NSLog(@"%@", [self class]);
        NSLog(@"%@", [super class]);
    }
    return self;
}

- (void)testInstancePrint {
    NSLog(@"%s", __func__);
}
  • main函数
#import <Foundation/Foundation.h>
#import "Person.h"
#import "Man.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Man* man = [[Man alloc] init];
        [man testInstancePrint];
        
    }
    return 0;
}

运行结果方法打印出的class竟然一致我不是打印的 [super class]

在这里插入图片描述

再次编译成cpp文件一探究竟这次编译的是这个类的实现文件Man.m。可以看到是通过objc_msgSendSuper发送给父类

在这里插入图片描述

苹果官方文档对其方法解释为

当遇到方法调用时编译器会生成对以下函数之一的调用objc_msgSend、objc_msgSend_stret、objc_msgSendSuper或objc_msgSendSuper_stret。发送到对象超类的消息使用super关键字使用objc_msgSendSuper发送其他消息使用objc_msgSend发送。使用objc_msgSendSuper_stret和objc_msgSend_stret发送以数据结构作为返回值的方法。

再翻译参数

super 指向objc_super数据结构的指针。传递值标识消息发送到的上下文包括要接收消息的 类的实例和要开始搜索方法实现的超类op SEL型指针。传递将处理消息的方法的选择器。 …包含方法参数的变量参数列表。

然是发送给"类的实例"回看刚才的代码这里接收者还是self

(__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Man"))}

"开始搜索方法实现的超类"这部分又是什么意思呢

来看objc_super结构体

/// Specifies the superclass of an instance. 
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};

根据编译后的源码参数{(id)self, (id)class_getSuperclass(objc_getClass("Man"))}。模拟super的实现构造objc_super结构体接收者是selfsuper_class使用父类Person

- (void)testInstancePrint {
    //NSLog(@"%s", __func__);
    struct objc_super ff_objc_super;
    ff_objc_super.receiver = self;
    ff_objc_super.super_class = Person.class;

    void* (*objc_msgSendSuperTyped)(struct objc_super *self,SEL _cmd) = (void *)objc_msgSendSuper;
        objc_msgSendSuperTyped(&ff_objc_super,@selector(testInstancePrint));

}

在这里插入图片描述

由此可见方法的接收和查找不一定是同一个

super只是关键字结构体中的super_class 等于父类代表从父类对象开始查找不代表接收者receiver是父类对象

objc_msgSendSuper的区别在于找方法的初始位置不一样。

快速查找

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

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

汇编过程

  • 首先从cmp p0,#0开始这里p0是寄存器存放的是消息接受者。当进入消息发送入口时先判断消息接收者是否存在不存在则重新执行objc_msgSend
  • b.le LNilOrTaggedb是跳转到的意思。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 NORMALcache中查找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就会进入CacheHitreturn or call imp返回或调用方法的实现(imp)。

在这里插入图片描述

CacheHit的内容上图的Mode代表走下面的NORMAL流程authenticate and call imp意思验证并调用方法实现。

// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
    //编码查找imp并且返回x17也就是imp
	TailCallCachedImp x17, x10, x1, x16	// authenticate and call imp
.elseif $0 == GETIMP
	mov	p0, p17
	cbz	p0, 9f			// don't ptrauth a nil imp
	AuthAndResignAsIMP x0, x10, x1, x16	// authenticate imp and re-sign as IMP
9:	ret				// return IMP
.elseif $0 == LOOKUP
	// No nil check for ptrauth: the caller would crash anyway when they
	// jump to a nil IMP. We don't care if that jump also fails ptrauth.
	AuthAndResignAsIMP x17, x10, x1, x16	// authenticate imp and re-sign as IMP
	cmp	x16, x15
	cinc	x16, x16, ne			// x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
	ret				// return imp via x17
.else
.abort oops
.endif
.endmacro

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

  • 如果没有找到缓存查找下一个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汇编部分就结束了接下来到了慢速查找过程c/c++环节。

总结消息发送快速查找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++慢速查找)

慢速查找

什么是方法缓存

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

慢速查找过程

先看lookUpImpOrForward函数的实现

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

    runtimeLock.assertUnlocked();

    if (slowpath(!cls->isInitialized())) {
        // The first message sent to a class is often +new or +alloc, or +self
        // which goes through objc_opt_* or various optimized entry points.
        //
        // However, the class isn't realized/initialized yet at this point,
        // and the optimized entry points fall down through objc_msgSend,
        // which ends up here.
        //
        // We really want to avoid caching these, as it can cause IMP caches
        // to be made with a single entry forever.
        //
        // Note that this check is racy as several threads might try to
        // message a given class for the first time at the same time,
        // in which case we might cache anyway.
        behavior |= LOOKUP_NOCACHE;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.

    runtimeLock.lock();

    // We don't want people to be able to craft a binary blob that looks like
    // a class but really isn't one and do a CFI attack.
    //
    // To make these harder we want to make sure this is a class that was
    // either built into the binary or legitimately registered through
    // objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
    // 检查当前类是个已知类
    checkIsKnownClass(cls);
    // 确定当前类的继承关系
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE); 
    // runtimeLock may have been dropped but is now locked again
    runtimeLock.assertLocked();
    curClass = cls;

    // The code used to lookup the class's cache again right after
    // we take the lock but for the vast majority of the cases
    // evidence shows this is a miss most of the time, hence a time loss.
    //
    // The only codepath calling into this without having performed some
    // kind of cache lookup is class_getInstanceMethod().

    for (unsigned attempts = unreasonableClassCount();;) {
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
            // 如果是常量优化缓存
            // 再一次从cache查找imp
            // 目的防止多线程操作时刚好调用函数此时缓存进来了
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下
            imp = cache_getImp(curClass, sel); //cache中找IMP
            if (imp) goto done_unlock; //找到就直接返回了
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else { //如果不是常量优化缓存
            // 当前类的方法列表。
            method_t *meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }
            // 每次判断都会把curClass的父类赋值给curClass
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                imp = forward_imp;
                break;
            }
        }

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

        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            break;
        }
        if (fastpath(imp)) {
            // 在超类中找到方法。在这个类中缓存它。
            goto done;
        }
    }

    // 没有实现尝试一次方法解析器。
	// 这里就是消息转发机制第一层的入口
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#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;
}

方法首先是定义一个消息的转发forward_imp接着判断类的初始化、加锁、检查是否已知的类…等等先不管这些。重点在于接下来的for循环

// unreasonableClassCount()表示循环的上限
    for (unsigned attempts = unreasonableClassCount();;) {
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
            // 如果是常量优化缓存
            // 再一次从cache查找imp
            // 目的防止多线程操作时刚好调用函数此时缓存进来了
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
            // curClass方法列表。
            method_t *meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }
            // 每次判断都会把curClass的父类赋值给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;
        }
    }

进入了一个循环逻辑

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

查找方式
看看在类和父类继承链中查找imp是个什么样的查找方式的getMethodNoSuper_nolock

/***********************************************************************
 * getMethodNoSuper_nolock
 * fixme
 * Locking: runtimeLock must be read- or write-locked by the caller
 **********************************************************************/
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

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

	// 找到方法列表
    auto const methods = cls->data()->methods();
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         mlists != end;
         ++mlists)
    {
        // <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()

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

fastpath()代表大概会走的路径以下是两种情况的查找。

  • findMethodInSortedMethodListSorted可知从已排序的方法列表里查找采用二分查找。
  • findMethodInUnsortedMethodListUnsorted可知从未排序方法列表用的线性查找通过for循环遍历一个个对比sel从而取出method_t
    看一下findMethodInSortedMethodList函数跳转findMethodInSortedMethodListALWAYS_INLINE代表这是始终内联的
// 方法内联
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    if (list->isSmallList()) {
        if (CONFIG_SHARED_CACHE_RELATIVE_DIRECT_SELECTORS && objc::inSharedCache((uintptr_t)list)) {
            return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.getSmallNameAsSEL(); });
        } else {
            return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.getSmallNameAsSELRef(); });
        }
    } else {
        return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.big().name; });
    }
}

编译后走的是以下流程这是通过二分查找进行方法查找的。

/***********************************************************************
 * search_method_list_inline
 **********************************************************************/
template<class getNameFunc>
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
    ASSERT(list);
		// 二分查找
  	// auto 代表自动匹配类型
    auto first = list->begin();
    auto base = first;
  	// decltype: declare type译为声明类型。这里获取表达式类型
    decltype(first) probe;

    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    
    for (count = list->count; count != 0; count >>= 1) {
        probe = base + (count >> 1);
        
        uintptr_t probeValue = (uintptr_t)getName(probe);
        
        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
                probe--;
            }
            return &*probe;
        }
        
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

分类优先

通过methods()方法可以看到会判断rwe而这就是因为分类产生的内存空间。

在这里插入图片描述

所以分类优先因为分类同名的方法会排在列表靠前。多个分类有同名方法时确保后编译的先调用。

跳出循环后

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缓存到本类cachelog_and_fill_cache。注意这里不管是本类还是本类的父类找到了imp都会缓存到本类中去

跳转 log_and_fill_cache :


/***********************************************************************
* log_and_fill_cache
* Log this method call. If the logger permits it, fill the method cache.
* cls is the method whose cache should be filled. 
* implementer is the class that owns the implementation in question.
**********************************************************************/
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_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);
}

找到之后会放入类的方法缓存里此时方法还未执行。

再回到一开始主方法如果慢查也没找到 curClass 赋值为父类的类对象然后从父类缓存里查找

在这里插入图片描述

如果父类里也没有循环又重头开始直至nil : if (slowpath((curClass = curClass->getSuperclass()) == nil))

此时消息发送阶段结束这时就要进入消息的转发。
总结消息发送慢速查找imp(c/c++):
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

消息的转发

动态决议

上面介绍了方法调用的本质是消息发送。那如果经过查找后没有找到方法系统会怎么处理这就是接下来介绍的方法动态决议和消息转发

动态决议过程

当本类和本类继承链下的cachemethod list都查找不到impimp被赋值成了_objc_msgForward_impcache但是它没有调用会进入动态方法解析流程并且只会执行一次。

resolveMethod_locked的源码声明

/***********************************************************************
* resolveMethod_locked
* Call +resolveClassMethod or +resolveInstanceMethod.
*
* Called with the runtimeLock held to avoid pressure in the caller
* Tail calls into lookUpImpOrForward, also to avoid pressure in the callerb
**********************************************************************/
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();
    //判断是不是元类
    if (! cls->isMetaClass()) {
        // 不是元类则是实例方法的动态方法解析
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        // 是元类则是类方法的动态方法解析
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls); // inst类对象   cls 元类
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

上述代码流程如下

  1. 判断进行解析的是否是元类
  2. 如果不是元类则调用_class_resolveInstanceMethod进行对象方法动态解析
  3. a. 如果是元类则调用_class_resolveClassMethod进行类方法动态解析
    b. 完成类方法动态解析后再次查询cls中的imp如果没有找到则进行一次对象方法动态解析

如果类的实例调用的是实例方法

resolveInstanceMethod(inst, sel, cls);

如果是类对象调用的类方法

// inst类对象     cls 元类
resolveClassMethod(inst, sel, cls); 
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
    resolveInstanceMethod(inst, sel, cls);
}

两个方法resolveInstanceMethodresolveClassMethod。也称为方法的动态决议

上述执行resolveMethod_locked方法后返回lookUpImpOrForwardTryCache

  • 来到lookUpImpOrForwardTryCache方法实际调用的是_lookUpImpTryCache方法
IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior) 
{
    return _lookUpImpTryCache(inst, sel, cls, behavior);
}

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

在这里插入图片描述

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

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

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

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

动态解析测试

resolveClassMethod:默认返回值是NO,如果你想在这个函数里添加方法实现需要借助class_addMethod

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

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

实现一个类类在.h文件中声明一个方法但在.m文件中并没有实现这个方法。在外部调用这个方法就会导致程序崩溃.
原因

  • 第一步查找方法中在自己的类对象以及父类的类对象中都没有找到这个方法的实现
  • 所以转向动态方法解析动态方法解析我们什么也没做
  • 所以进行第三步转向消息转发消息转发我们也什么都没做最后产生崩溃

此时我们在动态方法解析这一步补救它

  • 当调用的是对象方法时动态方法解析是在resolveInstanceMethod方法中实现的
  • 当调用的是类方法时动态方法解析是在resolveClassMethod中实现的

利用动态方法解析和runtime我们可以给一个没有实现的方法添加方法实现。

#import "Person.h"

@interface Man : Person

- (void)test;

@end

#import "Man.h"
#import <objc/runtime.h>
#import <objc/message.h>

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

@end

运行如下

在这里插入图片描述

可以看到为什么会有2次执行呢放到最后再讲。类方法也是如此。

既然是因为找不到imp而崩溃那么我们可以在这个方法里通过runtimeclass_addMethodsel动态的生成imp。其中第四个参数是返回值类型用void用字符串描述“v@:”

BOOL 
class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{
    if (!cls) return NO;

    mutex_locker_t lock(runtimeLock);
    return ! addMethod(cls, name, imp, types ?: "", NO);
}

方法修改

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

-(void)addMethod {
    NSLog(@"%s", __func__);
}

可以看到运行正常

在这里插入图片描述

消息转发

如果系统在动态决议阶段没有找到实现就会进入消息转发阶段。

消息的快速转发

cache没有找到imp类的继承链里的方法列表都没有找到imp并且resolveInstanceMethod / resolveClassMethod 返回NO就会进入消息转发。

我们在 lookUpImpOrForward 的时候就看到 imp 被指定成了_objc_msgForward_impcache

	//如果上述在类对象和父类对象中没有查到方法
	//我们就进入动态方法解析
 if (resolver  &&  !triedResolver) {//triedResolver用来判断是否曾经进行过动态方法解析如果没有那就进入动态方法解析如果进行过就跳过
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst); //动态方法解析函数
        runtimeLock.lock();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES; //进行过动态方法解析就把这个标识为设置为YES
       goto retry;//retry是前面的发送消息的过程
    }
    	
      //如果动态方法解析失败就进入消息转发

    imp = (IMP)_objc_msgForward_impcache; //由这一步进入消息转发
    cache_fill(cls, sel, imp, inst);
//如果消息转发失败程序崩溃
 done:
    runtimeLock.unlock();

所以如果本类没有能力去处理这个消息那么就转发给其他的类让其他类去处理。

看一下进行消息转发的函数__objc_msgForward_impcache的具体实现, 它就是消息转发的流程又到了我们的源码汇编阶段

STATIC_ENTRY __objc_msgForward_impcache
	// Method cache version

	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band condition register is NE for stret, EQ otherwise.

	jne	__objc_msgForward_stret
	jmp	__objc_msgForward

	END_ENTRY __objc_msgForward_impcache
	
	
	ENTRY __objc_msgForward
	// Non-stret version

	movq	__objc_forward_handler(%rip), %r11
	jmp	*%r11

	END_ENTRY __objc_msgForward

但是__objc_msgForward_handler并没有开源。

消息快速转发测试

  • Person类中定义func1方法但是不实现利用-(id)forwardingTargetForSelector:(SEL)aSelector 方法进行消息快速转发
  • Blank类中定义func1方法且实现
#import <Foundation/Foundation.h>

@interface Person : NSObject
- (void)func1;
@end

#import "Person.h"
#import "Blank.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation Person

-(id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%s, aSelector = %@",__func__, NSStringFromSelector(aSelector));

    if (aSelector == @selector(func1)) {
        return [Blank alloc];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end


#import <Foundation/Foundation.h>

@interface Blank : NSObject
- (void)func1;
@end

#import "Blank.h"

@implementation Blank
- (void)func1 {
    NSLog(@"%s",__func__);
}
@end

main.m文件新建person对象并调用func1方法

#import <Foundation/Foundation.h>
#import "Person.h"
#import "Man.h"
#import <objc/runtime.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {   
        Person* person = [[Person alloc] init];
        [person func1];
    }
    return 0;
}

运行如下

在这里插入图片描述

转发的作用在于如果当前对象无法响应消息就将它转发给能响应的对象。

这时候方法缓存在哪接收转发消息的对象

应用场景专门搞一个类来处理这些无法响应的消息。方法找不到时的crash收集。

演示的是实例方法如果是类方法只需要将 - 改成 +

消息的慢速转发

如果消息的快速转发也没有找到方法后面还有个methodSignatureForSelector方法作用是方法有效性签名。

将刚才使用快速转发forwardingTargetForSelector方法注释后添加上methodSignatureForSelector方法后能否正常运行

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

在这里插入图片描述

因为方法签名需要搭配另一个方法forwardInvocation

  • forwardInvocation方法提供了一个入参类型是NSInvocation它提供了targetselector用于指定目标里查找方法实现。
- (void)forwardInvocation:(NSInvocation *)anInvocation;

添加后就能正常运行了

在这里插入图片描述

在调用func1时虽然没有提供方法实现但是在了方法的慢速转发里提供了有效签名只要格式正确和实际返回类型不同也行代码就不崩溃了。

防止系统崩溃的三个救命稻草动态解析、快速转发、慢速转发。

应用场景统一处理没实现的方法进行提示。你也可以不做任何处理这样消息找不到的崩溃就不会出现了。

不过救命稻草不能解决实际问题只是为了app稳定性的一种手段。

总结

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;

消息转发机制基本上分为三个步骤也被称为消息的三次拯救

  1. 动态方法解析
  2. 备援接收者
  3. 完整消息转发

我们可以通过控制这三个步骤其中一环来解决这一个问题

特别注意如果是正常类的消息是不会走到这三个步骤的。所以走到这三个不步骤的前提条件已经确定该消息为未知消息

流程图

在这里插入图片描述

一些问题

runtime是如何通过selector找到对应的IMP地址的

缓存查找–>当前类查找–>父类逐级查找

如果子类调用父类方法缓存在哪个类

  • 子类没有父类方法时该方法的list.ptr为nil说明子类不存储父类方法。
  • 运行方法后先从父类对象缓存里找发现buckets一开始就是nil说明没存在父类对象里那再看当前的子类对象里有没有了。
  • 子类对象的cache缓存了该方法。

回看慢查找方法cls 是当前传入的类curClass是局部变量
在这里插入图片描述

最终是当前类的cache插入缓存

在这里插入图片描述

结论缓存到当前传入的类

两次动态决议的原因

用前面的demo打断点测试一下:
在这里插入图片描述

运行后lldb输入指令bt可以看到打印的信息

第一次进入该断点输入bt显示如下在这里插入图片描述

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

在这里插入图片描述

调用了___forwarding___符号还有熟悉的慢速转发methodSignatureForSelector方法 可知第二次是消息转发

在消息的第一次动态决议和快速转发都没找到方法后进入到慢速转发。过程中runtime还会调用一次lookUpImpOrForward这个方法里包含了动态决议这才造成了二次动态决议。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6