iOS 底层探索 - 方法

iOS 底层探索 - 方法

iOS 底层探索系列html

咱们在前面探索了对象和类的底层原理,接下来咱们要探索一下方法的本质,而在探索以前,咱们先简单过一遍 Runtime 的知识点,若是读者对这块内容已经很熟悉了的话能够直接跳过第一章。web

PS: 因为笔者对汇编暂时仍是摸索的阶段,关于汇编源码的部分若有错误,欢迎指正。

1、Runtime 简介

众所周知,Objective-C 是一门动态语言,而承载整个 OC 动态特性的就是 Runtime。关于 Runtime 更多内容能够直接进入官网文档查看。缓存

Runtime 是以 C/C++和汇编编写而成的,为何不用 OC 呢,这是由于对咱们编译器来讲,OC 属于更高级的语言,相比于 CC++ 以及汇编,执行效率更慢,而在运行时系统须要尽量快的执行效率。app

1.1 Runtime 的前世此生

Runtime 分为两个版本,legacymodern,分别对标 OC 1.0OC 2.0。咱们一般只须要专一于 modern 版本便可,在 libObjc 源码中体如今 new 后缀的文件上。ide

1.2 Runtime 三种交互方式

咱们与 Runtime 打交道有三种方式:函数

  • 直接在 OC 层进行交互:好比 @selector
  • NSObject 的方法:NSSelectorFromName
  • Runtime 的函数: sel_registerName

2、方法的本质探索

2.1 方法初探

image.png

咱们能够看到,经过 clang 重写以后,sayNB 在底层实际上是一个消息的发送。oop

咱们把右侧的发送消息的代码简化一下:post

LGPerson *person = objc_msgSend((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
objc_msgSend((id)person, sel_registerName("sayNB"));

因而可知,真正发送消息的地方是 objc_msgSend,这个方法有两个参数,一个是消息的接受者为 id 类型,第二个个是方法编号 sel学习

做为对比,run 方法就直接执行了,并无经过 objc_msgSend 进行消息发送:ui

image.png

2.2 方法发送的几种状况

LGStudent *s = [LGStudent new];
[s sayCode];        

objc_msgSend(s, sel_registerName("sayCode"));

上述代码表示的是向对象 s 发送 sayCode 消息。


id cls = [LGStudent class];
void *pointA = &cls;
[(__bridge id)pointA sayNB];

objc_msgSend(objc_getClass("LGStudent"), sel_registerName("sayNB"));

上述代码表示向 LGStudent 这个类发送 sayNB 消息。


// 向父类发消息(对象方法)
struct objc_super lgSuper;
lgSuper.receiver = s;
lgSuper.super_class = [LGPerson class];
objc_msgSendSuper(&lgSuper, @selector(sayHello));

上述代码表示向父类发送 sayHello 消息。


//向父类发消息(类方法)
struct objc_super myClassSuper;
myClassSuper.receiver = [s class];
myClassSuper.super_class = class_getSuperclass(object_getClass([s class]));// 元类
objc_msgSendSuper(&myClassSuper, sel_registerName("sayNB"));

上述代码表示向父类的类,也就是元类发送 sayNB 消息。

咱们在 OC 中使用 objc_msgSend 的时候,要注意须要将 Enbale Strict of Checking of objc_msgSend Calls 设置为 NO。这样才不会报警告。

image.png

3、探索 objc_msgSend

objc_msgSend 之因此采用汇编来实现,是由于

  • 汇编更容易能被机器识别
  • 参数未知、类型未知对于 CC++ 来讲不如汇编更驾轻就熟

3.1 消息查找机制

  • 快速流程
  • 慢速流程

3.2 定位 objc_msgSend 汇编源码

ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    cmp    p0, #0            // nil check and tagged pointer check

判断 p0 ,也就是咱们 objc_msgSend 的第一个参数 id 消息的接收者是否为空。

ldr    p13, [x0]        // p13 = isa
    GetClassFromIsa_p16 p13        // p16 = class

读取 x0 而后赋值到 p13 ,这里 p13 拿到的是 isa。为何要拿 isa 呢,由于不管是对象方法仍是类方法,咱们都须要在类或者元类的缓存或方法列表中去查找,因此 isa 是必需的。

3.3 GetClassFromIsa_p16

经过 GetClassFromIsa_p16,将获取到的 class 存在 p16 上面。

GetClassFromIsa_p16 源码以下:

.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA
    // Indexed isa
    mov    p16, $0            // optimistically set dst = src
    tbz    p16, #ISA_INDEX_IS_NPI_BIT, 1f    // done if not non-pointer isa
    // isa in p16 is indexed
    adrp    x10, _objc_indexed_classes@PAGE
    add    x10, x10, _objc_indexed_classes@PAGEOFF
    ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
    ldr    p16, [x10, p16, UXTP #PTRSHIFT]    // load class from array
1:

#elif __LP64__
    // 64-bit packed isa
    and    p16, $0, #ISA_MASK

#else
    // 32-bit raw isa
    mov    p16, $0

#endif

.endmacro

这个方法的目的就是经过位移操做获取 isashiftcls 而后进行位运算与操做获得真正的类信息。

LGetIsaDone:
    CacheLookup NORMAL        // calls imp or objc_msgSend_uncached

3.4 CacheLookup

获取完 isa 以后,接下来就要进行 CacheLookup ,查找方法缓存,咱们再来到 CacheLookup 的源码处:

/********************************************************************
 *
 * CacheLookup NORMAL|GETIMP|LOOKUP
 * 
 * Locate the implementation for a selector in a class method cache.
 *
 * Takes:
 *     x1 = selector
 *     x16 = class to be searched
 *
 * Kills:
 *      x9,x10,x11,x12, x17
 *
 * On exit: (found) calls or returns IMP
 *                  with x16 = class, x17 = IMP
 *          (not found) jumps to LCacheMiss
 *
 ********************************************************************/

.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

经过上述代码可知 CacheLookup 有三种模式:NORMALGETIMPLOOKUP

ldp    p10, p11, [x16, #CACHE]
  • CacheLookup 须要读取上一步拿到的类的 cache 缓存,而根据咱们前面对类结构的学习,这里显然进行 16 字节地址平移操做,而后把拿到的 cache_t 中的 bucketsoccupiedmask 赋值给 p10, p11
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
  • 这里是将 w1w11 进行与操做,其实本质就是 _cmd & mask。这一步和咱们探索 cache_t 时遇到的

image.png
是一模模同样样的道理。目的就是拿到下标。而后通过哈希运算以后,获得了 bucket 结构体指针,而后将这个结构体指针中的 impsel 分别存在 p17p9 中。

1:    cmp    p9, p1            // if (bucket->sel != _cmd)
    b.ne    2f            //     scan more
    CacheHit $0            // call or return imp
  • 接着咱们将上一步获取到的 sel 和咱们要查找的 sel(在这里也就是所谓的 _cmd)进行比较,若是匹配了,就经过 CacheHitimp 返回;若是没有匹配,就走下一步流程。
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
  • 因为上一步的 sel 没有匹配上,咱们须要接着进行搜索。

3.5 CheckMiss

咱们来到 CheckMiss 的源码:

.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 模式,因此会来到 __objc_msgSend_uncached:

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

__objc_msgSend_uncached 中最核心的逻辑就是 MethodTableLookup,意为查找方法列表。

3.6 MethodTableLookup

咱们再来到 MethodTableLookup 的定义:

.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

咱们观察 MethodTableLookup 内容以后会定位到 __class_lookupMethodAndLoadCache3。在 __class_lookupMethodAndLoadCache3 以前会作一些准备工做,真正的方法查找流程核心逻辑是位于 __class_lookupMethodAndLoadCache3 里面的。 可是咱们全局搜索 __class_lookupMethodAndLoadCache3 会发现找不到,这是由于此时咱们会从汇编跳入到 C/C++。因此去掉一个下划线就能找到:

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

4、总结

  • 方法的本质就是消息发送,消息发送是经过 objc_msgSend 以及其派生函数来实现的。
  • objc_msgSend 为了执行效率以及 C/C++ 不能支持参数未知,类型未知的代码,因此采用汇编来实现 objc_msgSend
  • 消息查找或者说方法查找,会优先去从类中查找缓存,找到了就返回,找不到就须要去类的方法列表中查找。
  • 由汇编过渡到 C/C++,在类的方法列表中查找失败以后,会进行转发。核心逻辑位于 lookUpImpOrForward

咱们下一章将会从 lookUpImpOrForward 开始探索,探索底层的方法查找的具体流程究竟是怎么样的,敬请期待~

相关文章
相关标签/搜索