【iOS】消息流程分析

文章目录


前言

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

动态类型

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

bash 复制代码
id someObject = [[NSString alloc] initWithString:@"Hello, World!"];
someObject = [[NSDate alloc] init];

动态绑定

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

动态语言

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

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

消息发送

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

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

调用方法后编译为C++的源码部分

运行时,上面Objc的方法调用会被翻译成一条C语言的函数调用, 如下:

bash 复制代码
id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter)

objc_msgSend

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

bash 复制代码
void objc_msgSend(id self, SEL cmd, ....)

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

SEL(selector)

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

bash 复制代码
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

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

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

bash 复制代码
// 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)

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

bash 复制代码
/// 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,省去了一些列的查找,直接向对象发送消息,效率会高一些。

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

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

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

Method

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

bash 复制代码
/// 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操作函数如下:

bash 复制代码
方法操作主要有以下函数:
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 方法,则最后会报错

快速查找

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

bash 复制代码
//---- 消息发送 -- 汇编入口--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

bash 复制代码
//!!!!!!!!!重点!!!!!!!!!!!!
.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++实现的源码

bash 复制代码
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. 判断是否进行过动态方法解析
    如果没有则执行
    执行过一次则进行消息转发流程

动态方法解析

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

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

bash 复制代码
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

老规矩还是看源码

bash 复制代码
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方法中,如下所示:

bash 复制代码
#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;
}

消息转发流程

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

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

bash 复制代码
- (id)forwardingTargetForSelector:(SEL)aSelector;

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

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

bash 复制代码
#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对象中既能选择方法选择器去替换,还能选择对象去替换

bash 复制代码
- (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会造成报错

总结

相关推荐
心情好的小球藻15 分钟前
Python应用进阶DAY9--类型注解Type Hinting
开发语言·python
惜.己27 分钟前
使用python读取json数据,简单的处理成元组数组
开发语言·python·测试工具·json
Y40900134 分钟前
C语言转Java语言,相同与相异之处
java·c语言·开发语言·笔记
mascon42 分钟前
U3D打包IOS的自我总结
ios
名字不要太长 像我这样就好1 小时前
【iOS】继承链
macos·ios·cocoa
karshey2 小时前
【IOS webview】IOS13不支持svelte 样式嵌套
ios
潜龙95272 小时前
第4.3节 iOS App生成追溯关系
macos·ios·cocoa
古月-一个C++方向的小白6 小时前
C++11之lambda表达式与包装器
开发语言·c++
沐知全栈开发6 小时前
Eclipse 生成 jar 包
开发语言
杭州杭州杭州7 小时前
Python笔记
开发语言·笔记·python