Objective-C是一门非常动态的语言,以至于确定调用哪个方法被推迟到了运行时,而非编译时。与之相反,C语言使用静态绑定,也就是说在编译期就能决定程序运行时所应该调用的函数,所以在C语言中, 如果某个函数没有实现,编译时是不能通过的。而Objective-C是相对动态的语言,运行时还可以向类中动态添加方法,所以编译时并不能确定方法到底有没有对应的实现,编译器在编译期间也就不能报错。
在对象上调用方法在Objective-C中非常普遍。用Objective-C的术语来讲, 叫做“给某个对象发送某条消息"。消息有 “名称”或“选择子” (selector)之说。消息可以接受参数,而且还可以有返回值。
消息传递之机制
这是发送消息的基本格式:
id returnValue = [someObject messageName:parameter];
讯享网
本例中,someObject叫做方法调用者,也叫做接受者(receiver) 。messageName:是方法名,也叫做选择子(selector) 。选择子与参数合起来叫做“消息”(message) 。在运行时,编译器会把上面这个格式的方法调用转化为一条标准的C语言函数调用,该函数就是objc_ msgSend(),该函数是消息objc里在运行时传递机制中的核心函数,其原型如下:
讯享网void objc_msgSend(id self, SEL cmd, ....)
显而易见,该函数参数可变,第一个参数代表接受者,第二个参数代表选择子(方法名),后面就是消息中的参数一一对应;
而在经过编译器的处理,代码会被处理为:
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:0xf63 //sell2:0xf68
其中需要注意的是:@selector等于是把方法名翻译成SEL方法名。其仅仅关心方法名和参数个数,并不关心返回值与参数类型
生成SEL的过程是固定的,因为它只是一个表明方法的ID,不管是在哪个类写这个eat方法,SEL值都是固定一个
在Runtime中维护了一个SEL的表,这个表存储SEL不按照类来存储,只要相同的SEL就会被看做一个,并存储到表中。在项目加载时,会将所有方法都加载到这个表中,而动态生成的方法也会被加载到表中。
在OC中要得到方法SEL 可以直接使用:
- @ selector指示符号,SEL act = @selector(setAge:);
- 也可是函数:NSSelectorFromString(NSString *)
- (NSString *)NSStringFromSeletor (SEL)
IMP
IMP: 一个函数指针,保存了方法地址
它是OC方法实现代码块的地址,通过他可以直接访问任意一个方法。免去发送消息的代码,IMP声明:
typedef id (&IMP)(id,SEL,...);
IMP 是一个函数指针,这个被只想的函数包含一个接收消息的对象id(self 指针),调用方法的选标SEL(方法名),以及不定个数的方法参数,并返回一个id.
IMP与SEL的区别与联系
- SEL:类方法的指针,相当于一种编号,区别与IMP
- IMP:函数指针,保存了方法的地址
SEL是通过表取对应关系的IMP,进行方法的调用
- 每一个继承于
NSObject的类都能自动获的runtime的支持,在这样的类中,有一个isa指针,指向该类定义的数据结构体,这个结构体是编译器编译时为类创建的.在这个结构体中包括了指向其父类类定义的指针及Dispatch table,Dispatch table是一张SEL和IMP的对应表。也就是说方法编号SEL最后还要通过Dispatch table表找到对应的IMP,IMP是一个函数指针,然后去执行这个方法;
消息发送
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_msgSendSuper
- 父类Person
讯享网#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface Person : NSObject - (void)testInstancePrint; @end NS_ASSUME_NONNULL_END #import "Person.h" @implementation Person - (void)testInstancePrint {
NSLog(@"%s", __func__); } @end
- 子类Man
#import "Person.h" NS_ASSUME_NONNULL_BEGIN @interface Man : Person - (void)testInstancePrint; @end NS_ASSUME_NONNULL_END #import "Man.h" #import <objc/runtime.h> #import <objc/message.h> #import <malloc/malloc.h> @implementation Man - (instancetype)init {
if (self = [super init]) {
NSLog(@"%@", [self class]); NSLog(@"%@", [super class]); } return self; } - (void)testInstancePrint {
NSLog(@"%s", __func__); }
- main函数
讯享网#import <Foundation/Foundation.h> #import "Person.h" #import "Man.h" int main(int argc, const char * argv[]) {
@autoreleasepool {
Man* man = [[Man alloc] init]; [man testInstancePrint]; } return 0; }
运行结果:方法打印出的class竟然一致?我不是打印的 [super class]吗?

再次编译成cpp文件一探究竟,这次编译的是这个类的实现文件Man.m。可以看到是通过objc_msgSendSuper发送给父类

苹果官方文档对其方法解释为:
当遇到方法调用时,编译器会生成对以下函数之一的调用:objc_msgSend、objc_msgSend_stret、objc_msgSendSuper或objc_msgSendSuper_stret。发送到对象超类的消息(使用super关键字)使用objc_msgSendSuper发送;其他消息使用objc_msgSend发送。使用objc_msgSendSuper_stret和objc_msgSend_stret发送以数据结构作为返回值的方法。
再翻译参数:
super 指向objc_super数据结构的指针。传递值,标识消息发送到的上下文,包括要接收消息的 类的实例和要开始搜索方法实现的超类。 op SEL型指针。传递将处理消息的方法的选择器。 …包含方法参数的变量参数列表。
既然是发送给"类的实例",回看刚才的代码:这里接收者还是self。
(__rw_objc_super){
(id)self, (id)class_getSuperclass(objc_getClass("Man"))}
"开始搜索方法实现的超类"这部分又是什么意思呢?
来看objc_super结构体:
讯享网/// Specifies the superclass of an instance. struct objc_super {
/// Specifies an instance of a class. __unsafe_unretained _Nonnull id receiver; /// Specifies the particular superclass of the instance to message. #if !defined(__cplusplus) && !__OBJC2__ /* For compatibility with old objc-runtime.h header */ __unsafe_unretained _Nonnull Class class; #else __unsafe_unretained _Nonnull Class super_class; #endif /* super_class is the first class to search */ };
根据编译后的源码参数:{(id)self, (id)class_getSuperclass(objc_getClass("Man"))}。模拟super的实现:构造objc_super结构体,接收者是self,super_class使用父类Person;
- (void)testInstancePrint {
//NSLog(@"%s", __func__); struct objc_super ff_objc_super; ff_objc_super.receiver = self; ff_objc_super.super_class = Person.class; void* (*objc_msgSendSuperTyped)(struct objc_super *self,SEL _cmd) = (void *)objc_msgSendSuper; objc_msgSendSuperTyped(&ff_objc_super,@selector(testInstancePrint)); }

由此可见,方法的接收和查找不一定是同一个;
super只是关键字,结构体中的super_class 等于父类,代表从父类对象开始查找;不代表接收者receiver是父类对象;
objc_msgSendSuper的区别在于找方法的初始位置不一样。
快速查找
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(汇编):
objc_msgSend(receiver, sel, …)
- 检查消息接收者receiver是否存在,为nil则不做任何处理
- 通过receiver的isa指针找到对应的class类对象
- 找到class类对象进行内存平移,找到cache
- 从cache中获取buckets
- 从buckets中对比参数sel,看在缓存里有没有同名方法
- 如果buckets中有对应的sel --> cacheHit --> 调用imp
- 如果buckets中没有对应的sel --> _objc_msgSend_uncached -> _lookUpImpOrForward (c/c++慢速查找)
慢速查找
什么是方法缓存
苹果认为如果一个方法被调用了,那个这个方法有更大的几率被再此调用,既然如此直接维护一个缓存列表,把调用过的方法加载到缓存列表中,再次调用该方法时,先去缓存列表中去查找,如果找不到再去方法列表查询。这样避免了每次调用方法都要去方法列表去查询,大大的提高了速率
慢速查找过程
先看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)) {
// 如果是常量优化缓存 // 再一次从cache查找imp // 目的:防止多线程操作时,刚好调用函数,此时缓存进来了 #if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下 imp = cache_getImp(curClass, sel); //cache中找IMP if (imp) goto done_unlock; //找到就直接返回了 curClass = curClass->cache.preoptFallbackClass(); #endif } else {
//如果不是常量优化缓存 // 当前类的方法列表。 method_t *meth = getMethodNoSuper_nolock(curClass, sel); if (meth) {
imp = meth->imp(false); goto done; } // 每次判断都会把curClass的父类赋值给curClass if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// No implementation found, and method resolver didn't help. // Use forwarding. imp = forward_imp; break; } } // 如果超类链中存在循环,则停止。 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)) {
// 在超类中找到方法。在这个类中缓存它。 goto done; } } // 没有实现,尝试一次方法解析器。 // 这里就是消息转发机制第一层的入口 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 // iOS操作系统且真机的情况下 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; }
方法首先是定义一个消息的转发forward_imp;接着判断类的初始化、加锁、检查是否已知的类…等等,先不管这些。重点在于接下来的for循环:
// unreasonableClassCount()表示循环的上限; for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
// 如果是常量优化缓存 // 再一次从cache查找imp // 目的:防止多线程操作时,刚好调用函数,此时缓存进来了 #if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下 imp = cache_getImp(curClass, sel); if (imp) goto done_unlock; curClass = curClass->cache.preoptFallbackClass(); #endif } else {
// curClass方法列表。 method_t *meth = getMethodNoSuper_nolock(curClass, sel); if (meth) {
imp = meth->imp(false); goto done; } // 每次判断都会把curClass的父类赋值给curClass if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// 没有找到实现,方法解析器没有帮助。 // 使用转发。 imp = forward_imp; break; } } // 如果超类链中存在循环,则停止。 if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list."); } // 超类缓存。 imp = cache_getImp(curClass, sel); if (slowpath(imp == forward_imp)) {
// 在超类中找到forward::条目。 // 停止搜索,但不要缓存;调用方法 // 首先为这个类解析器。 break; } if (fastpath(imp)) {
// 在超类中找到方法。在这个类中缓存它。 goto done; } }
进入了一个循环逻辑:
- 从本类的
method list查找imp(查找的方式是getMethodNoSuper_nolock,一会分析); - 从本类的父类的
cache查找imp(cache_getImp汇编写的) - 从本类的父类的
method list查找imp
…继承链遍历…(父类->…->根父类) - 若上面环节有任何一个环节查找到了
imp,跳出循环,缓存方法到本类的cache(log_and_fill_cache);
直到查找到nil,指定imp为消息转发,跳出循环。
查找方式
看看在类和父类继承链中查找imp是个什么样的查找方式的(getMethodNoSuper_nolock):
讯享网/* * getMethodNoSuper_nolock * fixme * Locking: runtimeLock must be read- or write-locked by the caller / 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/> 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; }
跳转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(); // 已排序的二分查找 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:从Sorted可知从已排序的方法列表里查找,采用二分查找。findMethodInUnsortedMethodList:从Unsorted可知从未排序方法列表用的线性查找,通过for循环遍历一个个对比sel从而取出method_t:。
看一下findMethodInSortedMethodList函数,跳转findMethodInSortedMethodList,ALWAYS_INLINE代表这是始终内联的
讯享网// 方法内联 ALWAYS_INLINE static method_t * findMethodInSortedMethodList(SEL key, const method_list_t *list) {
if (list->isSmallList()) {
if (CONFIG_SHARED_CACHE_RELATIVE_DIRECT_SELECTORS && objc::inSharedCache((uintptr_t)list)) {
return findMethodInSortedMethodList(key, list, [](method_t &m) {
return m.getSmallNameAsSEL(); }); } else {
return findMethodInSortedMethodList(key, list, [](method_t &m) {
return m.getSmallNameAsSELRef(); }); } } else {
return findMethodInSortedMethodList(key, list, [](method_t &m) {
return m.big().name; }); } }
编译后走的是以下流程,这是通过二分查找进行方法查找的。
/* * search_method_list_inline / template<class getNameFunc> ALWAYS_INLINE static method_t * findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName) {
ASSERT(list); // 二分查找 // auto 代表自动匹配类型; auto first = list->begin(); auto base = first; // decltype: declare type,译为声明类型。这里获取表达式类型; decltype(first) probe; uintptr_t keyValue = (uintptr_t)key; uint32_t count; 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; }
分类优先
通过methods()方法可以看到,会判断rwe,而这就是因为分类产生的内存空间。

所以分类优先,因为分类同名的方法会排在列表靠前。多个分类有同名方法时,确保后编译的先调用。
跳出循环后
讯享网done: if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下 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;
如果找到了imp,就会把imp缓存到本类cache里(log_and_fill_cache)。(注意这里不管是本类还是本类的父类找到了imp,都会缓存到本类中去)
跳转 log_and_fill_cache :
/* * log_and_fill_cache * Log this method call. If the logger permits it, fill the method cache. * cls is the method whose cache should be filled. * implementer is the class that owns the implementation in question. / 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 cls->cache.insert(sel, imp, receiver); }
找到之后,会放入类的方法缓存里;此时方法还未执行。
再回到一开始主方法,如果慢查也没找到? curClass 赋值为父类的类对象;然后从父类缓存里查找;

如果父类里也没有,循环又重头开始直至nil : if (slowpath((curClass = curClass->getSuperclass()) == nil))。
此时消息发送阶段结束,这时就要进入消息的转发。
总结消息发送慢速查找imp(c/c++):
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
- 从本类的
method list(二分查找/遍历查找)查找imp - 从本类的父类的
cache查找imp(汇编) - 从本类的父类的
method list(二分查找/遍历查找)查找imp
…继承链遍历…(父类->…->根父类)里找cache和method list的imp - 若上面环节有任何一个环节查找到了
imp,跳出循环,缓存方法到本类的cache,并返回imp - 直到查找到
nil,指定imp为消息转发,跳出循环,执行动态方法解析resolveMethod_locked
消息的转发
动态决议
上面介绍了方法调用的本质是消息发送。那如果经过查找后,没有找到方法,系统会怎么处理?这就是接下来介绍的方法动态决议和消息转发。
动态决议过程
当本类和本类继承链下的cache和method list都查找不到imp,imp被赋值成了_objc_msgForward_impcache但是它没有调用,会进入动态方法解析流程,并且只会执行一次。

resolveMethod_locked的源码声明:
讯享网/* * resolveMethod_locked * Call +resolveClassMethod or +resolveInstanceMethod. * * Called with the runtimeLock held to avoid pressure in the caller * Tail calls into lookUpImpOrForward, also to avoid pressure in the callerb / 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); // inst:类对象 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); }
上述代码流程如下:
- 判断进行解析的是否是元类
- 如果不是元类,则调用
_class_resolveInstanceMethod进行对象方法动态解析 - a. 如果是元类,则调用
_class_resolveClassMethod进行类方法动态解析
b. 完成类方法动态解析后,再次查询cls中的imp,如果没有找到,则进行一次对象方法动态解析
如果类的实例调用的是实例方法:
resolveInstanceMethod(inst, sel, cls);
如果是类对象调用的类方法:
讯享网// inst:类对象 cls: 元类 resolveClassMethod(inst, sel, cls); if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls); }
两个方法:resolveInstanceMethod和resolveClassMethod。也称为方法的动态决议。
上述执行resolveMethod_locked方法后返回lookUpImpOrForwardTryCache
- 来到
lookUpImpOrForwardTryCache方法,实际调用的是_lookUpImpTryCache方法:
IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior) {
return _lookUpImpTryCache(inst, sel, cls, behavior); }
进入_lookUpImpTryCache源码,可以看到这里有cache_getImp;也就是说在进行一次动态决议之后,还会通过cache_getImp从cache里找一遍方法的sel。

如果还是没找到(imp == NULL)?也就是无法通过动态添加方法的话,还会执行一次lookUpImpOrForward,这时候进lookUpImpOrForward方法,这里behavior传的值会发生变化。
第二次进入lookUpImpOrForward方法后,执行到if (slowpath(behavior & LOOKUP_RESOLVER))这个判断时
讯享网// 这里就是消息转发机制第一层的入口 if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER; return resolveMethod_locked(inst, sel, cls, behavior); }
根据变化后的behavior值和LOOKUP_RESOLVER值之间的关系导致该if语句内部只能进入第一次,因此这个判断相当于单例。解释了为什么开头说的该动态解析resolveMethod_locked为什么只执行一次。
动态解析测试
resolveClassMethod:默认返回值是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文件中声明一个方法,但在.m文件中并没有实现这个方法。在外部调用这个方法就会导致程序崩溃.
原因:
- 第一步查找方法中,在自己的类对象以及父类的类对象中都没有找到这个方法的实现
- 所以转向动态方法解析,动态方法解析我们什么也没做,
- 所以进行第三步,转向消息转发,消息转发我们也什么都没做,最后产生崩溃
此时我们在动态方法解析这一步补救它:
- 当调用的是对象方法时,动态方法解析是在
resolveInstanceMethod方法中实现的 - 当调用的是类方法时,动态方法解析是在
resolveClassMethod中实现的
利用动态方法解析和runtime,我们可以给一个没有实现的方法添加方法实现。
讯享网#import "Person.h" @interface Man : Person - (void)test; @end #import "Man.h" #import <objc/runtime.h> #import <objc/message.h> @implementation Man +(BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"%s, sel = %@", __func__, NSStringFromSelector(sel)); return [super resolveInstanceMethod:sel]; } @end
运行如下:

可以看到为什么会有2次执行呢?放到最后再讲。类方法也是如此。
既然是因为找不到imp而崩溃,那么我们可以在这个方法里通过runtime的class_addMethod,给sel动态的生成imp。其中第四个参数是返回值类型,用void用字符串描述:“v@:”
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) {
if (!cls) return NO; mutex_locker_t lock(runtimeLock); return ! addMethod(cls, name, imp, types ?: "", NO); }
方法修改:
讯享网+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"%s, sel = %@", __func__, NSStringFromSelector(sel)); if (sel == @selector(test)) {
IMP imp = class_getMethodImplementation(self.class, @selector(addMethod)); class_addMethod(self.class, sel, imp, "v@:"); } return [super resolveInstanceMethod:sel]; } -(void)addMethod {
NSLog(@"%s", __func__); }
可以看到运行正常:

消息转发
如果系统在动态决议阶段没有找到实现,就会进入消息转发阶段。
消息的快速转发
当cache没有找到imp,类的继承链里的方法列表都没有找到imp,并且resolveInstanceMethod / resolveClassMethod 返回NO就会进入消息转发。
我们在 lookUpImpOrForward 的时候就看到 imp 被指定成了_objc_msgForward_impcache。
//如果上述在类对象和父类对象中没有查到方法 //我们就进入动态方法解析 if (resolver && !triedResolver) {
//triedResolver用来判断是否曾经进行过动态方法解析,如果没有那就进入动态方法解析,如果进行过,就跳过 runtimeLock.unlock(); _class_resolveMethod(cls, sel, inst); //动态方法解析函数 runtimeLock.lock(); // Don't cache the result; we don't hold the lock so it may have // changed already. Re-do the search from scratch instead. triedResolver = YES; //进行过动态方法解析就把这个标识为设置为YES goto retry;//retry是前面的发送消息的过程 } //如果动态方法解析失败,就进入消息转发 imp = (IMP)_objc_msgForward_impcache; //由这一步进入消息转发 cache_fill(cls, sel, imp, inst); //如果消息转发失败,程序崩溃 done: runtimeLock.unlock();
所以如果本类没有能力去处理这个消息,那么就转发给其他的类,让其他类去处理。
看一下进行消息转发的函数__objc_msgForward_impcache的具体实现, 它就是消息转发的流程;又到了我们的源码汇编阶段:
讯享网STATIC_ENTRY __objc_msgForward_impcache // Method cache version // THIS IS NOT A CALLABLE C FUNCTION // Out-of-band condition register is NE for stret, EQ otherwise. jne __objc_msgForward_stret jmp __objc_msgForward END_ENTRY __objc_msgForward_impcache ENTRY __objc_msgForward // Non-stret version movq __objc_forward_handler(%rip), %r11 jmp *%r11 END_ENTRY __objc_msgForward
但是__objc_msgForward_handler并没有开源。
消息快速转发测试
Person类中定义func1方法但是不实现,利用-(id)forwardingTargetForSelector:(SEL)aSelector方法进行消息快速转发Blank类中定义func1方法且实现
#import <Foundation/Foundation.h> @interface Person : NSObject - (void)func1; @end #import "Person.h" #import "Blank.h" #import <objc/runtime.h> #import <objc/message.h> @implementation Person -(id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s, aSelector = %@",__func__, NSStringFromSelector(aSelector)); if (aSelector == @selector(func1)) {
return [Blank alloc]; } return [super forwardingTargetForSelector:aSelector]; } @end #import <Foundation/Foundation.h> @interface Blank : NSObject - (void)func1; @end #import "Blank.h" @implementation Blank - (void)func1 {
NSLog(@"%s",__func__); } @end
main.m文件,新建person对象并调用func1方法
讯享网#import <Foundation/Foundation.h> #import "Person.h" #import "Man.h" #import <objc/runtime.h> int main(int argc, const char * argv[]) {
@autoreleasepool {
Person* person = [[Person alloc] init]; [person func1]; } return 0; }
运行如下:

转发的作用在于,如果当前对象无法响应消息,就将它转发给能响应的对象。
这时候方法缓存在哪?接收转发消息的对象
应用场景:专门搞一个类,来处理这些无法响应的消息。方法找不到时的crash收集。
演示的是实例方法,如果是类方法,只需要将 - 改成 + ;
消息的慢速转发
如果消息的快速转发也没有找到方法;后面还有个methodSignatureForSelector方法,作用是方法有效性签名。
将刚才使用快速转发forwardingTargetForSelector方法注释后,添加上methodSignatureForSelector方法后能否正常运行?
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%s, aSelector = %@",__func__, NSStringFromSelector(aSelector)); return [NSMethodSignature signatureWithObjCTypes:"v@:"]; }

因为方法签名需要搭配另一个方法:forwardInvocation
forwardInvocation方法提供了一个入参,类型是NSInvocation;它提供了target和selector用于指定目标里查找方法实现。
讯享网- (void)forwardInvocation:(NSInvocation *)anInvocation;
添加后就能正常运行了

在调用func1时,虽然没有提供方法实现,但是在了方法的慢速转发里提供了有效签名(只要格式正确,和实际返回类型不同也行),代码就不崩溃了。
防止系统崩溃的三个救命稻草:动态解析、快速转发、慢速转发。
应用场景:统一处理没实现的方法,进行提示。你也可以不做任何处理,这样消息找不到的崩溃就不会出现了。
不过救命稻草不能解决实际问题,只是为了app稳定性的一种手段。
总结
OC方法调用的本质就是消息发送,消息发送是SEL-IMP的查找过程
动态决议
过消息发送机制也找不到方法,系统在进入消息转发前,还会进行动态决议。
实例方法的动态决议
+ (BOOL)resolveInstanceMethod:(SEL)sel; // 系统通过该方法调用上面OC类里的实现 static void resolveInstanceMethod(id inst, SEL sel, Class cls)
类方法的动态决议
讯享网+ (BOOL)resolveClassMethod:(SEL)sel;
消息转发
消息快速转发
- (id)forwardingTargetForSelector:(SEL)aSelector;
消息慢速转发
讯享网// 方法签名 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector; // 正向调用 - (void)forwardInvocation:(NSInvocation *)anInvocation;
消息转发机制基本上分为三个步骤,也被称为消息的三次拯救:
- 动态方法解析
- 备援接收者
- 完整消息转发
我们可以通过控制这三个步骤其中一环来解决这一个问题
特别注意:如果是正常类的消息,是不会走到这三个步骤的。所以走到这三个不步骤的前提条件已经确定该消息为未知消息
流程图

一些问题
runtime是如何通过selector找到对应的IMP地址的?
缓存查找–>当前类查找–>父类逐级查找
如果子类调用父类方法,缓存在哪个类?
- 子类没有父类方法时:该方法的list.ptr为nil,说明子类不存储父类方法。
- 运行方法后,先从父类对象缓存里找:发现buckets一开始就是nil,说明没存在父类对象里,那再看当前的子类对象里有没有了。
- 子类对象的cache缓存了该方法。
回看慢查找方法,cls 是当前传入的类,curClass是局部变量

最终是当前类的cache插入缓存;

结论:缓存到当前传入的类
两次动态决议的原因
用前面的demo打断点测试一下:

运行后,lldb输入指令bt可以看到打印的信息
第一次进入该断点输入bt显示如下:
第二次进入该断点输入bt显示如下:

调用了___forwarding___符号,还有熟悉的慢速转发methodSignatureForSelector方法 ,可知第二次是消息转发;
在消息的第一次动态决议和快速转发都没找到方法后,进入到慢速转发。过程中,runtime还会调用一次lookUpImpOrForward,这个方法里包含了动态决议,这才造成了二次动态决议。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/61281.html