【iOS】消息流程分析

文章目录

  • 前言
    • 动态类型
    • 动态绑定
    • 动态语言
    • 消息发送
    • objc_msgSend
    • SEL(selector)
    • IMP(implementation)
      • IMP高级用法
    • Method
    • SEL、IMP、Method总结
    • 流程概述
  • 快速查找
    • 消息发送快速查找的总结
    • buckets
  • 慢速查找
  • 动态方法解析
    • resolveInstanceMethod && resolveClassMethod
    • 优化
  • 消息转发流程
    • 快速转发(消息接受者替换)
    • 慢速转发(完整消息转发)
  • 总结


前言

前文学习了OC的类和对象的底层原理,看到结构体中涉及到方法列表,特此来学习一下消息发送以及消息转发

动态类型

OC中的对象是动态类型的,这意味着我们可以在运行时发送消息给对象,对象可以根据接收到的消息执行相应的方法。与静态语言类型不同,静态类型在编译时就必须要确定引用哪种对象,而动态类型使用更加泛化

id someObject = [[NSString alloc] initWithString:@"Hello, World!"];
someObject = [[NSDate alloc] init];

动态绑定

动态绑定是指方法调用可以在运行时解析,而不是在编译时。这意味着 Objective-C 在运行时决定要执行对象的哪个方法,而不是在编译时。这种机制是通过消息传递(而非直接函数调用)实现的,使得你可以在程序运行期间改变对象的调用的方法

动态语言

OC能被称为动态语言的一个核心点就是消息转发机制,消息转发机制允许开发者截取并处理未被对象识别的消息

这就使得即使某个方法或是函数没有被实现,编译时也不会报错,因为运行时还可以动态地添加方法

消息发送

OC对象调用方法,其本质就是发送消息,消息有名称与选择自,还可以有返回值

我们先前说过了OC是一门动态语言,其方法在在底层被编译成调用objc_msgSend函数. 这也就是我们在 Runtime 所提到的 消息发送机制

调用方法后编译为C++的源码部分
在这里插入图片描述
运行时,上面Objc的方法调用会被翻译成一条C语言的函数调用, 如下:

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

objc_msgSend

我们可以看到消息发送的核心是这个函数

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

我们来逐步分析一下各个参数

SEL(selector)

在上述苹果官网公开源码objc4的objc.h文件中,定义如下:

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

SEL:一个不透明的类型,代表方法的选择器/选择子。定义如下:

​ 在源码中没有直接找到objc_selector的定义,从一些书籍上与Blog上看到可以将SEL理解为一个char*指针。

// GNU OC 中的 objc_selector
struct objc_selector {  
  void *sel_id;  
  const char *sel_types;  
};  

SEL实际上就是一个方法选择器,告诉编译器我们当前想要调用哪一个方法

在运行时,方法选择器用来表示方法的名字。一个方法选择器就是一个C字符串,在OC的运行时被注册。编译器生成选择器在类加载时由运行时自动映射。
可以在运行时添加新的选择器,并使用sel_registerName函数检索现有的选择器。

获取SEL的三种方式
在这里插入图片描述

注意
OC在编译时会根据方法名字生成唯一一个区分的ID,这个ID是SEL类型的,只要方法名字相同,SEL返回的就相同

Runtime中维护了一个SEL的表,这个表按NSSet来存储,只要相同的SEL就会被看作是同一个方法并被加载到表中

因此OC中需要避免方法重载

IMP(implementation)

指向方法实现的首地址的指针。源码里实现如下:(可以看得出来是对方法类型起了一个别名。)

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif

IMP的数据类型是指针,指向方法实现开始的位置

IMP高级用法

有时候为了避免传递之间的开销,我们希望直接调用IMP,省去了一些列的查找,直接向对象发送消息,效率会高一些。

// 根据代码块获取IMP, 其实就是代码块与IMP关联
IMP imp_implementationWithBlock(id block) 
// 根据Method获取IMP
IMP method_getImplementation(Method m) 
// 根据SEL获取IMP
[[objc Class] instanceMethodForSelector:SEL] 

​ 当我们获取一个方法的IMP后,可以直接调用IMP:

IMP imp = method_getImplementation(Method m);
// result保存方法的返回值,id表示调用这个方法的对象,SEL是Method的选择器,argument是方法的参数。
id result = imp(id, SEL, argument);

Method

一个不透明的类型,表示类中定义的方法。定义如下:

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

可以看出Method是一个结构体类型指针,objc_method结构中有三个属性

method_name:SEL类型(选择器),表示方法名的字符串。

method_types:char*类型的,表示方法的类型;包含返回值和参数的类型。

method_imp:IMP类型,指向方法实现地址的指针。

Method操作函数如下:

方法操作主要有以下函数:
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types); // 添加方法
Method class_getInstanceMethod(Class cls, SEL name); // 获取实例方法
Method class_getClassMethod(Class cls, SEL name); // 获取类方法
Method *class_copyMethodList(Class cls, unsigned int *outCount); // 获取所有方法的数组
// 替代方法的实现
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types); 
// 交换两个方法的实现
method_exchangeImplementations(Method m1, Method m2)

SEL、IMP、Method总结

SEL:方法选择器,实际上就是一个字符串的名字
IMP:指向方法实现首地址的指针
Method:是一个结构体,包含一个SEL表示方法名、一个IMP指向函数的实现地址、一个Char*表示函数的类型(包括返回值和参数类型)

SELIMPMethod之间的关系
当向对象发送消息时,调用SEL在对象的类以及父类方法列表中进行查找Method,因为Method结构体中包含IMP指针,因此一旦找到对应的Method就直接调用IMP去实现方法

流程概述

在这里插入图片描述

接下来我们对发送消息后的操作进行一次流程概述

  • 快速查找:首先会在类的缓存cache中查找指定方法实现,也就是IMP,方法缓存是为了优化性能,避免每次调用都进行频繁查找。如果在缓存中没有找到匹配的方法选择子,则执行慢速查找过程,即调用 _objc_msgSend_uncached 函数,并进一步调用 _lookUpImpOrForward 函数进行全局的方法查找。
  • 慢速查找:如果在对应的类中的缓存列表中没有找到IMP,就去对应的类中的方法列表中寻找,如果还是没有找到就循着SuperClass继承链往上查找父类的缓存列表以及方法列表,找到了则将方法写入当前类的缓存中
  • 动态方法解析:如果前两步都没有找到就进行一次动态方法解析,即调用resolveInstanceMethod/resolveClassMethod 方法,然后再次进行一次前面的查找流程,并且设置标识符表示已经调用过一次动态方法解析,后面不会再次调用
    (如果在这两个方法中有在运行时动态添加新方法,那么再次查找就可能会查找到对应的方法)
  • 消息转发:如果动态方法解析之后还是没有找到方法,那么就会进行消息转发,消息转发中还有两次补救的机会:
  • 先调用forwardingTargetForSelector方法获取新的对象作为receiver 重新执行 selector即使用新的对象发送消息,然后重新执行上面的步骤),如果返回的内容不合法(为 nil 或者跟旧 receiver 一样),那就进入第二步
  • 调用 methodSignatureForSelector 获取方法签名后,判断返回类型信息是否正确,再调用 forwardInvocation 执行 NSInvocation 对象,并将结果返回。如果对象没实现 methodSignatureForSelector 方法,则最后会报错

快速查找

首先我们来到消息发送的源码,我们可以看到源码是由汇编实现的

//---- 消息发送 -- 汇编入口--objc_msgSend主要是拿到接收者的isa信息
ENTRY _objc_msgSend 
//---- 无窗口
	UNWIND _objc_msgSend, NoFrame 
	
//---- p0 和空对比,即判断接收者是否存在,其中p0是objc_msgSend的第一个参数-消息接收者receiver
	cmp	p0, #0			// nil check and tagged pointer check 
//---- le小于 --支持taggedpointer(小对象类型)的流程
#if SUPPORT_TAGGED_POINTERS
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative) 
#else
//---- p0 等于 0 时,直接返回 空
	b.eq	LReturnZero 
#endif 
//---- p0即receiver 肯定存在的流程
//---- 根据对象拿出isa ,即从x0寄存器指向的地址 取出 isa,存入 p13寄存器
	ldr	p13, [x0]    	// p13 = isa 
//---- 在64位架构下通过 p16 = isa(p13) & ISA_MASK,拿出shiftcls信息,得到class信息
	GetClassFromIsa_p16 p13		// p16 = class 
LGetIsaDone:
	// calls imp or objc_msgSend_uncached 
//---- 如果有isa,走到CacheLookup 即缓存查找流程,也就是所谓的sel-imp快速查找流程
	CacheLookup NORMAL, _objc_msgSend

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
//---- 等于空,返回空
	b.eq	LReturnZero		// nil check 

	// tagged
	adrp	x10, _objc_debug_taggedpointer_classes@PAGE
	add	x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
	ubfx	x11, x0, #60, #4
	ldr	x16, [x10, x11, LSL #3]
	adrp	x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
	add	x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
	cmp	x10, x16
	b.ne	LGetIsaDone

	// ext tagged
	adrp	x10, _objc_debug_taggedpointer_ext_classes@PAGE
	add	x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
	ubfx	x11, x0, #52, #8
	ldr	x16, [x10, x11, LSL #3]
	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

我们分析一下步骤:

  1. 判断objc_msgSend方法的第一个参数receiver是否为空
  2. 判断是否是小对象taggedpointer

随后我们进入核心代码CacheLookup

//!!!!!!!!!重点!!!!!!!!!!!!
.macro CacheLookup 
	//
	// Restart protocol:
	//
	//   As soon as we're past the LLookupStart$1 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$1,
	//   then our PC will be reset to LLookupRecover$1 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
	//
LLookupStart$1:

//---- p1 = SEL, p16 = isa --- #define CACHE (2 * __SIZEOF_POINTER__),其中 __SIZEOF_POINTER__表示pointer的大小 ,即 2*8 = 16
//---- p11 = mask|buckets -- 从x16(即isa)中平移16字节,取出cache 存入p11寄存器 -- isa距离cache 正好16字节:isa(8字节)-superClass(8字节)-cache(mask高16位 + buckets低48位)
	ldr	p11, [x16, #CACHE]				
//---- 64位真机
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 
//--- p11(cache) & 0x0000ffffffffffff ,mask高16位抹零,得到buckets 存入p10寄存器-- 即去掉mask,留下buckets
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets 
	
//--- p11(cache)右移48位,得到mask(即p11 存储mask),mask & p1(msgSend的第二个参数 cmd-sel) ,得到sel-imp的下标index(即搜索下标) 存入p12(cache insert写入时的哈希下标计算是 通过 sel & mask,读取时也需要通过这种方式)
	and	p12, p1, p11, LSR #48		// x12 = _cmd & mask 

//--- 非64位真机
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 
	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

//--- p12是下标 p10是buckets数组首地址,下标 * 1<<4(即16) 得到实际内存的偏移量,通过buckets的首地址偏移,获取bucket存入p12寄存器
//--- LSL #(1+PTRSHIFT)-- 实际含义就是得到一个bucket占用的内存大小 -- 相当于mask = occupied -1-- _cmd & mask -- 取余数
	add	p12, p10, p12, LSL #(1+PTRSHIFT)   
		             // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) -- PTRSHIFT是3
		             
//--- 从x12(即p12)中取出 bucket 分别将imp和sel 存入 p17(存储imp) 和 p9(存储sel)
	ldp	p17, p9, [x12]		// {imp, sel} = *bucket 
	
//--- 比较 sel 与 p1(传入的参数cmd)
1:	cmp	p9, p1			// if (bucket->sel != _cmd) 
//--- 如果不相等,即没有找到,请跳转至 2f
	b.ne	2f			//     scan more 
//--- 如果相等 即cacheHit 缓存命中,直接返回imp
	CacheHit $0			// call or return imp 
	
2:	// not hit: p12 = not-hit bucket
//--- 如果一直都找不到, 因为是normal ,跳转至__objc_msgSend_uncached
	CheckMiss $0			// miss if bucket->sel == 0 
//--- 判断p12(下标对应的bucket) 是否 等于 p10(buckets数组第一个元素,),如果等于,则跳转至第3步
	cmp	p12, p10		// wrap if bucket == buckets 
//--- 定位到最后一个元素(即第一个bucket)
	b.eq	3f 
//--- 从x12(即p12 buckets首地址)- 实际需要平移的内存大小BUCKET_SIZE,得到得到第二个bucket元素,imp-sel分别存入p17-p9,即向前查找
	ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket 
//--- 跳转至第1步,继续对比 sel 与 cmd
	b	1b			// loop 

3:	// wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//--- 人为设置到最后一个元素
//--- p11(mask)右移44位 相当于mask左移4位,直接定位到buckets的最后一个元素,缓存查找顺序是向前查找
	add	p12, p12, p11, LSR #(48 - (1+PTRSHIFT)) 
					// p12 = buckets + (mask << 1+PTRSHIFT) 
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	add	p12, p12, p11, LSL #(1+PTRSHIFT)
					// p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif

	// Clone scanning loop to miss instead of hang when cache is corrupt.
	// The slow path may detect any corruption and halt later.
//--- 再查找一遍缓存()
//--- 拿到x12(即p12)bucket中的 imp-sel 分别存入 p17-p9
	ldp	p17, p9, [x12]		// {imp, sel} = *bucket 
	
//--- 比较 sel 与 p1(传入的参数cmd)
1:	cmp	p9, p1			// if (bucket->sel != _cmd) 
//--- 如果不相等,即走到第二步
	b.ne	2f			//     scan more 
//--- 如果相等 即命中,直接返回imp
	CacheHit $0			// call or return imp  
	
2:	// not hit: p12 = not-hit bucket
//--- 如果一直找不到,则CheckMiss
	CheckMiss $0			// miss if bucket->sel == 0 
//--- 判断p12(下标对应的bucket) 是否 等于 p10(buckets数组第一个元素)-- 表示前面已经没有了,但是还是没有找到
	cmp	p12, p10		// wrap if bucket == buckets 
	b.eq	3f //如果等于,跳转至第3步
//--- 从x12(即p12 buckets首地址)- 实际需要平移的内存大小BUCKET_SIZE,得到得到第二个bucket元素,imp-sel分别存入p17-p9,即向前查找
	ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket 
//--- 跳转至第1步,继续对比 sel 与 cmd
	b	1b			// loop 

LLookupEnd$1:
LLookupRecover$1:
3:	// double wrap
//--- 跳转至JumpMiss 因为是normal ,跳转至__objc_msgSend_uncached

	JumpMiss $0 
.endmacro

//以下是最后跳转的汇编函数
.macro CacheHit
.if $0 == NORMAL
	TailCallCachedImp x17, x12, x1, x16	// authenticate and call imp
.elseif $0 == GETIMP
	mov	p0, p17
	cbz	p0, 9f			// don't ptrauth a nil imp
	AuthAndResignAsIMP x0, x12, 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, x12, x1, x16	// authenticate imp and re-sign as IMP
	ret				// return imp via x17
.else
.abort oops
.endif
.endmacro

.macro CheckMiss
	// miss if bucket->sel == 0
.if $0 == GETIMP 
//--- 如果为GETIMP ,则跳转至 LGetImpMiss
	cbz	p9, LGetImpMiss
.elseif $0 == NORMAL 
//--- 如果为NORMAL ,则跳转至 __objc_msgSend_uncached
	cbz	p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP 
//--- 如果为LOOKUP ,则跳转至 __objc_msgLookup_uncached
	cbz	p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

.macro JumpMiss
.if $0 == GETIMP
	b	LGetImpMiss
.elseif $0 == NORMAL
	b	__objc_msgSend_uncached
.elseif $0 == LOOKUP
	b	__objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

消息发送快速查找的总结

检查消息接收者是否存在,为nil则不做任何处理
通过isa指针找到对应的class对象
找到class类对象进行内存平移找到cache
cache中获取buckets
buckets中对比参数SEL,查看缓存中有没有同名的方法
如果buckets中有对应的sel --> cacheHit --> 调用imp
如果在缓存中没有找到匹配的方法选择子,则执行慢速查找过程,即调用 _objc_msgSend_uncached 函数,并进一步调用 _lookUpImpOrForward 函数进行全局的方法查找。

总的来说:消息发送会先通过缓存进行查找方法实现,如果在缓存中没有找到方法实现,就会进入慢速查找过程,去类的方法列表以及父类链中进行循环查找

buckets

我们在上面的源码中提到了buckets

bucket” 是缓存的基本存储单位,通常包含了一个方法选择器 (SEL) 和一个对应的方法实现指针 (IMP)。当你向一个对象发送消息时,Objective-C 运行时会使用选择器(SEL)作为键,在缓存中查找对应的 bucket。如果找到了相应的 bucket,就可以直接获取到方法的 IMP 并执行,大大加快了方法调用的速度。

慢速查找

我们在上文说到如果我们在缓存中查找不到我们的方法,会进入__objc_msgSend_uncached汇编函数

其核心是MethodTableLookup(即查询方法列表),其源码的核心是_lookUpImpOrForward

其是一个用C++实现的源码

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();

    // Optimistic cache lookup
    // 优先尝试从缓存中获取方法实现,这是一个快速路径
    if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock; // 如果找到,则直接跳到函数结束,避免锁操作
    }

    // 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.
    //
    // TODO: this check is quite costly during process startup.
    
    // 校验给定的类对象是合法已注册的类,防止通过伪造对象进行攻击。
    checkIsKnownClass(cls);

    // 如果类还未完全实现(realized),则进行实现,并保证锁在此过程中有效。
    if (slowpath(!cls->isRealized())) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
        // runtimeLock may have been dropped but is now locked again
    }

    // 如果需要初始化类,并且类还未初始化,则进行初始化。
    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
        // runtimeLock may have been dropped but is now locked again

        // If sel == initialize, class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }

    runtimeLock.assertLocked();
    curClass = cls;

    // The code used to lookpu 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();;) {
        // curClass method list.
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            imp = meth->imp;
            goto done;
        }

        // 如果没有找到实现,并且已经到了超类链的顶部,使用转发机制。
        if (slowpath((curClass = curClass->superclass) == nil)) {
            // No implementation found, and method resolver didn't help.
            // Use forwarding.
            imp = forward_imp;
            break;
        }

        // Halt if there is a cycle in the superclass chain.
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            break;
        }
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }

    // No implementation found. Try method resolver once.

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

 done:
    log_and_fill_cache(cls, imp, sel, inst, curClass);
    runtimeLock.unlock();
 done_nolock:
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

主要有以下几步:

  1. cache缓存中再次进行一次快速查找,防止多线程修改缓存,找到则直接返回,没找到就进入第二步
  2. 判断cls
    检查类是否已经实现,如果没有就需要先实现以确定父类链、rw
    检查是否初始化,如果没有,则进行初始化
  3. 进行for循环,沿着superclass继承链进行缓存与方法列表查找,具体步骤如下:
    1️⃣在当前cls的方法列表中使用二分查找查找方法,如果找到就进入将其写入cache并返回imp
    2️⃣当前cls被赋值为父类,如果父类等于nil则进行消息转发
    3️⃣在父类缓存中查找方法,如果找到则将其写入原始类的cache中,没找到则继续查找
  4. 判断是否进行过动态方法解析
    如果没有则执行
    执行过一次则进行消息转发流程

在这里插入图片描述

动态方法解析

如果快速查找与慢速查找都没有找到我们的方法,那么就会进入我们的动态方法解析

同样的我们首先查看动态解析的源码

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);
        //为什么要有这行代码? -- 类方法在元类中是对象方法,所以还是需要查询元类中对象方法的动态方法决议
        if (!lookUpImpOrNil(inst, sel, cls)) { //如果没有找到或者为空,在元类的对象方法解析方法中查找
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    //如果方法解析中将其实现指向其他方法,则继续走方法查找流程
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}

主要分为以下几步

  • 首先判断类是否为元类
  • 类:执行实例方法的动态方法决议resolveInstanceMethod
  • 元类:执行类方法的动态方法决议resolveClassMethod,如果在元类中没有找到或者为空,则在元类的实例方法的动态方法决议resolveInstanceMethod中查找,主要是因为类方法在元类中是实例方法,所以还需要查找元类中实例方法的动态方法决议
  • 如果确实在我们resolveInstanceMethod/resolveClassMethod这两个方法中将方法实现指向了其他方法,则继续调用lookUpImpOrForward进行一次慢速查找

resolveInstanceMethod && resolveClassMethod

老规矩还是看源码

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    
    // look的是 resolveInstanceMethod --相当于是发送消息前的容错处理
    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel); //发送resolve_sel消息

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    //查找say666
    IMP imp = lookUpImpOrNil(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));
        }
    }
}

主要分为以下步骤:

  • 发送resolveInstanceMethod消息前,需要查找cls类中是否有该方法的实现,即通过lookUpImpOrNil方法又会进入lookUpImpOrForward慢速查找流程查找resolveInstanceMethod方法
    如果没有,则直接返回
    如果有,则发送resolveInstanceMethod消息
  • 再次慢速查找实例方法的实现,即通过lookUpImpOrNil方法又会进入lookUpImpOrForward慢速查找流程查找实例方法

对于resolveClassMethod实现不在过多赘述,与resolveInstanceMethod相似

优化

我们通过源码可以得知无论是类方法还是实例方法,最后都会来到resolveClassMethod中,因此我们可以将实例方法 和 类方法的统一处理放在resolveInstanceMethod方法中,如下所示:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface MyClass : NSObject
@end

@implementation MyClass

// 动态方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(dynamicMethod)) {
        class_addMethod([self class], sel, (IMP)dynamicMethodImplementation, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void dynamicMethodImplementation(id self, SEL _cmd) {
    NSLog(@"Dynamic method has been resolved and called.");
}

@end

int main() {
    @autoreleasepool {
        MyClass *obj = [MyClass new];
        
        // 调用未实现的方法,触发动态方法解析
        [obj dynamicMethod];
    }
    return 0;
}

消息转发流程

快速转发(消息接受者替换)

如果动态方法解析仍没有找到方法实现,那么就会进入消息转发中的快速转发(消息接受者替换),给开发者一个机会返回一个能够响应该方法的对象。该方法的签名如下:

- (id)forwardingTargetForSelector:(SEL)aSelector;

开发者可以在该方法中根据需要返回一个实现了该方法的对象,使得该对象能够接收并处理该消息。返回的对象会被用于接收消息,并执行对应的方法。如果返回nil,则进入下一步的消息转发机制。

通俗理解解释当前接收者无法处理消息,要将消息交给其他接收者处理,也就是指定一个B对象去处理A对象无法处理的消息

#import <Foundation/Foundation.h>

// 定义备用接收者对象
@interface AnotherObject : NSObject
- (void)anotherMethod;
@end

@implementation AnotherObject

- (void)anotherMethod {
    NSLog(@"Method implemented in AnotherObject.");
}

@end

// 主要对象
@interface MyClass : NSObject
@end

@implementation MyClass

// 备用接收者
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(anotherMethod)) {
        return [AnotherObject new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

@end

int main() {
    @autoreleasepool {
        MyClass *obj = [MyClass new];
        
        // 调用未实现的方法,触发备用接收者
        [obj performSelector:@selector(anotherMethod)];
    }
    return 0;
}

慢速转发(完整消息转发)

这一步骤中涉及到了两个方法
-methodSignatureForSelector:: 当一个对象收到它无法识别的消息时,Objective-C 运行时首先调用这个方法以获取该消息对应方法的签名。这个方法签名是用于创建 NSInvocation 对象的基础,其中包括方法的返回类型、参数类型等信息

-forwardInvocation:: 一旦运行时通过-methodSignatureForSelector: 获得了方法签名并创建了 NSInvocation 对象,它接着调用 -forwardInvocation:。这个方法具体实现消息的转发过程,可以通过修改 NSInvocation 对象的属性(如目标对象和选择器)来控制消息如何被转发。

我们在NSInvocation对象中既能选择方法选择器去替换,还能选择对象去替换

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    // 如果当前类不响应某个方法,尝试从 SecondaryClass 获取方法签名
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (!signature) {
        signature = [self.secondaryHandler methodSignatureForSelector:aSelector];
    }
    return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {  
    anInvocation.selector = @selector(handleUnrecognizedMessage:);
     //选择一个消息接收者(对象)去替换
     anInvocation.target = self.secondaryHandler;
     [anInvocation invoke];
}

这里需要注意的是一定需要实现methodSignatureForSelector:方法之后返回NSInvocation *对象再去调用forwardInvocation才能算完成完整的消息转发,单单调用forwardInvocation会造成报错
在这里插入图片描述

总结

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/586236.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

如何远程访问服务器?

在现代信息技术的快速发展下&#xff0c;远程访问服务器已成为越来越多用户的需求。远程访问服务器能够让用户随时随地通过网络连接服务器&#xff0c;实现数据的传输和操作。本文将介绍远程访问服务器的概念&#xff0c;以及一种广泛应用于不同行业的远程访问解决方案——【天…

软考之零碎片段记录(二十九)+复习巩固(十七、十八)

学习 1. 后缀式&#xff08;逆波兰式&#xff09; 2. c/c语言编译 类型检查是语义分析 词法分析。分析单词。如单词的字符拼写等语法分析。分析句子。如标点符号、括号位置等语言上的错误语义分析。分析运算符、运算对象类型是否合法 3. java语言特质 即时编译堆空间分配j…

2024抖音AI图文带货班:在这个赛道上 乘风破浪 拿到好效果

课程目录 1-1.1 AI图文学习指南 1.mp4 2-1.2 图文带货的新机会 1.mp4 3-1.3 2024年优质图文新标准 1.mp4 4-1.4 图文如何避免违规 1.mp4 5-1.5 优质图文模板解析 1.mp4 6-2.1 老号重启 快速破局 1.mp4 7-2.2 新号起号 不走弯路 1.mp4 8-2.3 找准对标 弯道超车 1.mp4 9…

深度学习之基于Tensorflow卷积神经网络公共区域行人人流密度可视化系统

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 一、项目背景 在公共区域&#xff0c;如商场、火车站、地铁站等&#xff0c;人流密度的监控和管理对于确保公共安全…

anaconda的安装和Jupyter Notebook修改默认路径

anaconda的安装 就一个注意事项:在结尾时候记得配置系统环境变量 要是没有配置这个环境变量,后面就不能cmd启动Jupyter Notebook Jupyter Notebook修改默认路径 我们要找到Jupyter Notebook的配置文件 输入下面指令 jupyter notebook --generate-config就可以找到存放配置文…

Linux图形化界面怎么进入?CentOS 7图形界面切换

CentOS 7默认只安装命令行界面。要切换到图形界面&#xff0c;需要先检查系统是否安装图形界面&#xff0c;在终端输入以下命令&#xff1a; systemctl get-default若是返回结果是“multi-user.target”表示系统没有安装图形界面&#xff1b;若是返回结果是“graphical.target…

上传jar到github仓库,作为maven依赖存储库

记录上传maven依赖包到github仓库问题 利用GitHubPackages作为依赖的存储库踩坑1 仓库地址问题踩坑2 Personal access tokens正确姿势一、创建一个普通仓库&#xff0c;比如我这里是fork的腾讯Shadow到本地。地址是&#xff1a;https://github.com/dhs964057117/Shadow二、生成…

【分享】如何将word格式文档转化为PDF格式

在日常的办公和学习中&#xff0c;我们经常需要将Word文档转换为PDF格式。PDF作为一种通用的文件格式&#xff0c;具有跨平台、易读性高等优点&#xff0c;因此在许多场合下都更为适用。那么&#xff0c;如何实现Word转PDF呢&#xff1f;本文将介绍几种常用的方法&#xff0c;帮…

Git推送本地项目到gitee远程仓库

Git 是一个功能强大的分布式版本控制系统&#xff0c;它允许多人协作开发项目&#xff0c;同时有效管理代码的历史版本。开发者可以克隆一个公共仓库到本地&#xff0c;进行更改后将更新推送回服务器&#xff0c;或从服务器拉取他人更改&#xff0c;实现代码的同步和版本控制。…

Stability AI 推出稳定音频 2.0:为创作者提供先进的 AI 生成音频

概述 Stability AI 的发布再次突破了创新的界限。这一尖端模型以其前身的成功为基础&#xff0c;引入了一系列突破性的功能&#xff0c;有望彻底改变艺术家和音乐家创建和操作音频内容的方式。 Stable Audio 2.0 代表了人工智能生成音频发展的一个重要里程碑&#xff0c;为质量…

PHP算命源码_最新测算塔罗源码_可以运营

功能介绍 八字精批、事业财运、姓名分析、宝宝起名、公司测名、姓名配对、综合详批、姻缘测算、牛年感情、PC版测算、八字合婚、紫微斗数、鼠年运程、月老姻缘、许愿祈福、号码解析、塔罗运势、脱单占卜、感情继续、脱单占卜、塔罗爱情、心理有你、能否复合、暗恋对象、是否分…

JavaScript任务执行模式:同步与异步的奥秘

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

设计模式之代理模式ProxyPattern(六)

一、代理模式介绍 1、什么是代理模式&#xff1f; 代理模式是一种结构型设计模式&#xff0c;它允许为其他对象提供一个替代品或占位符&#xff0c;以控制对这个对象的访问。 2、代理模式的角色构成 抽象主题&#xff08;Subject&#xff09;&#xff1a;定义了真实主题和代…

FineBI学习:K线图

效果图 底表结构&#xff1a;日期、股票代码、股票名称、开盘价、收盘价、最高价、最低价 步骤&#xff1a; 横轴&#xff1a;日期 纵轴&#xff1a;开盘价、最低价 选择【自定义图表】&#xff0c;或【瀑布图】 新建字段&#xff1a;价差&#xff08;收盘-开盘&#xf…

鸿蒙准备1

鸿蒙心路 感慨索性&#xff0c; 看看鸿蒙吧。打开官网相关介绍 新建工程目录结构 感慨 最近面试Android应用开发&#xff0c;动不动就问framework的知识&#xff0c;什么touch事件的触发源是啥&#xff08;eventHub&#xff09;&#xff0c;gc流程是啥&#xff0c;图形框架是什…

VS2022 .Net6.0 无法打开窗体设计器

拿Vs2022 建了个Demo&#xff0c;运行环境是net6.0-windows&#xff0c;无论双击或是右键都打不开窗体设计器 打开项目目录下的*.csproj.user <?xml version"1.0" encoding"utf-8"?> <Project ToolsVersion"Current" xmlns"htt…

【Hadoop】-Hive客户端:HiveServer2 Beeline 与DataGrip DBeaver[14]

HiveServer2 & Beeline 一、HiveServer2服务 在启动Hive的时候&#xff0c;除了必备的Metastore服务外&#xff0c;我们前面提过有2种方式使用Hive&#xff1a; 方式1&#xff1a; bin/hive 即Hive的Shell客户端&#xff0c;可以直接写SQL方式2&#xff1a; bin/hive --…

llama_index微调BGE模型

微调模型是为了让模型在特殊领域表现良好,帮助其学习到专业术语等。 本文采用llama_index框架微调BGE模型,跑通整个流程,并学习模型微调的方法。 一、环境准备 Linux环境,GPU L20 48G,Python3.8.10。 pip该库即可。 二、数据准备 该框架实现了读取各种类型的文件,给…

C++学习第十六课:宏与模板的基础讲解示例

C学习第十六课&#xff1a;宏与模板的深度解析 宏和模板是C中两个强大的特性&#xff0c;它们都允许编写灵活且通用的代码。宏通过预处理器实现&#xff0c;而模板则是C的编译时特性。本课将深入探讨宏的定义、使用以及潜在的问题&#xff0c;以及模板的基本使用、特化、偏特化…

LeetCode 110.平衡二叉树(Java/C/Python3/Go实现含注释说明,Easy)

标签 树深度优先搜索递归 题目描述 给定一个二叉树&#xff0c;判断它是否是高度平衡的二叉树。 本题中&#xff0c;一棵高度平衡的二叉树定义为&#xff1a; 一个二叉树每个节点的左右两个子树的高度差的绝对值不超过1。 原题&#xff1a;LeetCode 110.平衡二叉树 思路及…
最新文章