iOS底层探索-objc_msgSend

984 阅读6分钟

上篇文章介绍了cache缓存的底层原理,知道cache是为了方法再次调用时能更快的被响应,这篇我们了解一下从cache缓存中读取方法

1、方法调用底层实现

1.1、转换cpp文件

  • 想要了解方法调用的底层实现,我们可以实际调用方法,然后看底层的C++实现,转换指令为 : clang -rewrite-objc xxx.m image.png

  • 生成cpp文件,因为方法调用写在了main函数里,所以在cpp文件中也查找main函数位置 image.png

1.2、objc_msgSend

  • 图中我们可以看出,有参数的方法只有参数部分相对复杂, 无参数的实例方法play与类方法jump 在编译后底层调用objc_msgSend方法,默认有两个参数:
    • 第一个参数 : 消息的接受者;实例方法为创建的具体person实例,类方法为 objc_getClass() 拿到的类
    • 第二个参数 : 消息的方法名
  • 我们可以直接将编译后的方法粘贴到代码中使用,(并且消息方法名sel_registerName("jump")也可以用NSSelectorFromString(@"jump")替换)能成功调起jump方法中的打印,说明消息确实通过 objc_msgSend 发送成功 image.png

1.3、objc_msgSendSuper

  • 我们再看一下常用的super类调用方法时的情况,对代码进行一些修改,在 init 方法中添加 self 调用与 super 调用 image.png
    • 可以看出,打印出的类都是LZPerson,而非LZPerson继承的NSObject,这是为什么呢?我们还需要看一下cpp文件
  • 因为写在 init 方法中,所以我们找 LZPerson 的 init 方法 image.png
    • 通过比对可以发现,[super class]中的内容不是通过 objc_msgSend 发送了,而是变成了 objc_msgSendSuper ,对于其作用我们可以借助苹果文档查阅,方式为 : 菜单栏 --> Help --> Developer Documentation image.png 对于其作用我们借助翻译工具查看大概 image.png
    • 再来需要看参数含义 : image.png 重点是说第一个参数是一个objc_super数据结构的指针,其中包括要接收消息的类的实例开始搜索方法实现的超类
    • 如此一来cpp文件中的内容就清晰了,接收消息的类的实例与objc_msgSend一样还是self,只是是从Super类开始找起,所以都打印了LZPerson image.png
    • objc_super结构体 :
      /// Specifies the superclass of an instance. 
      struct objc_super {
          /// Specifies an instance of a class.
          __unsafe_unretained _Nonnull id receiver;
      
          /// Specifies the particular superclass of the instance to message. 
      #if !defined(__cplusplus)  &&  !__OBJC2__
          /* For compatibility with old objc-runtime.h header */
          __unsafe_unretained _Nonnull Class class;
      #else
          __unsafe_unretained _Nonnull Class super_class;
      #endif
          /* super_class is the first class to search */
      };
      #endif
      
    • 手动仿写,能够正常打印 image.png

2、objc_msgSend流程

  • 每个架构下都有定义objc_msgSend,这里我们看arm64下的,使用了汇编代码,因为汇编代码效率更高执行更快 image.png
  • 看懂汇编不是我们的目的,所以省略分析汇编过程,后边直接给出里边代码的功能流程

2.1、获取类对象

  1. 判断 receiver是否存在
  2. 通过 receiver 的 isa指针 查找class

2.2、方法的快速查找(cache中查找)

CacheLookup
  • 找到类对象后,进入 CacheLookup 流程(汇编实现仅供了解)

    //在cache中通过sel查找imp的核心流程
    .macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
        //
        // Restart protocol:
        //
        //   As soon as we're past the LLookupStart\Function label we may have
        //   loaded an invalid cache pointer or mask.
        //
        //   When task_restartable_ranges_synchronize() is called,
        //   (or when a signal hits us) before we're past LLookupEnd\Function,
        //   then our PC will be reset to LLookupRecover\Function which forcefully
        //   jumps to the cache-miss codepath which have the following
        //   requirements:
        //
        //   GETIMP:
        //     The cache-miss is just returning NULL (setting x0 to 0)
        //
        //   NORMAL and LOOKUP:
        //   - x0 contains the receiver
        //   - x1 contains the selector
        //   - x16 contains the isa
        //   - other registers are set as per calling conventions
        //
    
    //从x16中取出class移到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
    //ldr表示将一个值存入到p10寄存器中
    //x16表示p16寄存器存储的值,当前是Class
    //#数值表示一个值,这里的CACHE经过全局搜索发现是2倍的指针地址,也就是16个字节
    //#define CACHE (2 * __SIZEOF_POINTER__)
    //经计算,p10就是cache
        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
    //真机64位看这个
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    //CACHE 16字节,也就是通过isa内存平移获取cache,然后cache的首地址就是 (bucket_t *)
        ldr p11, [x16, #CACHE] // p11 = mask|buckets
    #if CONFIG_USE_PREOPT_CACHES
    //获取buckets
    #if __has_feature(ptrauth_calls)
        tbnz p11, #0, LLookupPreopt\Function
        and p10, p11, #0x0000ffffffffffff // p10 = buckets
    #else
    //and表示与运算,将与上mask后的buckets值保存到p10寄存器
        and p10, p11, #0x0000fffffffffffe // p10 = buckets
    //p11与#0比较,如果p11不存在,就走Function,如果存在走LLookupPreopt
        tbnz p11, #0, LLookupPreopt\Function
    #endif
    //按位右移7个单位,存到p12里面,p0是对象,p1是_cmd
        eor p12, p1, p1, LSR #7
        and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
    #else
        and p10, p11, #0x0000ffffffffffff // p10 = buckets
    //LSR表示逻辑向右偏移
    //p11, LSR #48表示cache偏移48位,拿到前16位,也就是得到mask
    //这个是哈希算法,p12存储的就是搜索下标(哈希地址)
    //整句表示_cmd & mask并保存到p12
        and p12, p1, p11, LSR #48 // x12 = _cmd & mask
    #endif // CONFIG_USE_PREOPT_CACHES
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
        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
    
    //去除掩码后bucket的内存平移
    //PTRSHIFT经全局搜索发现是3
    //LSL #(1+PTRSHIFT)表示逻辑左移4位,也就是*16
    //通过bucket的首地址进行左平移下标的16倍数并与p12相与得到bucket,并存入到p13中
        add p13, p10, p12, LSL #(1+PTRSHIFT)
                            // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
    
                            // do {
    
    //ldp表示出栈,取出bucket中的imp和sel分别存放到p17和p9
    1: ldp p17, p9, [x13], #-BUCKET_SIZE //     {imp, sel} = *bucket--
    //cmp表示比较,对比p9和p1,如果相同就找到了对应的方法,返回对应imp,走CacheHit
        cmp p9, p1 //     if (sel != _cmd) {
    //b.ne表示如果不相同则跳转到2f
        b.ne 3f //         scan more
                //     } else {
    2: CacheHit \Mode // hit:    call or return imp
    //     }
    //向前查找下一个bucket,一直循环直到找到对应的方法,循环完都没有找到就调用_objc_msgSend_uncached
    3: cbz p9, \MissLabelDynamic //     if (sel == 0) goto Miss;
    //通过p13和p10来判断是否是第一个bucket
        cmp p13, p10 // } while (bucket >= buckets)
        b.hs 1b
    
  • CacheLookup 实现的功能

    1. class通过内存平移找到 cache
    2. 从cache 获取 bucket_t
    3. bucket_t 中比对 sel
    4. bucket_t中匹配到缓存的sel --> 调用cacheHit --> 调用对应imp
    5. bucket_t中未匹配到缓存的sel --> 调用_objc_msgSend_uncached

2.3、方法的慢速查找

  1. bucket_t中未能命中imp,调用 _objc_msgSend_uncached image.png

  2. 执行 MethodTableLookup 方法 image.png

  3. 执行 _lookUpImpOrForward 方法,跳出汇编层面,全局搜索 lookUpImpOrForward 找其实现 image.png 开始在方法列表里找 image.png

  4. 再找一次cache,为的是防止多线程操作时,刚好调用函数,还未找到的话调用 getMethodNoSuper_nolock尝试在 methodList 中找

  5. 简单的跳转找方法 search_method_list_inline --> findMethodInSortedMethodList

  6. 最终 findMethodInSortedMethodList 通过二分查找来找方法(分类方法放在MethodList前边位置) image.png

methodList中找到方法
  • goto done走下边 done 方法 image.png
  • log_and_fill_cache将方法缓存到 cache 中 image.png image.png
methodList中未找到方法
  • 如上边图示,将当前查找类换为其父类,并判断是否为nil,容易被忘的点是这些查找方法是在for循环中,换句话说 换成父类后会重新再进行相同的查找操作,不断向上查找到NSObject(NSObject父类为nil)为止,仍未找到那么将imp指针置为消息转发指针forward_imp,进入消息转发流程
总结
  • 方法调用流程 :
    • _objc_msgSend_uncached --> MethodTableLookup --> lookUpImpOrForward --> cache_getImp(再找cache) / getMethodNoSuper_nolock --> search_method_list_inline --> findMethodInSortedMethodList
  • 逻辑流程 :
    • lookUpImpOrForward --> 先找当前类的methodList --> 找父类cache --> 找父类methodList --> 父类为nil --> forward_imp --> 消息转发