美文网首页
OC底层原理11-objc_msgSend源码分析(方法查找快流

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

作者: Gomu_iOS | 来源:发表于2020-09-21 17:52 被阅读0次

    我们在 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]
    
    • 导入头文件#import "objc/message.h"
    • 修改配置将严厉的检查机制关掉,否则objc_msgSend的参数会报错,Build Settings -> Enable Strict Checking of objc_msgSend Call -> NO
    • sel_registerName相当于上层的@selector或者NSSelectorFromString

    三、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 运行时与编译时的区别

    • 编译时就报错的就是编译时,比如语法、词法自动纠错
    • 运行之后才报错的就是运行时,比如,你写的bug~
      比如:申明一个方法sayHello,但不实现它,直接调用[p sayHello],我们直接command+B不会报错,这就是编译时,但是如果我们运行command+R就会崩溃,这就是运行时

    4.4 Runtime的三种调用方式

    • 通过OC代码:[p sayHello]
    • 通过NSObject方法:isKindOfClass、isMeberOfClass
    • 通过Runtime API:class_getInstanceSize,objc_msgSend
      其三种实现方法与编译层和底层的关系如图所示
      image.png
      compiler就是编译器,即LLVM,例如OCalloc对应底层的objc_allocruntime system libarary就是底层系统库

    4.5 Runtime 官方文档

    Runtime

    相关文章

      网友评论

          本文标题:OC底层原理11-objc_msgSend源码分析(方法查找快流

          本文链接:https://www.haomeiwen.com/subject/wsmeyktx.html