方法的查找流程——快速查找

2021-03-10 14:16:55 浏览数 (1)

首先来看段代码:

代码语言: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里面的impsel拿出来。

第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。

以上。

0 人点赞