OC底层原理(07)运行时&方法的本质

134 阅读8分钟

一. Runtime

runtime 翻译过来称为运行时,与之对应的是编译时。在说 runtime 之前,我们先来了解下 编译时

1.1 Runtime 概述

编译时 顾名思义就是正在编译的时候 . 那啥叫 编译 呢?就是编译器帮你把 源代码翻译成机器能识别的代码。(当然只是一般意义上这么说,实际上可能只是翻译成某个中间状态的语言)

通俗的说 编译时就是简单的作一些翻译工作,比如检查你的代码有没有粗心写错啥关键 字了啊、词法分析、语法分析之类的过程。当你点下 build。那就开始编译了,如果下面有errors 或者 warning 信息,那都是编译器检查出来的。所谓这时的错误就叫 编译时错误。这个过程做的啥类型检查也就叫 编译时类型检查,或 静态类型检查(所谓静态嘛就是没把真把代码放内存中运行起来, 而只是把代码当作文本来扫描下). 所以有时一些人说编译时还分配内存啥的肯定是错误的说法

运行时 就是代码跑起来了,被装载到内存中去了。你的代码保存在磁盘上没装入内存之前是个死家伙,只有跑到内 存中才变成活的。而运行时类型检查就与前面讲的编译时类型检查(或者静态类型检查)不一样,不是简单的扫描代码,而是在内存中做些操作、判断。

1.2 Runtime 的使用的三种方式

runtime 的使用的三种方式,其三种实现方法与编译层和底层的关系如图所示

  • 通过 OC 上层的代码实现,例如 [YJPerson hello]
  • 通过 NSObject 方法实现,例如 isKindOfClass
  • 通过 Runtime API 底层方法实现,例如 class_getInstanceSize

Xnip2022-07-08_17-44-31.png

二. 方法的本质

2.1 objc_msgSend

源码:

@interface YJPerson : NSObject
@property (nonatomic, copy) NSString *name;
- (void)eat;
@end
@implementation YJPerson
- (void)eat {
    NSLog(@"%s",__func__);
}
@end

@interface YJStudent : YJPerson
@property (nonatomic, copy) NSString *studentId;
- (void)learn;
@end
@implementation YJStudent
- (void)learn {
    NSLog(@"%s",__func__);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        YJStudent *st = [[YJStudent alloc] init];
        [st eat];
        [st learn];
    }
    return 0;
}

使用clang 命令:clang -rewrite-objc main.m -o main.cpp生成 main.cpp 文件:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        YJStudent *st = ((YJStudent *(*)(id, SEL))(void *)objc_msgSend)((id)((YJStudent *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("YJStudent"), sel_registerName("alloc")), sel_registerName("init")); 
        ( (void (*) (id, SEL) )    (void *)objc_msgSend)((id)st, sel_registerName("eat"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)st, sel_registerName("learn"));
    }
    return 0;
}

通过 main.cpp 中的代码可以看出,底层都通过 objc_msgSend 调用的,是否可以在 OC 中直接使用 objc_msgSend 呢?来,咱试试。。。

Xnip2022-07-08_18-35-09.png

由输出结果可以得到 [st learn]objc_msgSend(st, @selector(learn)) 是一样的

注意:使用 objc_msgSend 时,需要导入头文件 #import <objc/message.h>Enable Strict Checking of objc_msgSend Calls 设置为 NO

启用 objc_msgSend 调用的严格检查,设置为 NO

Xnip2022-07-08_18-33-53.png

2.2 父类方法调用

messge.h 中查看 objc_msgSend 时,发现还有个 objc_msgSendSuper

Xnip2022-07-08_19-27-21.png

参数 superobjc_super 结构体指针,找到 objc_super 定义:

Xnip2022-07-08_19-31-48.png

objc_super中有 3 个成员

  • receiver 消息接收者
  • class !objc2 时才有,忽略
  • super_class 第一个要搜索的类

使用 objc_msgSendSuper 来调用父类方法:

Xnip2022-07-08_19-41-41.png

三. objc_msgSend 汇编探究

objc源码中全局搜索objc_msgSend,找到真机的汇编objc-msg-arm64.s

Xnip2022-07-08_20-41-06.png

3.1 汇编入口

下面的汇编会用到 p0-p17,大家可能对汇编中 x0x1 比较熟悉知道是寄存器。p0-p17就是对x0-x17重新定义

.endmacro
#endif
    // _objc_msgSend 入口,此时有两个参数一个是 id receiver(就是isa),另一个是 SEL _cmd 
    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    // receiver 和 0 比较
    cmp p0, #0 // nil check and tagged pointer check
// __LP64__ 64位支持 taggedpointer 类型
#if SUPPORT_TAGGED_POINTERS
    // 等于0,支持 taggedpointer 类型,走 LNilOrTagged
    b.le LNilOrTagged //  (MSB tagged pointer looks negative)
#else
    // 等于0,不支持 taggedpointer 类型,直接返回 nil,就是给一个空对象发消息
    b.eq LReturnZero
#endif
    // 把 x0 寄存器里面的地址读取到 p13 寄存器,对象的地址等于 isa 的地址
    ldr p13, [x0] // p13 = isa
    // p13 1 x0 作为参数传到 GetClassFromIsa_p16
    GetClassFromIsa_p16 p13, 1, x0 // p16 = class
// 这是一个标记符,拿到 isa 操作完以后继续后面的操作    
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    // 这个函数传递 3 个参数: NORMAL、_objc_msgSend、__objc_msgSend_uncached
    // 调用 CacheLookup
    CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    // 等于 0 直接返回 nil
    b.eq LReturnZero // nil check
    GetTaggedClass
    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
    // 结束 _objc_msgSend
    END_ENTRY _objc_msgSend

判断receiver是否等于nil, 在判断是否支持 Taggedpointer小对象类型

  • 支持 Taggedpointer 小对象类型,小对象为空返回nil,不为空处理 isa 获取 class 跳转 CacheLookup 流程
  • 不支持 Taggedpointer 小对象类型且 receiver = nil,跳转LReturnZero 流程返回 nil
  • 不支持 Taggedpointer 小对象类型且 receiver != nil,通过 GetClassFromIsa_p16 把获取到 class 存放在 p16 的寄存器中,然后走 CacheLookup 流程

3.1.1 GetClassFromIsa_p16 获取 Class

// src = p13(p13=isa),needs_auth=1,auth_address= x0
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
// armv7k or arm64_32
#if SUPPORT_INDEXED_ISA 
    // Indexed isa
    mov p16, \src // 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__ 
.if \needs_auth == 0 // _cache_getImp takes an authed class already
    mov p16, \src
.else // needs_auth = 1,所以走下面
    // 64-bit packed isa
    // 把 \src 和 \auth_address 传给 ExtractISA 得到的结果赋值给 p16寄存器
    ExtractISA p16, \src, \auth_address
.endif
#else
    // 32-bit raw isa
    mov p16, \src
#endif

GetClassFromIsa_p16 主要是获取 class 存放在 p16 寄存器

3.1.2 ExtractISA

.macro ExtractISA
    // and 表示 & 操作, $0 = $1(isa) & ISA_MASK = class
    and $0, $1, #ISA_MASK 
.endmacro

ExtractISA 主要功能 isa & ISA_MASK = class 存放到p16寄存器

3.2 CacheLookup流程

3.2.1 buckets 和下标 index

// Mode = NORMAL, Function = _objc_msgSend,  MissLabelDynamic = _objc_msgSend_uncached
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant

/* 省略 。。。 注释 */

// 将 寄存器x16的值(isa) 赋个 寄存器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 // arm64 模拟器

    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

#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 // arm64 真机

    // #define CACHE (2 * __SIZEOF_POINTER__) 即 2个指针的大小 16字节
    // 将 x16(isa) 平移 #CACHE(16字节) 得到 cache 地址,存储到寄存器 p11 = cache
    ldr p11, [x16, #CACHE] // p11 = mask|buckets
    #if CONFIG_USE_PREOPT_CACHES // ios 

        #if __has_feature(ptrauth_calls) // A12 以上
            tbnz p11, #0, LLookupPreopt\Function
            and p10, p11, #0x0000ffffffffffff // p10 = buckets
        #else // 其它
            // p10 = p11(cache地址,即_bucketsAndMaybeMask) & 0x0000fffffffffffe = 低48位,即buckets 地址
            and p10, p11, #0x0000fffffffffffe // p10 = buckets
            // 表示 _bucketsAndMaybeMask 第0位 != 0,则跳转到 LLookupPreopt/Function
            tbnz p11, #0, LLookupPreopt\Function
        #endif
        // p1 = _cmd,eor 是异或,p12 = p1 ^ (p1 >> 7) = _cmd ^ (_cmd >> 7)
        eor p12, p1, p1, LSR #7
        // p11(isa), LSR #48(右移48位) 得到高16位即:mask
        // p12 = ( _cmd ^ (_cmd >> 7)),即 (_cmd ^ (_cmd >> 7)) & mask
        // 这一步的作用就是 hash 求下标 index
        and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
        
    #else
        and p10, p11, #0x0000ffffffffffff // p10 = buckets
        and p12, p1, p11, LSR #48 // x12 = _cmd & mask
    #endif // CONFIG_USE_PREOPT_CACHES

#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 // arm32

    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

源码分析:首先是根据不同的架构判断,下面都是以真机为例。上面这段源码主要做了件事

  • 获取 _bucketsAndMaybeMask 地址也就是 cache 的地址:p16 = isa(class),p16 + 0x10 = _bucketsAndMaybeMask = p11
  • 获取 buckets 地址就是缓存内存的首地址:p10 = _bucketsAndMaybeMask & 0x0000fffffffffffe = buckets
  • 获取 hash下标:p12 =(cmd ^ ( _cmd >> 7))& msak 这一步的作用就是获取hash下标index
  • 流程如下:isa --> _bucketsAndMaybeMask --> buckets --> hash下标

3.2.2 遍历缓存

    // p10 = buckets
    // p12(下标index) =(cmd ^ ( _cmd >> 7))& msak
    // PTRSHIFT = 3,
    // p13 = p10 + (p12 << (1+3=4)),(index << 4)= index*16
    // p13 = buckets(首地址) + index*16(sizeof(bucket_t)) = index位置的 bucket_t
    add p13, p10, p12, LSL #(1+PTRSHIFT)

                         // do {
// 取出 x13 中的 imp/sel 分别放入 p17/p9 中
// 然后将 *bucket-- x13 = index对应的bucket地址先前移动一个位置
1: ldp p17, p9, [x13], #-BUCKET_SIZE //     {imp, sel} = *bucket--
    cmp p9, p1               //     if (sel != _cmd) { 比较 是不是要找的
    b.ne 3f                  //         scan more // 执行3,继续循环遍历
                             //     } else { // 是要找的
2: CacheHit \Mode                       // hit:    call or return imp 缓存命中
                             //     }
3: cbz p9, \MissLabelDynamic //     if (sel == 0) goto Miss;
    cmp p13, p10         // } while (bucket >= buckets)
    b.hs 1b
  • 根据下标 index 找到 index对应的 bucketp13 = buckets + ((_cmd ^ (_cmd >> 7)) & mask) << (1+PTRSHIFT))

  • bucket 中取出impsel存放到p17p9,然后 *bucket-- 向前移动一个位置

  • 1 流程:p9 = sel和 传入的参数_cmd进行比较。如果相等走2(缓存命中)流程,如果不相等走3(继续遍历)流程

  • 2 流程:缓存命中直接跳转 CacheHit 流程

  • 3 流程:判断sel = 0 条件是否成立。如果成立说明buckets里面没有传入的参数_cmd的缓存,没必要往下走直接跳转__objc_msgSend_uncached流程。如果sel != 0说明这个bucket被别的方法占用了。你去找下一个位置看看是不是你需要的。然后在判断下个位置的bucket和第一个bucket地址大小,如果大于第一个bucket的地址跳转1流程循环查找,如果小于等于则接继续后面的流程

  • 如果循环到第 1bucket里都没有找到符合的_cmd。那么会接着往下走,因为下标index后面的可能还有bucket还没有查询

3.2.3 CacheHit 流程

CacheHit \Mode的 Mode = NORMAL

.if $0 == NORMAL
    TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp

TailCallCachedImp是一个宏定义如下

.macro TailCallCachedImp
    // $0 = cached imp, $1 = buckets, $2 = SEL, $3 = class(也就是isa)
    eor $0, $0, $3 // $0 = imp ^ class 这一步是对imp就行解码,获取运行时的imp地址 
    br $0 //调用 imp 
.endmacro

3.2.4 遍历缓存流程图

为什么要判断bucket中的sel = 0,等于0直接查找缓存流程就结束了?

Xnip2022-07-10_17-56-22.png

  • 如果既没有 hash 冲突又没有目标方法的缓存,那么hash下标对应的bucket就是空的直接跳出缓存查找
  • 不会出现中间是有空的bucket,两边有目标bucket这种情况

缓存查询流程图

111.png