首先来看段代码:
代码语言:javascript复制- (void)viewDidLoad {
[super viewDidLoad];
Norman *norman1 = [[Norman alloc] init];
[norman1 play];
}
转成C 语言之后如下:
代码语言:javascript复制static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
Norman *norman1 = ((Norman *(*)(id, SEL))(void *)objc_msgSend)((id)((Norman *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Norman"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)norman1, sel_registerName("play"));
}
我们不难发现,OC中方法的本质就是:通过objc_msgSend来发送消息。
objc_msgSend中有两个参数:id类型的消息接收者,sel方法编号。
消息发送的几种场景
向实例对象发送消息
代码语言:javascript复制objc_msgSend(s, sel_registerName("sayCode"));
直接通过objc_msgSend进行消息发送,消息的接收者是实例对象本身
向类对象发送消息
代码语言:javascript复制objc_msgSend(objc_getClass("LGStudent"), sel_registerName("sayNB"));
直接通过objc_msgSend进行消息发送,消息的接收者是类对象本身
向实例对象的父类发送消息
代码语言:javascript复制// 向父类发消息(对象方法)
struct objc_super lgSuper;
lgSuper.receiver = s;
lgSuper.super_class = [LGPerson class];
objc_msgSendSuper(&lgSuper, @selector(sayHello));
通过objc_msgSendSuper进行消息发送。
消息的接收者是objc_super类型,其内部携带了当前方法的调用者——实例对象自身,以及实例对象的父类。
向类对象的父类发送消息
代码语言:javascript复制//向父类发消息(类方法)
struct objc_super myClassSuper;
myClassSuper.receiver = [s class]; // 类本身
myClassSuper.super_class = class_getSuperclass(object_getClass([s class]));// 类的父类,是元类
objc_msgSendSuper(&myClassSuper, sel_registerName("sayNB"));
通过objc_msgSendSuper进行消息发送。
消息的接收者是objc_super类型,其内部携带了当前方法的调用者——类对象自身,以及类对象的父类(元类)。
实际上objc_msgSendSuper最终也会通过调用objc_msgSend进行消息发送,具体可查阅我的这篇总结:[super class]和[self class]
快速查找流程
接下来我们开始正式分析快速查找流程。
首先打个断点。断点走到这里之后,Debug->Debug Workflow -> Always show disassembly,就可以看到对应的汇编代码:
然后将断点走到对应的objc_msgSend里面,点进去之后就进入到了消息快速查找流程的汇编源码libobjc.A.dylib'objc_msgSend'。这里需要着重说明的是,消息的快速查找流程是通过汇编语言来实现的,使用汇编的原因有二:
- 基于性能考虑。快速查找对于速度是有要求的,它要尽可能地快,而汇编语言是最接近机器语言的,因此其性能是最好的。
- C语言中不可能通过写一个函数来保留未知的参数并且跳转到一个任意的函数指针,C语言没有满足做这件事情的必要特性。
接下来开始看汇编源码:
如下就是整个_objc_msgSend的汇编源码:
代码语言:javascript复制 ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
// person - isa - 类
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
// tagged
adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
ubfx x11, x0, #60, #4
ldr x16, [x10, x11, LSL #3]
adrp x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
cmp x10, x16
b.ne LGetIsaDone
// ext tagged
adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
ubfx x11, x0, #52, #8
ldr x16, [x10, x11, LSL #3]
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
下面来详细阐述。
第4行 cmp p0 的作用大概就是检查当前消息的接收者是否为空。p0表示的就是0号寄存器的位置,消息的接收者就存在0号寄存器。
第11行 ldr p13, [x0] 的作用就是找到isa指针,p13表示的就是isa指针。
第12行 GetClassFromIsa_p16 p13 的作用就是:通过isa指针获取到对应的Class,
第13、14行
代码语言:javascript复制LGetIsaDone:
CacheLookup NORMAL
的作用就是:标明获取isa结束,开始在缓存中查找对应的方法实现。关于CacheLookup的定义和实现,下面?会详细说明。
CacheLookup 的定义如下:
代码语言:javascript复制.macro CacheLookup
// p1 = SEL, p16 = isa
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
and w11, w11, 0xffff // p11 = mask
#endif
and w12, w1, w11 // x12 = _cmd & mask
add p12, p10, p12, LSL #(1 PTRSHIFT)
// p12 = buckets ((_cmd & mask) << (1 PTRSHIFT))
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
add p12, p12, w11, UXTW #(1 PTRSHIFT)
// p12 = buckets (mask << 1 PTRSHIFT)
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // double wrap
JumpMiss $0
.endmacro
macro是宏定义的标识。
第3行 ldp p10, p11, [x16, 中,[x16表示的是cache,因为Cache前面有一个isa和一个superClass,它们共占用16个字节,x16是偏移16字节,因此就是Cache。p10, p11分别是在Cache中找到的buckets和occupied。
第8行 add p12, p10, p12, LSL 的作用是获取到对应的bucket
第11行 ldp p17, p9, [x12] 的作用是将这个bucket里面的imp和sel拿出来。
第12行到第14行
代码语言:javascript复制1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
的作用是:将当前的_cmd与bucket中的内容进行匹配,如果匹配不成功则进行下面的步骤,如果匹配成功则直接返回当前的imp.
第16到第21行
代码语言:javascript复制2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
的作用是:当缓存查找没有命中的时候,就通过CheckMiss进行查找(下面会详解CheckMiss)。
b.eq 3f表示的是,如果CheckMiss查找成功,就进行下面的步骤3。
第23、24行
代码语言:javascript复制3: // wrap: p12 = first bucket, w11 = mask
add p12, p12, w11, UXTW
就是对CheckMiss中查找到的方法进行缓存。
接下来我们看CheckMiss的汇编源码:
代码语言:javascript复制.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
我们正常的方法查找都是走得是NORMAL形式,因此走得是第6行__objc_msgSend_uncached。
__objc_msgSend_uncached的定义如下:
代码语言:javascript复制STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
第7行MethodTableLookup,其定义如下:
代码语言:javascript复制.macro MethodTableLookup
// push frame
SignLR
stp fp, lr, [sp, #-16]!
mov fp, sp
// save parameter registers: x0..x8, q0..q7
sub sp, sp, #(10*8 8*16)
stp q0, q1, [sp, #(0*16)]
stp q2, q3, [sp, #(2*16)]
stp q4, q5, [sp, #(4*16)]
stp q6, q7, [sp, #(6*16)]
stp x0, x1, [sp, #(8*16 0*8)]
stp x2, x3, [sp, #(8*16 2*8)]
stp x4, x5, [sp, #(8*16 4*8)]
stp x6, x7, [sp, #(8*16 6*8)]
str x8, [sp, #(8*16 8*8)]
// receiver and selector already in x0 and x1
mov x2, x16
bl __class_lookupMethodAndLoadCache3
// IMP in x0
mov x17, x0
// restore registers and return
ldp q0, q1, [sp, #(0*16)]
ldp q2, q3, [sp, #(2*16)]
ldp q4, q5, [sp, #(4*16)]
ldp q6, q7, [sp, #(6*16)]
ldp x0, x1, [sp, #(8*16 0*8)]
ldp x2, x3, [sp, #(8*16 2*8)]
ldp x4, x5, [sp, #(8*16 4*8)]
ldp x6, x7, [sp, #(8*16 6*8)]
ldr x8, [sp, #(8*16 8*8)]
mov sp, fp
ldp fp, lr, [sp], #16
AuthenticateLR
.endmacro
这里的作用说白了就是:在对应的类中,找到bits,然后找到bits的rw,然后找到rw中的ro,然后找到ro中的methodlist,进而找到对应的方法实现。(详见OC类的原理探究(一))。
其中,第8到第18行都是一些内存位移的准备条件,真正开启上面所说的查找流程的是第22行的__class_lookupMethodAndLoadCache3方法,我们点进去看一下其源码(全局搜索_class_lookupMethodAndLoadCache3):
代码语言:javascript复制/***********************************************************************
* _class_lookupMethodAndLoadCache.
* Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp().
* This lookup avoids optimistic cache scan because the dispatcher
* already tried that.
**********************************************************************/
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
到这里为止,汇编代码结束,开始走C代码了,也就是说,从这里就开始进入了慢速查找流程。
接下来看一道题目。
代码语言:javascript复制BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL re3 = [(id)[Norman class] isKindOfClass:[Norman class]];
BOOL re4 = [(id)[Norman class] isMemberOfClass:[Norman class]];
NSLog(@"n re1 :%hhdn re2 :%hhdn re3 :%hhdn re4 :%hhdn",re1,re2,re3,re4);
BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];
BOOL re7 = [(id)[Norman alloc] isKindOfClass:[Norman class]];
BOOL re8 = [(id)[Norman alloc] isMemberOfClass:[Norman class]];
NSLog(@"n re5 :%hhdn re6 :%hhdn re7 :%hhdn re8 :%hhdn",re5,re6,re7,re8);
其打印结果如下:
2021-02-13 09:34:49.287997 0800 Test[19265:2139692]
re1 :1
re2 :0
re3 :0
re4 :0
2021-02-13 09:34:49.288236 0800 Test[19265:2139692]
re5 :1
re6 :1
re7 :1
re8 :1
接下来进行分析。
首先来看isMemberOfClass,源码如下:
代码语言:javascript复制 (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
我们看到,对于实例对象而言,isMemberOfClass表示的是该实例对象的类是否是入参cls;对于类对象而言,isMemberOfClass表示的是该类对象的类(即对应的元类)是否是入参cls。
[(id)[NSObject class] isMemberOfClass:[NSObject class]]:isMemberOfClass的调用者[NSObject class]是类对象,其类是根元类,而这里的入参[NSObject class]是根类,因此这里返回NO。
[(id)[Norman class] isMemberOfClass:[Norman class]]:isMemberOfClass的调用者[Norman class]是类对象,其类是Norman元类对象,而这里的入参[Norman class]是类对象,因此这里返回NO。
[(id)[NSObject alloc] isMemberOfClass:[NSObject class]]:isMemberOfClass的调用者[NSObject alloc]是实例对象,其类是NSObject类对象,而这里的入参[NSObject class]就是NSObject类对象,因此这里返回YES。
[Norman alloc] isMemberOfClass:[Norman class]]:isMemberOfClass的调用者[Norman alloc]是实例对象,其类是Norman类对象,而这里的入参[Norman class]就是Norman类对象,因此这里返回YES。
接下来分析isKindOfClass,源码如下:
代码语言:javascript复制 (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
实例对象调用isKindOfClass之后,首先会获取到该实例对象的类对象,然后与入参cls进行匹配,匹配成功之后返回YES;如果匹配不成功,则获取到当前类对象的父类,然后与入参cls进行匹配,匹配成功之后返回YES;如果还是没有匹配成功,则获取父类的父类,以此类推;最终找到根类NSObject,将之与入参cls进行匹配,匹配成功之后返回YES;如果还是匹配不成功,则返回NO。
这里说明一点,循环的条件是当前类是否存在,根类NSObject的父类是nil,因此当遍历完了根类之后,整个循环就会结束,然后返回NO。
类对象调用isKindOfClass之后,首先会获取到该类对象的类,即对应的元类对象,然后与入参cls进行匹配,匹配成功之后返回YES;如果匹配不成功,则获取到当前元类对象的父类,然后与入参cls进行匹配,匹配成功之后返回YES;如果还是没有匹配成功,则获取元类的父类的父类,以此类推;最终找到根元类NSObject,将之与入参cls进行匹配,匹配成功之后返回YES;如果匹配不成功,则获取到根元类对象的父类,即根类NSObject,然后与入参cls进行匹配,匹配成功之后返回YES;如果还是匹配不成功,则返回NO。
这里说明一点,循环的条件是当前类是否存在,根元类NSObject的父类是根类NSObject,根类NSObject的父类是nil,因此当遍历完了根类之后,整个循环就会结束,然后返回NO。
[(id)[NSObject class] isKindOfClass:[NSObject class]]:
isKindOfClass的调用者[NSObject class]是类对象,其类是根元类,根元类的父类是NSObject根类,此时与这里的入参[NSObject class]是匹配的,因此这里返回YES。
[(id)[Norman class] isKindOfClass:[Norman class]]:
isKindOfClass的调用者[Norman class]是类对象,其类是Norman元类,Norman元类的所有父类都是元类,只有根元类的父类是NSObject根类,而这里的入参[Norman class]是类对象,它并不能与Norman元类的所有父类中的唯一的类对象NSObject根类对象匹配成功,因此这里返回NO。
[(id)[NSObject alloc] isKindOfClass:[NSObject class]]:
isKindOfClass的调用者[NSObject alloc]是实例对象,其类是根类NSObject,此时与这里的入参[NSObject class]是匹配的,因此这里返回YES。
[(id)[Norman alloc] isKindOfClass:[Norman class]]:
isKindOfClass的调用者[Norman alloc]是实例对象,其类是Norman类对象,而这里的入参[Norman class]就是Norman类对象,匹配成功,因此这里返回YES。
以上。