资讯详情

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

目录:

  • 参考博客:
  • 学习新闻传递机制
    • 选择子SEL
      • 小的总结:
    • objc_msgSend()执行过程
    • objc_msgSend
      • 在cache中快速查找
      • 在方法类表中找到
      • 总结缓存搜索和方法列表
    • resolveMethod动态分析(动态决策)
    • 消息转发
      • 替换新闻接收者
      • 全新闻转发
      • 总结与思考

参考博客:

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语言函数调用一样使用函数指针。

小的总结:

但与C语言中的函数指针不同,该指针直接保存了该方法的地址,但SEL只是方法编号。**IMP:**保存方法地址的函数指针

每一个继承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];

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. 消息转发阶段:如果也没有实现动态解析方法,则会进行消息转发阶段,将消息转发给可以处理消息的接受者来处理

对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在调用的时候有两个默认参数,第一个参数是消息的接收者,第二个参数是方法名。

这一点可以通过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, se

标签: 5w12v直插二极管sick小型光电传感器w12g

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台