07、iOS底层探索-objc_msgSend

语言: CN / TW / HK

highlight: hybrid

上篇文章介绍了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结构体 :
      ```swift /// 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 流程(汇编实现仅供了解) ```swift //在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中未找到方法
  • 如上边图示,将当前查找类换为其父类,imp指针置为消息转发指针forward_imp,容易被忘的点是这些查找方法是在for循环中,换句话说 换成父类后会重新再进行一遍相同的查找操作,直到最终扔未找到那么forward_imp指针不再被替换,转入消息转发流程
总结
  • 方法调用流程 :
    • _objc_msgSend_uncached --> MethodTableLookup --> lookUpImpOrForward --> cache_getImp(再找cache) / getMethodNoSuper_nolock --> search_method_list_inline --> findMethodInSortedMethodList
  • 逻辑流程 :
    • lookUpImpOrForward --> 先找当前类的methodList --> 找父类cache --> 找父类methodList --> 父类为nil --> forward_imp --> 消息转发