【iOS】探索消息流程

news2025/5/20 23:59:53

探索消息流程

  • Runtime介绍
  • `OC`三大核心动态特性
    • 动态类型
    • 动态绑定
    • 动态语言
  • 方法的本质
    • 代码转换
    • `objc_msgSend`
      • SEL
      • IMP
      • Method
    • 父类方法在子类中的实现
  • 消息查找流程
    • 开始查找
    • 快速查找流程
    • 慢速查找流程
      • 二分查找方法列表
      • 父类缓存查找
  • 动态方法解析
    • 动态方法决议
    • 实例方法
    • 类方法
    • 优化
  • 消息转发机制
    • 快速转发流程
    • 慢速转发

Runtime介绍

runtime是一套API,由C、C++、汇编一起写成的,为OC提供运行时,区别于编译时:

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

runtime的使用有以下三种方式,三种实现方法与编译层和底层的关系如图所示:

  • 通过OC代码,例如[person sayNB]
  • 通过NSObject方法,例如isKindOfClass
  • 通过Runtime API,例如class_getInstanceSize

请添加图片描述

其中的compiler就是我们了解的编译器,即LLVM,例如OC的alloc 对应底层的objc_allocruntime system libarary 就是底层库

OC三大核心动态特性

动态类型

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

id someObject = [[NSString alloc] initWithString:@"hello"];
someObject = [[NSData alloc] init];//运行时someObject的类型转换成了NSDate

动态绑定

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

动态语言

OC被称为动态语言的一个核心点就是消息转发机制,消息转发机制允许开发者截取并处理未被对象识别的消息。这使得即使某个方法或者函数没有被实现,编译的时候也不会报错,因为在运行时还可以动态的添加方法。

方法的本质

这里我们先使用Clang将mian.m编译成cpp文件

CJLPerson* person = [CJLPerson alloc];
CJLTeacher* teacher = [CJLTeacher alloc];
[person sayHello];

//使用Clang编译
CJLPerson* person = ((CJLPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("CJLPerson"), sel_registerName("alloc"));
CJLTeacher* teacher = ((CJLTeacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("CJLTeacher"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));

通过上述代码我们可以看出,方法的本质就是objc_msgSend函数给对象发送消息。

代码转换

  • ((CJLTeacher *(*)(id, SEL))是类型强转,将objc_msgSend强制转换为特定返回类型和参数的函数指针
  • ((id)objc_getClass("CJLTeacher")是获取CJLTeacher对象
  • sel_registerName("alloc"))等同于@selector()

objc_msgSend

函数原型:

id objc_msgSend(id self, SEL op, ...);

SEL

在源码中,其定义如下所示:

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

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

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

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

在运行时,方法选择器用来表示方法的名字,一个方法选择器就预示一个C字符串,在OC运行的时候被注册,编译器生成选择器在类加载时由运行时自动映射。

可以在运行时 添加新的选择器,并使用sel_registerName函数检索现有的选择器。

获取SEL的三种方式:

SEL selA = @selector(study);
SEL selB = sel_registerName(@"study");
SEL selC = NSSelectorFromString(@"study");

注意:

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

Runtime中维护一个SEL的表,该表按照NSSet来存储,只要相同的SEL就会被看作是同一个方法并且被加载到表中,故而OC中需要避免方法重载

IMP

指向方法实现的首地址的指针,这里我们可以看到一个他的定义:

#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif

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

Method

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

typedef struct objc_method *Method;

struct objc_method {
    SEL _Nonnull method_name   OBJC2_UNAVAILABLE; //表示方法名的字符串
    char * _Nullable method_types   OBJC2_UNAVAILABLE; //char* 类型的,表示方法的类型,包含返回值和参数的类型
    IMP _Nonnull method_imp    OBJC2_UNAVAILABLE; //IMP类型,指向方法实现地址的指针
}                                                            OBJC2_UNAVAILABLE;

这里我们可以看出Method是一个结构体类型指针

当向对象发送消息的时候,调用SEL在对象的类以及父类方法列表中进行查找Method,由于Method结构体中包含IMP指针,故而一旦找到对应的Method就直接调用IMP去实现方法

父类方法在子类中的实现

这里我们定义两个类,一个类继承于另一个类

-(void)study {
    NSLog(@"%@ say: %s",[self class], __func__);
    NSLog(@"%@ say: %s",[super class], __func__);
//     [super study];
}

我们使用Clang将这段代码编译成cpp代码:

static void _I_MyTeacher_study(MyTeacher * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_07_67glw_2n3csgg41v0tztm7w00000gn_T_MyTeacher_a75b57_mi_0,((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")), __func__);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_07_67glw_2n3csgg41v0tztm7w00000gn_T_MyTeacher_a75b57_mi_1,((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MyTeacher"))}, sel_registerName("class")), __func__);
     ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MyTeacher"))}, sel_registerName("study"));
}

我们可以发现在编译期间[super class]转化成了objc_msgSendSuper的方式发送消息,其中消息的接受者依旧是self

消息查找流程

开始查找

这里我们主要研究arm64结构的汇编实现,来到objc-msg-arm64.s,这里先给出其汇编的整体流程图:

请添加图片描述

//----消息发送 -- 汇编入口 -- objc_msgSend主要是拿到接受者的isa信息	
ENTRY _objc_msgSend
//--- 流程开始,无需frame
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 
	GetTaggedClass
	b	LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

LReturnZero:
	// x0 is already zero
	mov	x1, #0
	movi	d0, #0
	movi	d1, #0
	movi	d2, #0
	movi	d3, #0
	ret

	END_ENTRY _objc_msgSend

判断objc_msgSend方法的第一个参数receiver是否为空

  • 如果支持taggedpointer(小对象类型),跳转到LNilOrTagged:
    • 如果小对象为空,则直接返回空,即LReturnZero
    • 如果小对象不为空,则处理小对象的isa,走到第二步
  • 如果既不是小对象,receiver也不为空,就有以下两步:
    • receiver中取出isa存入p13寄存器
    • 通过GetClassFromIsa_p16中,arm64架构下通过isa & ISA_MASK获取shiftcls位域的类信息,即class,而后走到第二步
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
//这里用于watchOS
#if SUPPORT_INDEXED_ISA
	// Indexed isa
//将isa的值存入p16寄存器
	mov	p16, \src			// optimistically set dst = src
	tbz	p16, #ISA_INDEX_IS_NPI_BIT, 1f	// done if not non-pointer isa
	// isa in p16 is indexed
	adrp	x10, _objc_indexed_classes@PAGE
	add	x10, x10, _objc_indexed_classes@PAGEOFF
	ubfx	p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
	ldr	p16, [x10, p16, UXTP #PTRSHIFT]	// load class from array
1:
//用于64位系统
#elif __LP64__
.if \needs_auth == 0 // _cache_getImp takes an authed class already
	mov	p16, \src
.else
	// 64-bit packed isa
	ExtractISA p16, \src, \auth_address
.endif
#else
	// 32-bit raw isa即用于32位系统
	mov	p16, \src

#endif
  • 获取isa结束,进入查找流程CacheLookup

快速查找流程

.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
	//

	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, #CACHE]				// p10 = mask|buckets
	lsr	p11, p10, #48			// p11 = mask
	and	p10, p10, #0xffffffffffff	// p10 = buckets
	and	w12, w1, w11			// x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16//在64位真机
 //从x16(isa)中平移16个字节,取出cache存入p11寄存器 -- isa距离cache刚好16字节   
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
	tbnz	p11, #0, LLookupPreopt\Function
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
	tbnz	p11, #0, LLookupPreopt\Function
#endif
	eor	p12, p1, p1, LSR #7
	and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
//
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
	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

	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

						// do {
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
	cmp	p9, p1				//     if (sel != _cmd) {
	b.ne	3f				//         scan more
						//     } else {
2:	CacheHit \Mode				// hit:    call or return imp
						//     }
3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
	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

总结:

  • 检查消息接受者是否存在,若为nil则不做任何的处理
  • 通过receiver的isa指针找到对应的class对象
  • 找到class类对象进行内存平移找到cache,而后从其中获取buckets
  • buckets中对比参数SEL,查看缓存中有没有同名的方法
  • 如果buckets中有对应的sel -->cacheHit --> 调用imp
  • 若在缓存中没有找到匹配的方法选择子sel,就执行慢速查找,调用_objc_msgSend_uncached函数,并进一步调用_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锁
    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().

  //----查找类的缓存
  // unreasonableClassCount -- 表示类的迭代的上限,防止出现无限循环
  //(猜测这里递归的原因是attempts在第一次循环时作了减一操作,然后再次循环时,仍在上限范围内,所以可以继续递归)
    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.
          //---当前类方法列表(采用二分查找算法),若找到,则返回,将方法缓存到cache中
            method_t *meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }

            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {//当前类=当前类的父类,判断父类是否为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;
        }//如果在父类中找到了forword,就停止查找,并且不缓存,首先调用此类的方法解析器
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }//在父类中找到方法,存储在cache中
    }

    // No implementation found. Try method resolver once.--没有找到方法实现,尝试一次方法解析
	
    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;
}

总结

  • cache缓存中进行查找,即快速查找,找到则直接返回imp

  • 判断cls:

    • 类是否实现,若没有则先实现,确定父类链。这时实例化的目的是为了确定父类链、ro、以及rw等内容
    • 是否初始化,若没有则进行初始化
  • 进行for循环,按照类继承链或者元类继承链的顺序去查找

    • 当前cls的方法列表中使用二分查找算法查找方法,若找到,就进入cache写入流程,并且返回imp,没有找到就返回nil

    • 父类链中若存在循环,直接报错终止循环

    • 父类缓存中查找方法没找到则返回nil,继续循环查找;找到的话,直接返回imp,执行写入流程

  • 判断是否执行过动态方法解析:

    • 若没有,执行动态方法解析
    • 若执行过一次,走消息转发流程

以上就是方法慢速查找流程,下面分别详细解释一下二分查找原理和父类缓存查找详细步骤

二分查找方法列表

查找方法列表流程

getMethodNoSuper_nolock->search_method_list_inline->findMethodInSortedMethodList

template<class getNameFunc>
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
    ASSERT(list);

    auto first = list->begin();
    auto base = first;
    decltype(first) probe;

    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    //base相当于low,count是max,probe是middle,这就是一个二分算法
    for (count = list->count; count != 0; count >>= 1) {
      //从首地址+下标移动到中间位置
        probe = base + (count >> 1);
        
        uintptr_t probeValue = (uintptr_t)getName(probe);
        //如果查找的key的keyValue等于中间位置的probeValue,就直接返回中间位置
        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;
}

请添加图片描述

这里笔者附图详细说明二分查找方法列表的步骤

父类缓存查找

cache_getImp方法是通过汇编_cache_getImp实现,传入的$0GETIMP,如下所示 父类缓存查找流程

  • 如果父类缓存中找到了方法实现,则跳转至CacheHit即命中,则直接返回imp
  • 如果在父类缓存中,没有找到方法实现,则跳转至CheckMiss 或者 JumpMiss,通过判断$0 跳转至LGetImpMiss,直接返回nil

动态方法解析

当我们在使用objc_msgSend的快速查找与慢速查找都没有找到方法实现的一个情况下,Apple官方给出了两种建议:

  1. 动态方法决议:慢速查找结束后,会执行一次动态方法决议
  2. 消息转发:若使用动态方法决议韩式没有找到实现,就会进行消息转发

动态方法决议

当慢速查找流程未找到方法实现的时候,会先尝试一次动态方法决议

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

动态决议方法的意义在于在运动时动态地为为实现方法提供实现

步骤详细:

  • 判断类是否为元类
    • 如果是类,执行实例方法的动态方法决议resolveInstanceMethod
    • 如果是元类,执行类方法的动态方法决议resolveClassMethod,如果在元类中没找到或者为空,就在元类的实例方法的动态方法决议resolveInstanceMethod中查找,主要是因为类方法在元类中是实例方法,所以还需要查找元类中实例方法的动态方法决议
  • 如果动态方法决议中,将其实现指向了其他方法,则继续指定的imp,即继续慢速查找lookUpImpOrForward

流程如下所示:

请添加图片描述

实例方法

针对实例方法的调用,当我们最后尝试动态方法决议的时候,会走到resolveInstanceMethod这一步:

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);
		//这一步相当于发送消息前的容错处理
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // 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
 		//查找详细方法,例如sayHello
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

如下步骤所示:

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

崩溃修改:

针对实例方法sayHello未实现的报错崩溃,可以通过类中重写resolveInstanceMethod类方法,将其指向其他方法的实现,在CJLPerson中重写resolveInstanceMethod将实例方法sayHello的实现指向say666方法实现就可以了,如下:

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(sayHello)) {
        NSLog(@"%@哦no", NSStringFromSelector(sel));
        //获取sayMaster方法的imp
        IMP imp = class_getMethodImplementation(self, @selector(say666));
        //获取sayMaster的实例方法
        Method sayMethod  = class_getInstanceMethod(self, @selector(say666));
        //获取sayMaster的丰富签名
        const char *type = method_getTypeEncoding(sayMethod);
        //将sel的实现指向sayMaster
        return class_addMethod(self, sel, imp, type);
    }
    return [super resolveClassMethod:sel];
}

这样就可以打印正确的结果了

类方法

对于类方法而言,与实例方法其实是相同的,同样通过重写resolveClassMethod类方法来解决问题,下面代码举例:

+ (BOOL)resolveClassMethod:(SEL)sel:(SEL)sel {
    if (sel == @selector(sayMethod)) {
        NSLog(@"%@哦no", NSStringFromSelector(sel));
        //获取sayMaster方法的imp
        IMP imp = class_getMethodImplementation(objc_getMetaClass("CJLPerson"), @selector(say666));
        //获取sayMaster的实例方法
        Method sayMethod  = class_getInstanceMethod(objc_getMetaClass("CJLPerson"), @selector(say666));
        //获取sayMaster的丰富签名
        const char *type = method_getTypeEncoding(say666);
        //将sel的实现指向sayMaster
        return class_addMethod(self, sel, imp, type);
    }
    return [super resolveClassMethod:sel];
}

这里我们需要注意的是,在resolveClassMethod中传入cls不是类,而是元类,可以通过objc_getMetaClass来获取类的元类,这时由于类方法在元类中是实例方法

优化

我们通过方法慢速查找流程可以方法查找的两条路径:

  • 实例方法:类 – 父类 – 根类 – nil
  • 类方法:元类 – 根元类 – 根类 – nil

看这两者的路径,我们可以发现他们都会在跟类即NSObject中来查找,所以我们是不是可以将上述的两个方法统一整合,由于类方法在元类中是实例方法,这里我们就可以使用resolveInstanceMethod来实现:

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(say666)) {
        NSLog(@"%@ 来了", NSStringFromSelector(sel));
        
        IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
        Method sayMethod  = class_getInstanceMethod(self, @selector(sayMaster));
        const char *type = method_getTypeEncoding(sayMethod);
        return class_addMethod(self, sel, imp, type);
    }else if (sel == @selector(sayNB)) {
        NSLog(@"%@ 来了", NSStringFromSelector(sel));
        IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        Method lgClassMethod  = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        const char *type = method_getTypeEncoding(lgClassMethod);
        return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
    }
    return NO;
}

这个方法的实现,与源码中针对类方法的处理逻辑是一致的,完美的阐述了为什么调用类方法动态方法决议,还要调用对象方法动态方法决议,原因是因为类方法在元类中的实例方法

当然,上面这种写法还是会有其他的问题,比如系统方法也会被更改,针对这一点,是可以优化的,即我们可以针对自定义类中方法统一方法名的前缀,根据前缀来判断是否是自定义方法,然后统一处理自定义方法,例如可以在崩溃前pop到首页,主要是用于app线上防崩溃的处理,提升用户的体验。

消息转发机制

快速转发流程

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

- (id)forwardingTargetForSelector:(SEL)aSelector;

这里我们需要返回一个实现了该方法的对象,使该对象能接收并且处理该消息,返回的对象用于接收消息,并且执行对应的方法。若是返回nil,则进入慢速转发

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

慢速转发

若是快速转发中还是没有找到,就会进入最后一次挽救机会,在CJLPerson中重写methodSignatureForSelector,如下所示:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(sayBye)) {
        
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];// 返回方法签名
    }
    return [super methodSignatureForSelector:aSelector];
}

// 消息转发逻辑
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = [anInvocation selector];
    // 检查备用对象是否响应方法
    if ([self.backupObject respondsToSelector:selector]) {
        [anInvocation invokeWithTarget:self.backupObject];
    } else {
        // 调用父类默认处理(抛出异常)
        [super forwardInvocation:anInvocation];
    }
}
  • 第一个方法中,为未实现的方法提供方法签名,避免运行时直接崩溃
    • v@:在这里表示方法返回值类型为void
  • 第二个方法中,将无法处理的消息转发给备用对象 _backupObject
  • 这里我们必须要注意,这两个方法必须顺序实现,仅调用第二个方法会产生报错

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

413 Payload Too Large 问题定位

源头 一般是服务器或者nginx 配置导致的 nginx http {client_max_body_size 50m; # 调整为所需大小&#xff08;如 50MB&#xff09;# 其他配置... }nginx 不配置&#xff0c;默认是1M 服务器 spring 不配置也是有默认值的好像也是1M 如果出现413 可以试着修改配置来避…

2025年渗透测试面试题总结-360[实习]安全工程师(题目+回答)

网络安全领域各种资源&#xff0c;学习文档&#xff0c;以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具&#xff0c;欢迎关注。 目录 1. 自我介绍 2. WAF及其绕过方式 3. IPS/IDS/HIDS 4. 云安全 5. 绕过安骑士/安全狗 6. Gopher扩展…

Ubuntu16.04升级gcc/g++版本方法

0 前言 gcc与g分别是GNU的c和c编译器&#xff0c;Ubuntu16.04默认的gcc和g的版本是5.4.0&#xff0c;在使用一些交叉编译工具链会提示找不到GLIBC_2.27&#xff0c;而GLIBC_2.27又需要gcc 6.2以上版本&#xff0c;因此本文介绍Ubuntu16.04升级gcc/g版本的方法。 1 Ubuntu16.0…

微信小程序van-dialog确认验证失败时阻止对话框的关闭

使用官方(Vant Weapp - 轻量、可靠的小程序 UI 组件库)的before-close&#xff1a; wxml&#xff1a; <van-dialog use-slot title"名称" show"{{ show }}" show-cancel-button bind:cancel"onClose" bind:confirm"getBackInfo"…

OceanBase 的系统变量、配置项和用户变量有何差异

在继续阅读本文之前&#xff0c;大家不妨先思考一下&#xff0c;数据库中“系统变量”、“用户变量”以及“配置项”这三者之间有何不同。如果感到有些模糊&#xff0c;那么本文将是您理清这些概念的好帮手。 很多用户在使用OceanBase数据库中的“配置项”和“系统变量”&#…

【Python】Jupyter指定具体路径

一、右键Jupyter Notebook 右击Jupyter Notebook点击属性 二、修改以下两个地方

RNope:结合 RoPE 和 NoPE 的长文本建模架构

TL;DR 2025 年 Cohere 提出的一种高效且强大的长上下文建模架构——RNope-SWA。通过系统分析注意力模式、位置编码机制与训练策略&#xff0c;该架构不仅在长上下文任务上取得了当前最优的表现&#xff0c;还在短上下文任务和训练/推理效率方面实现了良好平衡。 Paper name …

virtualbox虚拟机中的ubuntu 20.04.6安装新的linux内核5.4.293 | 并增加一个系统调用 | 证书问题如何解决

参考文章&#xff1a;linux添加系统调用【简单易懂】【含32位系统】【含64位系统】_64位 32位 系统调用-CSDN博客 安装新内核 1. 在火狐下载你需要的版本的linux内核压缩包 这里我因为在windows上面下载过&#xff0c;配置过共享文件夹&#xff0c;所以直接复制粘贴通过共享文…

unity UGUI虚线框shader

Shader "Custom/DottedLineShader" {Properties{_MainTex ("Texture", 2D) "white" {}_Color("Color",COLOR) (1,1,1,1)_LineLength("虚线长度",float) 0.08}SubShader{Tags //设置支持UGUI{ "Queue""Tran…

chirpstack v4版本 全流程部署[ubuntu+docker]

背景介绍 由于chirpstackv3 版本使用的是锐米提供的版本,从网络上寻找的资源大多数都是一样的v3版本,是经过别人编译好发布出来的,原本的chirpsatck项目是运行的linxu环境下的,因此我的想法是在linux服务器上部署chirpsatckv4,暂时使用linux上的chirpstack v4版本,目前编译成e…

数字信号处理-大实验1.1

MATLAB仿真实验目录 验证实验&#xff1a;常见离散信号产生和实现验证实验&#xff1a;离散系统的时域分析应用实验&#xff1a;语音信号的基音周期&#xff08;频率&#xff09;测定 目录 一、常见离散信号产生和实现 1.1 实验目的 1.2 实验要求与内容 1.3 实验…

对抗性机器学习:AI模型安全防护新挑战

随着采用对抗性机器学习&#xff08;Adversarial Machine Learning, AML&#xff09;的AI系统融入关键基础设施、医疗健康和自动驾驶技术领域&#xff0c;一场无声的攻防战正在上演——防御方不断强化模型&#xff0c;而攻击者则持续挖掘漏洞。2025年&#xff0c;对抗性机器学习…

[[春秋云境] Privilege仿真场景

文章目录 靶标介绍&#xff1a;知识点卷影拷贝(VSS) 外网任意文件读取Jenkins管理员后台rdp远程登录Gitlab apiToken 内网搭建代理 Oracle RCESeRestorePrivilege提权mimikatzspn卷影拷贝提取SAM 参考文章 靶标介绍&#xff1a; 在这个靶场中&#xff0c;您将扮演一名资深黑客…

Redis学习打卡-Day3-分布式ID生成策略、分布式锁

分布式 ID 当单机 MySQL 已经无法支撑系统的数据量时&#xff0c;就需要进行分库分表&#xff08;推荐 Sharding-JDBC&#xff09;。在分库之后&#xff0c; 数据遍布在不同服务器上的数据库&#xff0c;数据库的自增主键已经没办法满足生成的主键全局唯一了。这个时候就需要生…

数据库第二次作业--SQL的单表查询与多表查询

单表查询 查询专业信息表中的专业名称和专业类型 SELECT Mname, Mtype FROM MajorP;查询一个学校有多少个专业 SELECT COUNT(Mno) AS 专业数量 FROM MajorP;查询学校的男女学生各有多少位 SELECT Ssex&#xff0c; COUNT(*) AS 人数 FROM StudentP GROUP BY Ssex查询每个专业…

在Cursor中启用WebStorm/IntelliJ风格快捷键

在Cursor中启用WebStorm/IntelliJ风格快捷键 方法一&#xff1a;使用预置快捷键方案 打开快捷键设置 Windows/Linux: Ctrl K → Ctrl SmacOS: ⌘ K → ⌘ S 搜索预设方案 在搜索框中输入keyboard shortcuts&#xff0c;选择Preferences: Open Keyboard Shortcuts (JSON) …

vue3:十三、分类管理-表格--编辑、新增、详情、刷新

一、效果 实现封装表格的新增、编辑、详情查看,表格刷新功能 实现表格组件中表单的封装 1、新增 如下图,新增页面显示空白的下拉,文本框,文本域,并实现提交功能 2、编辑 如下图,点击行数据,可将行数据展示到编辑弹窗,并实现提交功能 3、详情 如下图,点击行数据,…

c#基础01(.Net介绍)

文章目录 .Net平台介绍.Net平台简介跨平台开源.Net Core.Net Framework开发工具安装选项 创建项目 .Net平台介绍 .Net平台简介 .NET是一种用于构建多种应用的免费开源开放平台&#xff0c;例如&#xff1a; Web 应用、Web API 和微服务 云中的无服务器函数 云原生应用 移动…

Logrotate:配置日志轮转、高效管理Linux日志文件

Logrotate 是 Linux 系统中用于自动化管理日志文件的工具&#xff0c;能够定期轮转、压缩、删除日志文件&#xff0c;确保系统日志不会无限制增长&#xff0c;占用过多磁盘空间。 它通常由 Cron 作业定期执行&#xff0c;也可以手动触发。 1. &#x1f527; 核心功能 日志轮转…

贵州某建筑物挡墙自动化监测

1. 项目简介 某建筑物位于贵州省某县城区内&#xff0c;靠近县城主干道&#xff0c;周边配套学校、医院、商贸城。建筑物临近凤凰湖、芙蓉江等水系&#xff0c;主打“湖景生态宜居”。改建筑物总占地面积&#xff1a;约5.3万平方米&#xff1b;总建筑面积&#xff1a;约15万平…