OC底层原理11-objc_msgSend源码分析(方法查找快流

2020-09-21  本文已影响0人  Gomu_iOS

我们在 OC底层原理10-cache_t分析(插入流程) 一文中探索了cache的插入流程,那cache是谁来读取的呢?又是怎么读取的呢?这就是本次研究的重心:objc_msgSend方法查找流程之快流程cache的读取

一、准备工作

1.1、objc4可编译源码,可直接跳到文章最后,下载调试好的源码

1.2、clang编译.cpp文件,OC底层原理04-对象的本质 一文中有clang命令简介

1.3、新建一个macOS-Command Line Tool项目,.main实现如下

#import <Foundation/Foundation.h>

@interface GomuTeacher : NSObject
- (void)sayHello;
@end

@implementation GomuTeacher
- (void)sayHello{
    NSLog(@"%s",__func__);
}
@end

@interface GomuPerson : GomuTeacher
- (void)sayHello;
- (void)sayNB;
@end

@implementation GomuPerson
- (void)sayNB{
    NSLog(@"%s",__func__);
}
- (void)sayHello{
    NSLog(@"%s",__func__);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
    }
    return 0;
}

二、方法的本质

2.1、通过clang.m编译成.cpp文件,查看main函数中方法调用的实现如下

//: .main object代码
GomuPerson *person = [GomuPerson alloc];
[person sayNB];
[person sayHello];

//: .cpp 编译后的c++代码
GomuPerson *person = ((GomuPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("GomuPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));

结论:方法的本质就是objc_msgSend消息发送

2.2、调用runtimeapi,验证上述结论

GomuPerson *person = [GomuPerson alloc];
//: 用实例对象调用方法
[person sayNB];
[person sayHello];
        
//: 用`objc_msgSend`方式调用方法
objc_msgSend(person, sel_registerName("sayNB"));
objc_msgSend(person, sel_registerName("sayHello"));

//: 打印
-[GomuPerson sayNB]
-[GomuPerson sayHello]
-[GomuPerson sayNB]
-[GomuPerson sayHello]

//: 扩展,调用GomuPerson父类GomuTeacher中的sayHello,不实例化teacher
//: 构造参数一 objc_super
struct objc_super gomuJc_super;
gomuJc_super.receiver = person;
gomuJc_super.super_class = [GomuTeacher class];
//: 调用objc_msgSendSuper
objc_msgSendSuper(&gomuJc_super, sel_registerName("sayHello"));
//: 打印
-[GomuTeacher sayHello]

三、objc_msgSend快速查找流程(读取cache)分析

由于objc_msgSend是由汇编写的,而我们研究的是arm64构架下的源码,所以直接进入objc4-781.2 源码中找到objc-msg-arm64.s查看,.s后缀代表汇编源码

3.1 找入口ENTRY _objc_msgSend,源码如下

//: -- objc_msgSend 汇编入口
    ENTRY _objc_msgSend
//: -- 无窗口
    UNWIND _objc_msgSend, NoFrame
//: -- p0:objc_msgSend的第一个参数,即消息接受者
//: -- cmp: 比较
//: -- #0:nil
//: -- 判断p0是否为空
    cmp p0, #0          // nil check and tagged pointer check
//: -- 支持taggedpointer(小对象类型)
#if SUPPORT_TAGGED_POINTERS
//: -- b.le: 执行标号,判断上面cmp的值是小于等于LNilOrTagged
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
//: -- b.eq: 执行标号,判断上面cmp的值等于LReturnZero
//: -- p0为空,返回nil
    b.eq    LReturnZero
#endif
//: -- p0不为空
//: -- p13 = x0栈内存中的值,即把isa赋值给p13
    ldr p13, [x0]       // p13 = isa
//: -- 通过isa & mask,然后得到class,这个后面单独分析
    GetClassFromIsa_p16 p13     // p16 = class
//: -- #define LGetIsaDone  7,可以通过LGetIsaDone跳到这里,执行下面语句
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
//: -- 如果isa存在,调用CacheLookup,开始cache查找流程(快速查找流程sel->imp)
//: -- 找到就返回imp,没找到就返回objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend

#if SUPPORT_TAGGED_POINTERS
//: -- LNilOrTagged条件判断逻辑
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

objc_msgSend第一个流程图:

未命名文件.png

3.2 GetClassFromIsa_p16源码分析

//: -- .macro 汇编宏定义
.macro GetClassFromIsa_p16 /* src */
//: -- __ARM_ARCH_7K__ >= 2  ||  (__arm64__ && !__LP64__)
#if SUPPORT_INDEXED_ISA
//: -- 把传入的值src赋值给p16,p16 = src
    mov p16, $0         // optimistically set dst = src
//: -- 判断如果不是非指针isa,则跳转到1,直接结束
    tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f  // done if not non-pointer isa
//: -- 如果是非指针isa,则走这里
//: -- adrp: 通过基地址 + 偏移 获得一个字符串(全局变量),后面看不懂
    adrp    x10, _objc_indexed_classes@PAGE
//: -- add: 相加,后面看不懂
    add: x10, x10, _objc_indexed_classes@PAGEOFF
//: -- ubfx: 无符号位段提取
    ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
//: -- ldr: 将p16后面的值赋值给p16
    ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:
//: -- 如果是64位,我们当前研究的环境arm64 & LP64 会走这里
#elif __LP64__
//: -- 传入的值src & ISA_MASK(isa的面具)
    and p16, $0, #ISA_MASK

#else
//: -- 如果是32位,则这就把src赋值给mov
    // 32-bit raw isa
    mov p16, $0

#endif
//: -- 宏定义结束
.endmacro

3.3 CacheLookup源码分析

//: -- 定义CacheLookup宏
.macro CacheLookup
//: -- 从$1开始查询,objc_msgSend第二个参数sel
LLookupStart$1:
    // p1 = SEL, p16 = isa
//: -- #define CACHE (2 * __SIZEOF_POINTER__),CACHE = 2*8 = 16
//: -- 从isa地址(cls首地址)开始平移16位,取出cache,存入p11中
//: -- isa 第一位站8字节,superclass占8字节,平移16位就是cache
//: -- arm64中_maskAndBuckets存在cache第一个位置
//: -- p11 = _maskAndBuckets,前16位是mask,后48位是buckets
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets
//: -- 64位真机
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//: -- p11 & #0x0000ffffffffffff(前16位为0后48位为1),相当于把前16位抹零,取到buckets,存入寄存器p10中
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
//: -- LSR #48:逻辑右移48位,则拿到mask,存入寄存器p11中
//: -- p1(sel) & p11(mask),得到sel-imp的下标index
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask
//: -- 非64位真机
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    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
//: -- p12: 下标index,p10: buckets数组首地址,PTRSHIFT:3
//: -- (_cmd & mask),取余,如果mask = 3 相当于取(0,1,2)
//: -- p12, LSL #(1+PTRSHIFT):index逻辑左移4位,相当于index*16
//: -- p12 = buckets + index*16,通过位移,拿到当前buckets中存在index位置的bucket
     add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
//: -- ldp:取栈内存中的值
//: -- p17 = imp,p9 = sel
    ldp p17, p9, [x12]      // {imp, sel} = *bucket
//: -- p9:当前位置bucket中存的sel
//: -- 传入的sel
//: -- 比较当前位置bucket中存的sel和传入的sel
1:  cmp p9, p1          // if (bucket->sel != _cmd)
//: -- 如果不相等就跳转到2
    b.ne    2f          //     scan more
//: -- 如果想等,则找到传入sel存在缓存中的bucket,返回imp,缓存命中
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
//: -- 如果一直找不到,因为这里是normarl,跳转至__objc_msgSend_uncached
    CheckMiss $0            // miss if bucket->sel == 0
//: -- p12:当前下标中的bucket
//: -- p10:buckets首地址,存的buckets第一个下元素
//: -- 判断当前bucket是否等于buckets的第一个元素
    cmp p12, p10        // wrap if bucket == buckets
//: -- 如果相等,跳转到3
    b.eq    3f
//: -- 如果不相等,则向前遍历
//: -- 从x12(即p12 buckets首地址),实际需要平移的内存大小BUCKET_SIZE,得到得到第二个bucket元素,imp-sel分别存入p17-p9,即向前查找
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
//: -- 跳转到第一步,继续对比,循环遍历
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask
//: -- 真机64位
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//: -- p11:_maskAndBuckets 
//: -- p12:前下标中的bucket
//: -- p11,LSR #(48 - (1+PTRSHIFT)),p11逻辑右移44位,相当于mask左移4位
//: -- 主动设置到最后一个元素 (有疑问)
    add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
                    // p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    add p12, p12, p11, LSL #(1+PTRSHIFT)
                    // p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.
//: -- ldp:取栈内存中的值
//: -- p17 = imp,p9 = sel
//: -- 再查找一遍缓存
    ldp p17, p9, [x12]      // {imp, sel} = *bucket
//: -- 比较当前位置bucket中存的sel和传入的sel
1:  cmp p9, p1          // if (bucket->sel != _cmd)
//: -- 如果不相等就跳转到2 
    b.ne    2f          //     scan more
//: -- 如果想等,则找到传入sel存在缓存中的bucket,返回imp,缓存命中
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
//: -- 如果一直找不到,因为这里是normarl,跳转至__objc_msgSend_uncached
    CheckMiss $0            // miss if bucket->sel == 0
//: -- 判断当前bucket是否等于buckets的第一个元素
    cmp p12, p10        // wrap if bucket == buckets
//: -- 如果相等,跳转到3
    b.eq    3f
//: -- 如果不相等,则向前遍历
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
//: -- 跳转到第一步,继续对比,循环遍历
    b   1b          // loop

LLookupEnd$1:
LLookupRecover$1:
3:  // double wrap
//: -- 跳转至JumpMiss 因为是normal ,跳转至__objc_msgSend_uncached
    JumpMiss $0
.endmacro

objc_msgSend快速查询流程图

objc_msgSend快速查询流程图.png

四、拓展知识-运行时

4.1 定义

运行时:是装载在内存,提供运行时功能(运行时的功能依赖runtime)
runtime:是一套由C、C++、汇编一起写成的api,给OC提供运行时

4.2 编译时

运行时相对应的是编译时

编译时:是源代码翻译成机器能识别的代码的过程,主要是对语言进行最基本的检查报错,即词法分析、语法分析等,是一个静态的阶段

4.3 运行时与编译时的区别

4.4 Runtime的三种调用方式

4.5 Runtime 官方文档

Runtime

上一篇下一篇

猜你喜欢

热点阅读