iOS底层探索 -- objc_msgSend()流程分析

2020-09-21  本文已影响0人  iOS小木偶

引子:我们在很早时候就听过OC是一个运行时语言,那么什么是运行时?

引入两个概念,编译时运行时

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

2.运行时 :就是代码跑起来被装载到内存中去的阶段.

运行时语言主要就是讲OC将数据类型的确定编译时推迟到了运行时runtime.

运行时机制使我们知道运行时采取决定一个对象的类别,以及调用该类对象指定方法
而不同对象以自己的方式响应相同消息的能力叫做多态。

那么这个runtime对我们开发来说有什么作用?

runtime

   runtime是一套比较底层的纯C语言API, 属于1个C语言库, 包含了很多底层的C语言API。 runtime库里
面包含了跟类、成员变量、方法相关的API,比如获取类里面的所有成员变量,为类动态添 加成员变量,动态改变
类的方法实现,为类动态添加新的方法等 需要导入<objc/message.h><objc/runtime.h>
   在我们平时编写的OC代码中, 程序运行过程时, 其实最终都是转成了runtime的C语言代码,比如类转成了
runtime库里面的结构体等数据类型,方法转成了runtime库里面的C语言函数,平时调方法都是转成了objc_msgSend
函数(所以说OC有个消息发送机制)因此,可以说runtime是OC的底层实现,是OC的幕后执行者有了runtime库,
能做什么事情呢?runtime库里面包含了跟类、成员变量、方法相关的API,runtime是属于OC的底层, 可以进
行一些非常底层的操作(用OC是无法现实的, 不好实现)

 1.在程序运行过程中, 动态创建一个类(比如KVO的底层实现)
 2.在程序运行过程中, 动态地为某个类添加属性\方法, 修改属性值\方法
 3.遍历一个类的所有成员变量(属性)\所有方法 

 有了runtime,想怎么改就怎么改, runtime算是OC的幕后工作者.

在网上,我找到了这样一段关于runtime的描述。那么接下来,我们尝试一下其中提到的runtime库,以及里面最基础的objc_msgSend方法。

我们依旧使用之前的工程,在main中,我们写这样一段代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        FQPerson *person = [FQPerson alloc];
        Class personClass = [FQPerson class];
        [person eat1];
        [person eat2];
        [person eat3];
        [person eat4];
        [person sayHelloWorld];
        NSLog(@"%@",personClass);  
    }
    return 0;
}

这里可以说是我们最基础的写法。

现在,我们通过终端使用clang指令 生成main.m对应的main.cpp文件。

我们在其中找到对应上面 main 函数的方法

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 



        FQPerson *person = ((FQPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("FQPerson"), sel_registerName("alloc"));

        Class personClass =((Class (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("class"));

        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("eat1"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("eat2"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("eat3"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("eat4"));



        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHelloWorld"));

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_bs_g7z1ww_14gq20hnx_3hcvm2w0000gn_T_main_3fd2f3_mi_5,personClass);


    }
    return 0;
}

我们惊奇发现经过clang编译后,本身我们写的方法就已经被转为了我们之前提到的objc_msgSend方法。

那么,对我们来时就轻松了 可以直接的来“抄”一下这段系统调用方法的代码

首先引入头文件

#import <objc/message.h>

然后,在main函数中,我们将之前的方法改成objc_msgSend()的调用。

注意,我们需要先将工程中target -> BuildSetting -> enableStrictChecking of objc_msgSend calls -> NO 否则会报错


环境设置.jpg
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        FQPerson *person = [FQPerson alloc];
        objc_msgSend(person, sel_registerName("sayHelloWorld"));
        [person sayHelloWorld];
        
    }
    return 0;
}

运行后打印


打印1.jpg

可见两种代码的实现方式一模一样。

接下来,进入今天的正题,让我们来研究一下objc_msgSend()

objc_msgSend()的方法查找流程

下面我们来看看objc_msgSend()

在我们一般的oc代码中

[person sayHelloWorld]
通过对象person,去调用他的类FQPersonsayHelloWorld方法

这个很好理解,因为直接在.h中声明,.m中实现,很清楚。

但在objc_msgSend中,我们是通过发送消息 通过 对象,和方法名SEL,让他们直接去查找对应的IMP进而找到方法的实现。

那么这个由sel-> imp的过程 objc_msgSend 是如何实现的

首先回忆一下之前的内容,关于方法的存储。

对象->类->cache_t或者 bits中的methodlist

回到我们的781版本的源码

全局搜索objc_msgSend

objc_msgSend全局查找.jpg

茫茫多的结果,我们不可能每个去看。

简单分析一下。

objc_msgSendruntime中的,我们之前提到OC运行时装载是在编译时之后,编译后代码是汇编的,那我们现在要研究的也应该是在汇编中的,所以结尾应该是.s文件,我们开发面向的又是真机,那么我们第一要研究的必然是真机对应的arm64

打开对应的obj-msg-arm64.s。好了,研究开始。

继续研究源码

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    ldr p13, [x0]       // p13 = isa
    GetClassFromIsa_p16 p13     // p16 = class

  1. 其中p0第一个参数,即为当前objc_msgSend中的消息接收者。

  2. p0nil比较,即为判断第一个obj是否为空

  3. 若为tagged pointer或为都会跳转到其他流程,则剩下的为不为空的流程

  4. 此时取首地址即isa 然后通过GetClassFromIsa_p16获取class

GetClassFromIsa_p16 流程

.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA
    // Indexed isa
    mov p16, $0         // 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__
    // 64-bit packed isa
    and p16, $0, #ISA_MASK

#else
    // 32-bit raw isa
    mov p16, $0

#endif

例如
LP64 中,直接通过isa与上ISA_MASK 得到类,与我们之前探索一致

继续之前流程

LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend
  1. 得到isa后 开始CacheLookup流程。
.macro CacheLookup
    //
    // Restart protocol:
    //
    //   As soon as we're past the LLookupStart$1 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$1,
    //   then our PC will be reset to LLookupRecover$1 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
    //
LLookupStart$1:

    // p1 = SEL, p16 = isa
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask
#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


    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    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, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

LLookupEnd$1:
LLookupRecover$1:
3:  // double wrap
    JumpMiss $0

.endmacro

汇编源码比较难懂,所幸苹果在旁边写了清楚的注释。

提醒我们自己写代码多写点注释,不然时间久了自己都看不懂

闲话扯完继续分析

之前我们得到了class的地址

ldr p11, [x16, #CACHE]  // p11 = mask|buckets
#define CACHE            (2 * __SIZEOF_POINTER__)
  1. 所以即为平移16个字节,得到maskAndBuckets

  2. 然后通过位运算求得 buckets 以及 _cmd & mask

add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

这里回到了我们之前研究的地址平移的内容

  1. buckets的地址,直接加上_cmd&mask<<(1+PTRSHIFT)个单位的长度,那么就能知道找到_cmd&mask为下标的bucket.
  1. 分别取出SEL 存入P17 IMP 存入P9

  2. 开始递归循环

流程简图为


objc_msgSend流程.png

这样,我们就研究了objc_msgSend缓存查找,或者说快速查找流程
后续的慢速查找下次继续研究更新

上一篇 下一篇

猜你喜欢

热点阅读