objc_msgSend底层探索

废话不多说,先上一张图:

objc_msgSend流程分析.png

没错,这就是裸奔的objc_msgSendl流程图。接下来,我们来解读一下这张图:

一、首先我们先认识一下Runtime

  • 1.0 什么是Runtime?
    runtime(运行时系统),是一套基于C语言API,包含在 <objc/runtime.h><objc/message.h>中,运行时系统的功能是在运行期间(而不是编译期或其他时机)通过代码去动态的操作类(获取类的内部信息和动态操作类的成员),如创建一个新类、为某个类添加一个新的方法或者为某个类添加实例变量、属性,或者交换两个方法的实现、获取类的属性列表、方法列表等和Java中的反射技术类似。
  • 1.1 Runtime 版本:
    Runtime 分为两个版本,legacymodern,分别对应 Objective-C 1.0 和 Objective-C 2.0。在ARC环境下我们只需要专注于 modern 版本即可。
  • 1.2 Runtime 三种交互方式:
    a.直接在OC层进行交互,:比如@selector;
    b.NSObjCRuntime 的:NSSelectorFromString 方法;
    c.Runtime API:sel_registerName
  • 1.3 Runtime 结构图:(细细的品)


    Runtime底层结构示意图

二、方法的本质

1.0 通过Clang 探索方法
a.直接上代码:在main函数中实例化一个对象,并调用它的对象方法。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        CPPerson *person = [CPPerson alloc];
        [person sayHello];
    }
    return 0;
}

b.使用终端命令行切换到mian.m文件所在的目录下并执行 ,编译成Cpp 文件。

 clang -rewrite-objc main.m

c.runtime代码如下:

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

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

    }
    return 0;
}

我们可以看到,经过重写之后,sayHello 方法在底层其实就是一个消息的发送。我们把上面的发送消息的代码简化一下:

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

如果要在工程当中直接使用 objc_msgSend API,我们需要导入头文件 <objc/message.h> 和 将 Enbale Strict Checking of objc_msgSend Calls 设置为 NO,这样才不会报错。
可以看到,也是可以正常调用方法的,由此可见,真正发送消息的地方是 objc_msgSend,这个方法有基本的两个参数,第一个参数是消息的接收者为 id 类型,第二个参数是方法编号为 SEL 类型。

三、objc_msgSend(主角)

3.0 objc_msgSend之所以采用汇编来实现,有以下两种主要因素:

  • 汇编更容易更快的被机器识别
  • 在C语言中不可能通过一个函数来保留未知的参数并且跳转到一个任意的函数指针。C语言没有满足做这件事情的必要性
    3.1objc_msgSend消息查询机制
    消息查找机制分为两个机制
  • 快速流程
  • 慢速流程

接下来我们开始本文的重点,针对 方法缓存 cache_t 的分析(本文采用的源码版本为 objc4-781 )

3.1 cache_t 源码分析
我们知道,当我们的 OC 项目在编译完成之后,类的实例方法(方法编号 SEL 和函数指针地址 IMP)会保存在类的方法列表中。

OC 为了实现其动态性,将 方法的调用包装成了 SEL 寻找 IMP 的过程。我们可以想象一下,如果每次调用方法,都要去类的方法列表或者父类、根类的方法列表里面去查询函数地址的话,必然会对性能造成极大的损耗。为了解决这个问题,OC 采用了方法缓存的机制来提高调用效率,也就是 cache_t,其作用就是缓存已调用的方法。当调用方法时,objc_msgSend 会先去缓存中查找,如果找到就执行该方法;如果不在缓存中,则去类的方法列表或者父类、根类的方法列表去查找,找到后会将方法的 SELIMP 缓存到 cache_t 中,以便下次调用时能够快速执行。

3.2 objc_msgSend 读取缓存
之前我们已经分析过 cache_t 写入缓存的工作流程,下面我们来分析一下 objc_msgSend 读缓存的代码。( 以 arm64 架构汇编为例

3.2.1 _objc_msgSend 入口函数源码实现

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    // 判断P0,也就是我们 `objc_msgSend` 的第一个参数 `id` 消息的接收者是否存在
    cmp p0, #0          // nil check and tagged pointer check
    // 是否是 `taggedPointer` 对象判断处理
#if SUPPORT_TAGGED_POINTERS
    // `tagged` 或者空判断
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    // 直接返回空
    b.eq    LReturnZero
#endif
    // 读取 `x0`,然后复制到 `p13`,这里 `p13` 拿到的是 `isa`。为什么要拿 `isa` 呢,因为不论是对象方法还是类方法,我们都需要在类或者元类的缓存或者方法列表中去查找,所以 `isa` 是必须的。
    ldr p13, [x0]       // p13 = isa
    // 通过 `GetClassFromIsa_p16`,将获取到的 `class` 存到 `p16`。
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    // 获取完 `isa` 之后,接下来就要进行 `CacheLookup`,进行查找方法缓存,我们接下来到 `CacheLookup`的源码处
    CacheLookup NORMAL, _objc_msgSend

3.2.2 CacheLookup 源码实现:

// CacheLookup NORMAL|GETIMP|LOOKUP <function>
.macro CacheLookup
    // p1 = SEL, p16 = isa
    // `CacheLookup` 需要读取上一步拿到的类的 `cache` 缓存,然后进行 16 字节地址平移操作,把 `cache_t` 中的 `_maskAndBuckets` 复制给 `p11`。
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets
    
    // 将 `_maskAndBuckets` & `bucketsMask` 掩码,然后将结果放在 `p10 = buckets`
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
    // LSP逻辑右移,将 `p11` 右移48位得到 `mask`, `sel & mask` 后把结果放入到 `p12`,这里的本质就是我们在写入内存遇到的 `cache_hash` 方法一模一样,目的就是拿到方法缓存的哈希下标。
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask
    
    // LSL逻辑左移,`p10` 是 `buckets` 也是缓存数组的首地址,每个 `bucket(sel 8字节 + imp 8字节)` 的大小为 16 字节,`p12` 为方法缓存的哈希下标,`buckets + (index << 4)` 得到 下标处对应的 `bucket`,然后把结果放到 `p12`。
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
    // 从 `bucket` 结构体将中 `imp` 和 `sel` 分别存到 `p17` 和 `p9` 中。
    ldp p17, p9, [x12]      // {imp, sel} = *bucket
    
// 接着我们将上一步获取到的 `sel` 和我们要查找的 `sel`(在这里也就是所谓的 `_cmd`)进行比较,如果匹配了,就通过 `CacheHit` 将 imp 返回;如果没有匹配,就走下一步流程。
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    // 命中缓存,返回结果
    CacheHit $0         // call or return imp
    
    // 没找到 `bucket`
2:  // not hit: p12 = not-hit bucket
    // 如果从最后一个元素往前遍历都找不到缓存,那么走 `CheckMiss`
    CheckMiss $0            // miss if bucket->sel == 0
    // 判断当前查询的 `bucket` 是否为第一个元素
    cmp p12, p10        // wrap if bucket == buckets
    // 如果是第一个元素,那么将当前查询的` bucket` 设置为最后一个元素 `(p12 = buckets + (mask << 1+PTRSHIFT))`
    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)
#endif

    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

3.2.3 CheckMiss 源码实现:

.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL
    // 由于我们是 `NORMAL` 模式,所以会来到这 `__objc_msgSend_uncached`
    cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

3.2.4 __objc_msgSend_uncached 源码实现:

STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
    
// 这里面最核心的逻辑就是 `MethodTableLookup`,查找方法列表。
MethodTableLookup
TailCallFunctionPointer x17

END_ENTRY __objc_msgSend_uncached

3.2.5 MethodTableLookup 源码实现:

.macro MethodTableLookup
    
    // push frame
    SignLR
    stp fp, lr, [sp, #-16]!
    mov fp, sp

    // save parameter registers: x0..x8, q0..q7
    sub sp, sp, #(10*8 + 8*16)
    stp q0, q1, [sp, #(0*16)]
    stp q2, q3, [sp, #(2*16)]
    stp q4, q5, [sp, #(4*16)]
    stp q6, q7, [sp, #(6*16)]
    stp x0, x1, [sp, #(8*16+0*8)]
    stp x2, x3, [sp, #(8*16+2*8)]
    stp x4, x5, [sp, #(8*16+4*8)]
    stp x6, x7, [sp, #(8*16+6*8)]
    str x8,     [sp, #(8*16+8*8)]

    // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // receiver and selector already in x0 and x1
    mov x2, x16
    mov x3, #3
  // `重点`
    bl  _lookUpImpOrForward

    // IMP in x0
    mov x17, x0
    
    // restore registers and return
    ldp q0, q1, [sp, #(0*16)]
    ldp q2, q3, [sp, #(2*16)]
    ldp q4, q5, [sp, #(4*16)]
    ldp q6, q7, [sp, #(6*16)]
    ldp x0, x1, [sp, #(8*16+0*8)]
    ldp x2, x3, [sp, #(8*16+2*8)]
    ldp x4, x5, [sp, #(8*16+4*8)]
    ldp x6, x7, [sp, #(8*16+6*8)]
    ldr x8,     [sp, #(8*16+8*8)]

    mov sp, fp
    ldp fp, lr, [sp], #16
    AuthenticateLR

.endmacro

总结:

  • 我们观察 MethodTableLookup 内容之后会定位到 _lookUpImpOrForward。真正的方法查找流程核心逻辑是位于 _lookUpImpOrForward 里面的。 但是我们全局搜索 _lookUpImpOrForward 会发现找不到,这是因为此时我们会从 汇编 跳入到 C/C++。

  • 方法的本质就是消息发送,消息发送是通过 objc_msgSend 以及其派生函数来实现的。
    objc_msgSend 为了执行效率以及 C/C++ 不能支持参数未知,类型未知的代码,所以采用 汇编 来实现 objc_msgSend。
    消息查找或者说方法查找,会优先去从类中查找缓存,找到了就返回,找不到就需要去 类的方法列表 中查找。
    由汇编过渡到 C/C++,在类的方法列表中查找失败之后,会进行转发。核心逻辑位于 lookUpImpOrForward。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335