• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

[iOS]-消息传递和消息转发机制

武飞扬头像
&Carry
帮助1

参考的博客:

Objective-C 消息发送与转发机制原理
[iOS开发]消息传递和消息转发机制
iOS八股文(六)objc_msgSend之方法查找源码解析
iOS八股文(七)objc_msgSend之动态解析和消息转发

消息传递机制的学习

之前学习过这个机制的一些内容:对象、消息、运行期

在对象上调用方法,术语就叫做传递消息,消息有名称和选择器(方法),可以接受参数,还可能有返回值。

在很多语言,比如 C ,调用一个方法其实就是跳到内存中的某一点并开始执行一段代码。没有任何动态的特性,因为这在编译时就决定好了。

而在 Objective-C 中,[object foo] 语法并不会立即执行 foo这个方法的代码。它是在运行时给object 发送一条叫 foo 的消息。这个消息,也许会由 object 来处理,也许会被转发给另一个对象,或者不予理睬假装没收到这个消息。多条不同的消息也可以对应同一个方法实现。这些都是在程序运行的时候决定的。

消息传递机制的学习其实就是理解OC是怎么样进行调用方法的。

id returnValue = [someObject messageName:parameter];

这样一条代码编译器会将其处理成

 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就会被看做一个,并存储到表中。在项目加载时,会将所有方法都加载到这个表中,而动态生成的方法也会被加载到表中。

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

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

小的总结:

SEL : 类成员方法的指针,但不同于C语言中的函数指针,函数指针直接保存了方法的地址,但SEL只是方法编号。**IMP:**一个函数指针,保存了方法的地址

IMP和SEL关系

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

1.通过方法获得方法的编号:SEL methodId=@selector(methodName);或者SEL methodId = NSSelectorFromString(methodName);

2.通过方法编号执行该编号的方法: [self performSelector:methodId withObject:nil];

3.通过方法编号获取该编号的方法名 NSString*methodName = NSStringFromSelector(methodId);

4.通过方法编号获得IMP IMP methodPoint = [self methodForSelector:methodId];

5.执行IMP void (*func)(id, SEL, id) = (void *)imp; func(self, methodName,param);

**注意分析:**如果方法没有传入参数时:void (*func)(id, SEL) = (void *)imp; func(self, methodName);

如果方法传入一个参数时:void (*func)(id, SEL,id) = (void *)imp; func(self, methodName,param);

如果方法传入俩个参数时:void (*func)(id, SEL,id,id) = (void *)imp; func(self, methodName,param1,param2);

objc_msgSend()的执行流程

  1. 消息发送阶段:负责从类及父类的缓存列表及方法列表查找方法
  2. 动态解析阶段:如果消息发送阶段没有找到方法,则会进入动态解析阶段:负责动态地添加方法实现
  3. 消息转发阶段:如果也没有实现动态解析方法,则会进行消息转发阶段,将消息转发给可以处理消息的接受者来处理

消息发送和转发流程可以概括为:消息发送(Messaging)是 Runtime 通过 selector 快速查找 IMP 的过程,有了函数指针就可以执行对应的方法实现;消息转发(Message Forwarding)是在查找 IMP 失败后执行一系列转发流程的慢速通道,如果不作转发处理,则会打日志和抛出异常。

在学习消息发送和转发流程的前提是: 对OC Runtime已经有一定的了解,消息,Class的结构,selector、IMP、元类等等

objc_msgSend

此函数是消息发送必经之路,但只要一提 objc_msgSend,都会说它的伪代码如下或类似的逻辑,反正就是获取 IMP 并调用:

id objc_msgSend(id self, SEL _cmd, ...) {
  Class class = object_getClass(self);
  IMP imp = class_getMethodImplementation(class, _cmd);
  return imp ? imp(self, _cmd, ...) : 0;
}

objc_msgSend参数
objc_msgSend在调用的时候有两个默认参数,第一个参数是消息的接收者,第二个参数是方法名。

这一点可以通过oc代码重写成cpp代码来证明。

int object_c_source_m() {
    OSTestObject1 *obj1 = [[OSTestObject1 alloc] init];
    [obj1 print];
    return 0;
}

重写后:

int object_c_source_m() {
    OSTestObject1 *obj1 = ((OSTestObject1 *(*)(id, SEL))(void *)objc_msgSend)((id)((OSTestObject1 *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("OSTestObject1"), sel_registerName("alloc")), sel_registerName("init"));

    ((void (*)(id, SEL))(void *)objc_msgSend)((id)obj1, sel_registerName("print"));

    return 0;
}

可以看到print的调用转化成了objc_msgSend调用并传入 objc1print。如果方法本身有参数,会把本身的参数拼接到这两个参数后面。

源码解析
用伪代码的原因就是objc_msgSend 是用汇编语言写的,针对不同架构有不同的实现。苹果为什么objc_msgSend这部分代码要使用汇编来编写呢?答案很简单–效率。汇编的效率是比c/c 更快的,因为汇编大多是直接对寄存器的读写,相比较对内存的操作更底层,效率也更高。另外苹果在所有的汇编方法命值钱都会用下划线开头,目的是为了防止符号冲突。

下方就是arm64结构下的源码:

	ENTRY _objc_msgSend//进入消息转发
	UNWIND _objc_msgSend, NoFrame
	//p0寄存器,消息接收者
	cmp	p0, #0			// nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)//b是跳转,le是小于等于,也就是p0小于等于0时,跳转到LNilOrTagged
#else
	b.eq	LReturnZero
#endif
	ldr	p13, [x0]		// p13 = isa
	GetClassFromIsa_p16 p13, 1, x0	// p16 = class
LGetIsaDone:
	// calls imp or objc_msgSend_uncached
	//缓存查找
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged://如果接收者为nil,跳转至此
	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//结束
学新通
  1. 首先从cmp p0,#0开始,这里p0是寄存器,存放的是消息接受者。b.le LNilOrTagged,b是跳转到的意思。le是如果p0小于等于0,总体意思是若p0小于等于0,则跳转到LNilOrTagged,执行b.eq LReturnZero直接退出这个函数
  2. 如果消息接受者不为nil,汇编继续跑,到CacheLookup NORMALCacheLookup 这个宏是在类的缓存中查找 selector 对应的 IMP(放到 p10)并执行。如果缓存没中,那就得到 Class 的方法表中查找了来看一下具体的实现

其实只需要看注释就能知道大概流程。
这部分其实是objc_msgSend 开始到找类对像cache方法结束的流程。
首先判断receiver是否存在,以及是否是taggedPointer类型的指针,如果不是taggedPointer类型,我们就取出对象的isa指针(x13寄存器中),通过isa指针找到类对象(x16寄存器),然后通过CacheLookup,在类对象的cache中查找是否有方法缓存,如果有就调用,如果没有走objc_msg_uncached分支。

在cache中快速查找

下面就是CacheLookup的源码:

//objc_msgSend开始找到类对象cache方法结束的流程中的 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
	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

	ldr	x17, [x10, x9, LSL #3]		// x17 == sel_offs | (imp_offs << 32)
	cmp	x12, w17, uxtw

.if \Mode == GETIMP
	b.ne	\MissLabelConstant		// cache miss
	sub	x0, x16, x17, LSR #32		// imp = isa - imp_offs
	SignAsImp x0
	ret
.else
	b.ne	5f				// cache miss
	sub	x17, x16, x17, LSR #32		// 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
学新通

大致看看注释,不用深究汇编代码逻辑,大概应该是通过类对象内存平移找到cache,然后再获取buckets,然后再查找方法。 注释解释:如果没有找到返回NULL,查找的时候x0存放方法接收者,x1存放方法名,x16存放isa指针。

如果没有找到,直接走_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//此处调用了loolUpImpOrForward方法

	// IMP in x0
	mov	x17, x0

	RESTORE_REGS MSGSEND

.endmacro
学新通

搜索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)) {
#if CONFIG_USE_PREOPT_CACHES
            //cache缓存中查找
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
            // curClass method list.
            //方法列表中查询
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }

            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                //找不到实现,方法解析器也没有帮助。
				//使用转发。
                imp = forward_imp;//由这一步进入消息转发
                break;
            }
        }

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

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

    // 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;
}
学新通

其中关键代码如下:

    for (unsigned attempts = unreasonableClassCount();;) {
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
            //cache缓存中查找
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
            // curClass method list.
            //方法列表中查询
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }

            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                //找不到实现,方法解析器也没有帮助。
				//使用转发。
                imp = forward_imp;//由这一步进入消息转发
                break;
            }
        }
学新通

这段代码是非常厉害的,支持缓存的话要先从缓存当中查找,否则从方法列表中查找。这里有一个疑问,之前不是站在cache中查找过了么?为什么还要再查找一次,首先是因为多线程同步问题,还有就是注意在这个循环里面curClass要继续从superClass中去找,这样逻辑统一,也需要在superClasscache中查找。

如何在方法列表中寻找的呢,就是在getMethodNoSuper_nolock方法里,下面是其实现:

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;
}
学新通

上方的method_t *m = search_method_list_inline(*mlists, sel);是查找方法列表的关键,我们现在来看一下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();
    
    //fastpath大概率走这里
    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代表的是大概率走这边,然后再看findMethodInSortedMethodList的这个方法名,关键字InSorted这里面可以大胆从字面翻译一下从排过序的方法类表中查找方法

findMethodInSortedMethodList的话有两个对应的方法实现,是因为C/C 可以通过参数来区分方法,这两个方法一个是两个参数,一个是三个参数,两个参数的这个方法最终也是调用三个参数的方法的。所以直接看三个参数的实现就可以:

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;
    
    //下方的count>>1=1相当于count = count / 2,所以这个是二分法查找
    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;
}
学新通

注意看for循环中的count的变化。count>>=1 相当与 count = count / 2。再结合之前的InSorted,可以看到这是明显的二分法查找。

在找到方法之后需要缓存找到的方法,缓存找到的方法的流程如下:
接着我们先回到loolUpImpOrForward的关键实现部分中:

    for (unsigned attempts = unreasonableClassCount();;) {
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
            //cache缓存中查找
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
            // curClass method list.
            //方法列表中查询
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            //如果找到了方法,就进行缓存操作
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }

            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                //找不到实现,方法解析器也没有帮助。
				//使用转发。(开始消息转发流程)
                imp = forward_imp;//由这一步进入消息转发
                break;
            }
        }
学新通

其中的:

	//如果找到了方法,就进行缓存操作
    if (meth) {
        imp = meth->imp(false);
        goto done;
    }

将找到的方法保存然后去了done

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

我们看到缓存找到方法的关键操作是:log_and_fill_cache方法,下面是其实现:

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
	//用cache的insert将其写入缓存
    cls->cache.insert(sel, imp, receiver);
}

我们发现其是调用cache的insert方法写入到缓存中的,要注意的是cls传的的是msgSend receiver 的isa指向的类对象/元类对象。也就是说,即使方法是从父类的类对象/元类对象中找到的,这个方法缓存也是存再自己的cache中的。

总结一下缓存查找和方法列表查找

用一张图总结:
学新通
上面我们了解了objc_msgSend过程中的消息查找的流程,其中有个查找方法的函数名叫lookUpImpOrForward,字面翻译查找方法或者转发。那么本文就来记录下objc_msgSend的其他两个过程。

resolveMethod动态解析(动态决意)

lookUpImpOrForward方法中有一段代码如下:

    // No implementation found. Try method resolver once.
	//没有发现方法实现,尝试一次动态解析
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

其中注释:没有找到方法实现,尝试一次方法解析。

继续来看里面resolveMethod_locked的实现:

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);
}
学新通

这里对cls进行了是否是元类的判断,如果是元类,说明是类方法的调用,则调用resolveClassMethod,如果是类,说明是对象方法的调用,则调用resolveInstanceMethod

resolveInstanceMethod方法代码如下:

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    //下方的resolveInstanceMethod:方法需要程序员自行编写实现
    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);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    //  resolveInstanceMethod adds to self a.k.a. cls
    //缓存结果(好或坏),以便解析器下次不会触发。
    // resolveInstanceMethod添加到self a.k.a.cls
    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));
        }
    }
}
学新通

resolveClassMethod方法源码如下:

static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());

    if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
        // Resolver not implemented.
        //未实现解析程序。
        return;
    }

    Class nonmeta;
    {
        mutex_locker_t lock(runtimeLock);
        //下面这个方法是用来获取元类对应的类对象的,然后再对类对象发送消息(相当于调用类方法)
        nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
        //  initialize path should have realized nonmeta already
        // 初始化路径应已实现非meta
        if (!nonmeta->isRealized()) {
            _objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
                        nonmeta->nameForLogging(), nonmeta);
        }
    }
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    //下方的resolveClassMethod:方法需要程序员自行编写实现
    bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    //  resolveClassMethod adds to self->ISA() a.k.a. cls
    //缓存结果(好或坏),以便解析程序下次不会触发。
    // resolveClassMethod添加到self->ISA()也称cls
    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 resolveClassMethod:%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));
        }
    }
}
学新通

其中resolveClassMethod resolveInstanceMethod是通过msgSend去调用类方法resolveClassMethod:resolveInstanceMethod:的。其中resolveClassMethod中的cls本来是元类,通过getMaybeUnrealizedNonMetaClass来获取元类对应的类对象,然后再对类对象发送消息(相当于调用类方法)。

getMaybeUnrealizedNonMetaClass方法的注释如下方所示:
学新通
可以看到其主要功能为返回该类或元类的普通类。

resolveClassMethod:resolveClassMethod:的实现就交给了开发者,开发者可以再这里动态添加方法实现。

// 第一根稻草,使用动态解析,动态添加方法
  (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(methodOne)) {
        Method methodNormal = class_getInstanceMethod(self, @selector(methodNormal));
        class_addMethod(self,
                        @selector(methodOne),
                        method_getImplementation(methodNormal),
                        method_getTypeEncoding(methodNormal));
        return YES
    }
    return [super resolveInstanceMethod:sel];
}

这样methodOne的调用就可以使用methodNormal的实现了。

注:resolveClassMethod:resolveInstanceMethod:默认返回值是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文件中声明一个实例方法methodOne,但在.m文件中并没有实现这个方法

我们在外部调用这个方法就会导致程序崩溃

这个很容易理解:

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

但是如果我们对动态解析函数resolveInstanceMethod:进行一个编写,下方代码与上方动态方法解析例子相同:

  (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(methodOne)) {
        Method methodNormal = class_getInstanceMethod(self, @selector(methodNormal));
        class_addMethod(self,
                        @selector(methodOne),
                        method_getImplementation(methodNormal),
                        method_getTypeEncoding(methodNormal));
        return YES
    }
    return [super resolveInstanceMethod:sel];
}

这样methodOne的调用就可以使用methodNormal的实现了,所以当第一步中示例方法methodOne查找失败时,我们就会开始动态方法解析,在动态方法解析的resolveInstanceMethod:函数中我们为methodOne方法动态添加了methodNormal方法的实现,那么走到这一步时,就会返回YES,并成功执行methodOne方法。

消息转发

如果动态解析(动态决意)阶段还是没有对应的方法,那么就会来到消息转发阶段。消息转发阶段分为两部分,替换消息接收者阶段,也有叫快速转发,和完全消息转发阶段。

lookUpImpOrForward方法的关键代码中:

    for (unsigned attempts = unreasonableClassCount();;) {
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
            //cache缓存中查找
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
            // curClass method list.
            //方法列表中查询
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }

            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                //找不到实现,方法解析器也没有帮助。
				//使用转发。
                imp = forward_imp;//由这一步进入消息转发
                break;
            }
        }
学新通

我们可以看到imp = forward_imp;//由这一步进入消息转发由这一步进入了消息转发,这里的原理是,如果已经进行过一次动态方法解析了,但是还是没有找到对应方法的实现,那么lookUpImpOrForward方法就需要将_objc_msgForward_impcache作为结果并写入缓存,然后将_objc_msgForward_impcache标记结果返回到lookUpImpOrNil方法后会经过判断继续向class_getMethodImplementation方法返回nil,然后接受到nil的class_getMethodImplementation方法就会返回_objc_msgForward正式开始消息转发流程,整个过程的代码如下:

__attribute__((flatten))
IMP class_getMethodImplementation(Class cls, SEL sel)
{
    IMP imp;

    if (!cls  ||  !sel) return nil;

    lockdebug_assert_no_locks_locked_except({ &loadMethodLock });

    imp = lookUpImpOrNilTryCache(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);

    // Translate forwarding function to C-callable external version
    //如果imp为nil就开始返回_objc_msgForward正式进入消息转发
    if (!imp) {
        return _objc_msgForward;
    }

    return imp;
}
学新通

lookUpImpOrNilTryCache 函数获取不到 IMP 时就返回 _objc_msgForwardlookUpImpOrNilTryCachelookUpImpOrForward 的功能很相似,只是将 lookUpImpOrForward 实现中的 _objc_msgForward_impcache 替换成了 nil:

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

我们发现lookUpImpOrNilTryCache方法调用了_lookUpImpTryCache方法,接下来我们就看一看_lookUpImpTryCache的实现:

ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertUnlocked();

    if (slowpath(!cls->isInitialized())) {
        // see comment in lookUpImpOrForward
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

    IMP imp = cache_getImp(cls, sel);
    //如果缓存的位置不是NULL,就跳去执行done
    if (imp != NULL) goto done;
#if CONFIG_USE_PREOPT_CACHES
    if (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) {
        imp = cache_getImp(cls->cache.preoptFallbackClass(), sel);
    }
#endif
    if (slowpath(imp == NULL)) {
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

done:
	//缓存位置如果是_objc_msgForward_impcache的话,就返回nil
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        return nil;
    }
    return imp;
}
学新通

到这里我们就理解了刚开始消息转发部分的流程了

由于最后返回的是_objc_msgForward,那_objc_msgForward到底是什么呢,其实它是一个入口,_objc_msgForward* 系列本质都是函数指针,都用汇编语言实现,都可以与 IMP 类型的值作比较,除_objc_msgForward外,还有 _objc_msgForward_stret,看后缀我们就知道_objc_msgForward_stret的返回值肯定是一个结构体,接下来我们来看一下_objc_msgForward的汇编实现:

	STATIC_ENTRY __objc_msgForward_impcache

	// No stret specialization.
	b	__objc_msgForward

	END_ENTRY __objc_msgForward_impcache

	
	ENTRY __objc_msgForward

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

发现里面调用了__objc_forward_handler,但是很可惜__objc_forward_handler并没有开源,但是其中其实是调用了objc_setForwardHandler来为_objc_forward_handler_objc_forward_handler_stret赋值,而赋值的参数则为汇编的__CF_forwarding_prep_0___forwarding_prep_1___,而__CF_forwarding_prep_0___forwarding_prep_1___ 函数都调用了 ___forwarding___,而___forwarding___又调用了 ___invoking___ 函数,消息转发的逻辑几乎都写在 ___forwarding___ 函数中了,所以我们看一下找到的 **___forwarding___**函数的伪代码进行学习(这小段内容详见:Objective-C 消息发送与转发机制原理):

int __forwarding__(void *frameStackPointer, int isStret) {
  id receiver = *(id *)frameStackPointer;
  SEL sel = *(SEL *)(frameStackPointer   8);
  const char *selName = sel_getName(sel);
  Class receiverClass = object_getClass(receiver);

  // 调用 forwardingTargetForSelector:
  if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
    id forwardingTarget = [receiver forwardingTargetForSelector:sel];
    if (forwardingTarget && forwarding != receiver) {
    	if (isStret == 1) {
    		int ret;
    		objc_msgSend_stret(&ret,forwardingTarget, sel, ...);
    		return ret;
    	}
      return objc_msgSend(forwardingTarget, sel, ...);
    }
  }

  // 僵尸对象
  const char *className = class_getName(receiverClass);
  const char *zombiePrefix = "_NSZombie_";
  size_t prefixLen = strlen(zombiePrefix); // 0xa
  if (strncmp(className, zombiePrefix, prefixLen) == 0) {
    CFLog(kCFLogLevelError,
          @"*** -[%s %s]: message sent to deallocated instance %p",
          className   prefixLen,
          selName,
          receiver);
    <breakpoint-interrupt>
  }

  // 调用 methodSignatureForSelector 获取方法签名后再调用 forwardInvocation
  if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
    NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
    if (methodSignature) {
      BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct;
      if (signatureIsStret != isStret) {
        CFLog(kCFLogLevelWarning ,
              @"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'.  Signature thinks it does%s return a struct, and compiler thinks it does%s.",
              selName,
              signatureIsStret ? "" : not,
              isStret ? "" : not);
      }
      if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
        NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];

        [receiver forwardInvocation:invocation];

        void *returnValue = NULL;
        [invocation getReturnValue:&value];
        return returnValue;
      } else {
        CFLog(kCFLogLevelWarning ,
              @"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message",
              receiver,
              className);
        return 0;
      }
    }
  }

  SEL *registeredSel = sel_getUid(selName);

  // selector 是否已经在 Runtime 注册过
  if (sel != registeredSel) {
    CFLog(kCFLogLevelWarning ,
          @"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort",
          sel,
          selName,
          registeredSel);
  } // doesNotRecognizeSelector
  else if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
    [receiver doesNotRecognizeSelector:sel];
  } 
  else {
    CFLog(kCFLogLevelWarning ,
          @"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort",
          receiver,
          className);
  }

  // The point of no return.
  kill(getpid(), 9);
}
学新通

这么一大坨代码就是整个消息转发路径的逻辑,概括如下:

  1. 先调用 forwardingTargetForSelector 方法获取新的 target 作为 receiver 重新执行 selector,如果返回的内容不合法(为 nil 或者跟旧 receiver 一样),那就进入第二步调用 methodSignatureForSelector
  2. 调用 methodSignatureForSelector 获取方法签名后,判断返回类型信息是否正确,再调用 forwardInvocation 执行 NSInvocation 对象,并将结果返回。如果对象没实现 methodSignatureForSelector 方法,进入第三步调用 doesNotRecognizeSelector
  3. 调用 doesNotRecognizeSelector 方法,如果实现了doesNotRecognizeSelector 方法,就先执行该方法,再打印日志抛出异常,如果没有实现该方法,那么就直接去打印日志抛出异常。

自此,我们全部的消息转发的过程就全部讲完了,下面我们详细讲解一下消息转发主要用到的流程。

消息接收者替换

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

我们可以实现forwardingTargetForSelector方法,并返回替换的对象:

//第二根稻草,使用快速消息转发,找其他对象来实现方法
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(methodTwo)) {
        //也就是本类中的其他对象,此处选取的对象是forwardObject
        return self.forwardObject;
    }
    return nil;
}

候就会去_forwardObject里面去找methodTwo了。_forwardObject的类实现如下:

@implementation OSMsgSendForwardObject
//从消息转发而来
- (void)methodTwo {
    NSLog(@"%s__ %@",__FUNCTION__,[self class]);
}
@end

完全消息转发

如果forwardingTargetForSelector方法返回的是nil,那么我们还有最后一根稻草可以抓住完全消息转发。相比于快速转发,不仅可以替换消息接受者,还能替换方法:

//第三根稻草,使用完全消息转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(methodThree)) {
        NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"v@:"];
        return sig;
    }
    return nil;
}

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

这里有两个类,NSMethodSignatureNSInvocation。其中NSMethodSignature是方法签名,可以通过方法的字符来实例化。NSInvocation是方法调用实体,其中有targetselector参数构成。

这第三个救命稻草的逻辑就是: 先判断methodSignatureForSelector有没有被实现且返回值不为nil,如果已经实现且返回值不为nil,那么就进行下一步判断forwardInvocation有没有被实现,如果forwardInvocation已经实现那么就使用方法签名生成NSInvocation对象并调用forwardInvocation方法,最后返回forwardInvocation执行的结果,如果forwardInvocation方法没有被实现,那就直接调用doesNotRecognizeSelector 方法打印日志抛出异常。如果methodSignatureForSelector没有被实现或返回值为nil,那么就直接调用doesNotRecognizeSelector 方法打印日志抛出异常。

附件(methodSignatureForSelector方法中方法签名用到的字符编码):

为了协助运行时系统,编译器用字符串为每个方法的返回值和参数类型和方法选择器编码。使用的编码方案在其他情况下也很有用,所以它是public 的,可用于@encode() 编译器指令。当给定一个类型参数,返回一个编码类型字符串。类型可以是一个基本类型如int,指针,结构或联合标记,或任何类型的类名,事实上,都可以作为C sizeof() 运算符的参数。这个机制也是为了提高Runtime的效率,编码表如下:

学新通
由于调用一个方法我们需要知道方法调用者,方法名,方法参数。而methodSignatureForSelector方法中我们创建方法签名的原因就是缺一个返回值和参数类型,所以方法签名中都是由上表中的字符构成的。

总结与思考

这张图很好地总结了消息转发三次拯救的过程:
学新通
我们首先明白OC方法调用的本质就是消息发送,消息发送是SEL-IMP的查找过程

我们先进行正常的消息发送,等到一个函数找不到时,OC运行时提供了三种方式去补救(三根救命稻草):

  1. 调用resolveInstanceMethod或者resolveClassMethod方法,尝试给一个没有实现的方法添加实现(动态解析)
  2. 调用forwardingTargetForSelector尝试让本类中其他的对象去执行这个函数(快速地消息转发)
  3. 如果没有进行快速转发,可以用methodSignatureForSeletor 和 frowardInvocation 来进行完全消息转发,不仅可以替换消息接受者,还能替换方法。(完整的消息转发)

最后附上一张大佬的完整版消息发送与转发流程图:
大佬的详细解释详见:Objective-C 消息发送与转发机制原理
学新通

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgbkjji
系列文章
更多 icon
同类精品
更多 icon
继续加载