资讯详情

【iOS】消息传递&转发

文章目录

  • 前言
  • 信息传递机制基础知识
    • SEL选择子
    • IMP
    • IMP与SEL区别与联系
  • 消息发送
    • objc_msgSend
    • 快速查找
      • 汇编过程
      • 总结信息快速发送imp(汇编):
    • 慢速查找
      • 缓存的方法是什么?
      • 慢慢搜索过程
        • 查找方式
          • 分类优先
        • 跳出循环后
      • 总结信息发送缓慢搜索imp(c/c ):
  • 消息的转发
    • 动态决议
      • 动态决议过程
      • 动态分析测试
    • 消息转发
      • 快速转发消息
        • 快速转发消息测试
      • 慢速转发消息
  • 总结
    • 流程图
  • 一些问题
    • runtime是如何通过selector找到对应的IMP地址的?
    • 假如子类调用父类方法,缓存在哪类?
    • 两个动态决议的原因

前言

Objective-C它是一种非常动态的语言,以至于哪种方法被推迟到操作,而不是编译。与之相反,C语言采用静态绑定,即编译期可以决定程序运行时应调用的函数,因此在C语言中, 如果未实现函数,则无法编译。而Objective-C它是一种相对动态的语言,在操作过程中也可以动态添加到类中,因此编译不能确定该方法是否实现相应的实现,编译器在编译过程中不能报告错误。

信息传递机制基础知识

在对象上调用方法Objective-C很常见Objective-C对于术语, 叫给对象发一条信息"。消息有 (selector)说。新闻可以接受参数,也可以有返回值。

  • 在Objective-C在中间,如果信息传递给一个对象,它将使用动态绑定机制来确定需要调用的方法。在底层,所有的方法都是普通的C语言函数。然而,在对象收到信息后,调用该方法的完整运行期甚至可以在程序运行中更改,这使得Objective-C成为一种真正的动态语言。

id returnValue = [someObject messageName:parameter]; 
  • 本例中,someObject称为方法调用器,又称方法调用器,又称方法调用器,又称方法调用器,又称方法调用器,又称方法调用器,又称方法调用器,又称方法调用器,又称方法调用器,方法调用器,方法调用器,方法调用器,方法调用器,方法调用器,方法调用器,方法调用器,方法调用器,方法调用器,方法调用器,方法调用器,方法调用器,方法调用器,方法调用器,方法调用器,方法调用器,方法调用器,方法调用器,方法调用器,方法调用器,方法调用器,方法调用器,方法调用器,方法调用器,方法调用器,方法调用器(receiver) 。messageName:又称方法名(selector) 。选择子与参数结合称为”(message) 。在运行过程中,编译器将上述格式的方法转换为一个标准的C语言函数,这是著名的objc_ msgSend(),函数是新闻objc其原型如下:
void objc_msgSend(id self, SEL cmd, ....) 
  • 这是一个参数个数可变的函数。可接收两个或两个以上的参数,。后续参数是新闻中的参数,顺序保持不变。接受者是调用方法的对象或类别(本质上,类别也是对象,称为类别对象)。选择子是指方法的名称。选择子和方法通常交替使用。事实上,这意味着一个。操作时,上面Objc将方法调用翻译成C语言的函数调用, 如下:
id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter) 

SEL选择子

Objective-C编译时,根据方法名称(包括参数序列)生成一个用途 区分这种方法的唯一方法ID,这个ID就是SEL类型。只要方法的名称(包括参数序列)相同,我们需要注意它们ID都是一样的。也就是说,无论是超类还是子类,无论是否有超类与子类的关系,只要名字相同ID就是一样的。

  • 这样的机制大大的增加了我们的程序的灵活性,我们可以通过给一个方法传递SEL参数,使该方法动态执行某种方法;我们还可以通过配置文件指定要执行的方法,程序读取配置文件后将该方法的字符串翻译成SEL然后将此消息发送给相应的对象。

从效率的角度来看,执行不是通过方法名称,而是方法ID也就是说,整数搜索方法,因为整数搜索和匹配比字符串快得多,所以可以在一定程度上提高执行效率

  1. @ selector指示符号,SEL act = @selector(setAge:);
  2. 也可以是函数:NSSelectorFromString(NSString *)
  3. (NSString *)NSStringFromSeletor (SEL)

IMP

  • 它是OC方法实现代码块的地址,通过他可以直接访问任意一个方法。免去发送消息的代码,IMP声明:
typedef id (&IMP)(id,SEL,...);

IMP 是一个函数指针,这个被只想的函数包含一个接收消息的对象id(self 指针),调用方法的选标SEL(方法名),以及不定个数的方法参数,并返回一个id.

IMP与SEL的区别与联系

  • SEL:类方法的指针,相当于一种编号,区别与IMP
  • IMP:函数指针,保存了方法的地址

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

消息发送

C语言是静态类型,OC是动态类型。在编译的时候不知道具体类型。运行的时候才会检查数据类型,根据函数名找到实现。实现语言动态的就是runtime的api。它的知识点都围绕

  1. 动态配置。动态的修改类的信息。添加属性、方法,甚至成员变量的值等数据结构。
  2. 消息传递。包括发送和转发。在编译的时候,方法调用就会转换成objc_msgsend函数进行消息发送,这是

objc_msgSend

以下demo为例,定义一个Person类,实现study方法,并调用该方法。

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject
- (void)study;

@end

NS_ASSUME_NONNULL_END

#import "Person.h"

@implementation Person
- (void)study { 
        
    NSLog(@"%s",__func__);
}
@end

#import <Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) { 
        
    @autoreleasepool { 
        
        Person* person = [[Person alloc] init];
        [person study];
        
    }
    return 0;
}

打开终端,在项目目录下通过clang指令,讲main.m文件编译成后缀.cpp的c++类型文件

clang -rewrite-objc main.m

打开找到main函数,编译后的方法调用都是通过objc_msgSend发送的,证明方法的本质就是消息发送。

#pragma clang assume_nonnull end
int main(int argc, const char * argv[]) { 
        
    /* @autoreleasepool */ { 
         __AtAutoreleasePool __autoreleasepool; 
        Person* person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("study"));

    }
    return 0;
}
  • objc_msgSend带有默认的2个隐式参数:消息的接收者id类型,消息的方法名SEL类型。
  • 开始的alloc方法是给类对象发消息objc_getClass("Person")
  • 如果消息接收者是实例对象,实例对象会通过isa找到类对象,从中找到实例方法。类方法同理,在元类对象中找到。

来到cpp文件的顶部,可以看到objc_msgSend方法不止一种,这是个家族

请添加图片描述

以下方法依次代表发给

__OBJC_RW_DLLIMPORT void objc_msgSend(void);
__OBJC_RW_DLLIMPORT void objc_msgSendSuper(void);
__OBJC_RW_DLLIMPORT void objc_msgSend_stret(void);
__OBJC_RW_DLLIMPORT void objc_msgSendSuper_stret(void);
__OBJC_RW_DLLIMPORT void objc_msgSend_fpret(void);

快速查找

objc_msgSend在不同架构下都有实现:以arm64为例,代码实现是汇编。

  • 为什么选用汇编来实现?速度更快,直接使用参数,免去大量参数的拷贝的开销。
  • 在函数和全局变量前面会加下划线“_”,防止符号冲突。

汇编过程

  • 首先从cmp p0,#0开始,这里p0是寄存器,存放的是消息接受者。当进入消息发送入口时,先判断消息接收者是否存在,不存在则重新执行objc_msgSend
  • b.le LNilOrTagged,b是跳转到的意思。le是如果p0小于等于0,总体意思是若p0小于等于0,则跳转到LNilOrTagged,执行b.eq LReturnZero直接退出这个函数
	//进入objc_msgSend流程
	ENTRY _objc_msgSend
    //流程开始,无需frame
	UNWIND _objc_msgSend, NoFrame

    //判断p0(消息接收者)是否存在,不存在则重新开始执行objc_msgSend
	cmp	p0, #0			// nil check and tagged pointer check
//如果支持小对象类型,返回小对象或空
#if SUPPORT_TAGGED_POINTERS
    //b是进行跳转,b.le是小于判断,也就是p0小于0的时候跳转到LNilOrTagged
	b.le	LNilOrTagged		// (MSB tagged pointer looks negative)
#else
    //等于,如果不支持小对象,就跳转至LReturnZero退出
	b.eq	LReturnZero
#endif
    //通过p13取isa
	ldr	p13, [x0]		// p13 = isa
    //通过isa取class并保存到p16寄存器中
	GetClassFromIsa_p16 p13, 1, x0	// p16 = class

  • 如果消息接受者不为nil,汇编继续跑,到CacheLookup NORMAL,在cache中查找imp,来看一下具体的实现
//在cache中通过sel查找imp的核心流程
.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
	//

    //从x16中取出class移到x15中
	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表示p16寄存器存储的值,当前是Class
    //#数值 表示一个值,这里的CACHE经过全局搜索发现是2倍的指针地址,也就是16个字节
    //#define CACHE (2 * __SIZEOF_POINTER__)
    //经计算,p10就是cache
	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
//真机64位看这个
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    //CACHE 16字节,也就是通过isa内存平移获取cache,然后cache的首地址就是 (bucket_t *)
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
//获取buckets
#if __has_feature(ptrauth_calls)
	tbnz	p11, #0, LLookupPreopt\Function
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else
    //and表示与运算,将与上mask后的buckets值保存到p10寄存器
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
    //p11与#0比较,如果p11不存在,就走Function,如果存在走LLookupPreopt
	tbnz	p11, #0, LLookupPreopt\Function
#endif
    //按位右移7个单位,存到p12里面,p0是对象,p1是_cmd
	eor	p12, p1, p1, LSR #7
	and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
    //LSR表示逻辑向右偏移
    //p11, LSR #48表示cache偏移48位,拿到前16位,也就是得到mask
    //这个是哈希算法,p12存储的就是搜索下标(哈希地址)
    //整句表示_cmd & mask并保存到p12
	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

    //去除掩码后bucket的内存平移
    //PTRSHIFT经全局搜索发现是3
    //LSL #(1+PTRSHIFT)表示逻辑左移4位,也就是*16
    //通过bucket的首地址进行左平移下标的16倍数并与p12相与得到bucket,并存入到p13中
	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

						// do { 
        
//ldp表示出栈,取出bucket中的imp和sel分别存放到p17和p9
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	// {imp, sel} = *bucket--
    //cmp表示比较,对比p9和p1,如果相同就找到了对应的方法,返回对应imp,走CacheHit
	cmp	p9, p1				// if (sel != _cmd) { 
        
    //b.ne表示如果不相同则跳转到3f
	b.ne	3f				// scan more
						// } else { 
        
2:	CacheHit \Mode				// hit: call or return imp
						// }
//向前查找下一个bucket,一直循环直到找到对应的方法,循环完都没有找到就调用_objc_msgSend_uncached
3:	cbz	p9, \MissLabelDynamic		// if (sel == 0) goto Miss;
    //通过p13和p10来判断是否是第一个bucket
	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

通过 类对象/元类 (objc_class) 通过内存平移得到cache,获取buckets,通过内存平移的方式获取对应的方法(对比sel)。

在缓存中找到了方法那就直接调用,找到sel就会进入CacheHit,去return or call imp:返回或调用方法的实现(imp)。

CacheHit的内容:上图的Mode代表走下面的NORMAL流程,authenticate and call imp意思验证并调用方法实现。

// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
    //编码查找imp,并且返回x17,也就是imp
	TailCallCachedImp x17, x10, x1, x16	// authenticate and call imp
.elseif $0 == GETIMP
	mov	p0, p17
	cbz	p0, 9f			// don't ptrauth a nil imp
	AuthAndResignAsIMP x0, x10, 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, x10, x1, x16	// authenticate imp and re-sign as IMP
	cmp	x16, x15
	cinc	x16, x16, ne			// x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
	ret				// return imp via x17
.else
.abort oops
.endif
.endmacro

  • 如果没有找到缓存,查找下一个bucket,一直循环直到找到对应的方法,循环完都没有找到就调用__objc_msgSend_uncached

下面是上述判断跳转代码:

//LGetIsaDone是一个入口
LGetIsaDone:
	// calls imp or objc_msgSend_uncached
    //进入到缓存查找或者没有缓存查找方法的流程
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

__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

	// IMP in x0
	mov	x17, x0

	RESTORE_REGS MSGSEND

.endmacro

其中bl表示调用了方法_lookUpImpOrForward_lookUpImpOrForward在汇编里找不到,因为汇编的函数比C++的多一个下划线,需要去掉下划线,去找到lookUpImpOrForward方法实现

至此快速查找imp汇编部分就结束了,接下来到了:c/c++环节。

总结消息发送快速查找imp(汇编):

  1. 检查消息接收者receiver是否存在,为nil则不做任何处理
  2. 通过receiverisa指针找到对应的class类对象
  3. 找到class类对象进行内存平移,找到cache
  4. cache中获取buckets
  5. buckets中对比参数sel,看在缓存里有没有同名方法
  6. 如果buckets中有
  7. 如果buckets中没有对应的

慢速查找

什么是方法缓存

苹果认为如果一个方法被调用了,那个这个方法有更大的几率被再此调用,既然如此直接维护一个缓存列表,把调用过的方法加载到缓存列表中,再次调用该方法时,先去缓存列表中去查找,如果找不到再去方法列表查询。这样避免了每次调用方法都要去方法列表去查询,大大的提高了速率

慢速查找过程

先看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();;
        标签: 5w12v直插二极管sick小型光电传感器w12g

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

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