OC底层探究(6)--方法调用

一、运行时runtime

1.什么是runtime

runtime是一套由c、c++、汇编混合写成的,为oc提供运行时功能的api。
那为什么不直接用oc来实现运行时呢?oc是一门高级语言,我们知道越是高级的原因,被机器识别的能力就越弱,汇编是最接近底层的机器语言的。所以就需要借助c、c++和汇编来保证系统稳定性和提高执行效率。

2.runtime版本

Objective-C运行时系统有两个版本:早期版本(legacy 1.0)和现行版本(modern 2.0)。现行版本主要是Objective-C 2.0 及与其相关的新特性。在看底层源码的时候,有时可以看到一些宏定义,用来判断不同版。

3.运行平台

iPhone 程序和 Mac OS X v10.5 及以后的系统中的 64 位程序使用的都是 Objective-C 运行时系统的现行版 本。
其它情况(Mac OS X 系统中的 32 位程序)使用的是早期版本。

4.交互方式

Objective-C 程序有三种途径和运行时系统交互:
通过 Objective-C代码,比如@selector()
通过 Foundation框架中NSObject的方法,比如NSSelectorFromString()
通过直接调用runtime函数, sel_registerName()

二、方法的本质探索

将一段简单的方法调用代码,放在main方法中,然后使用clang命令编译。

 ZPerson *person = [ZPerson alloc];
 [person sayHello];

使用终端进入当前main.m文件所在领,然后使用clang -rewrite-objc main.m命令编译。得到如下编译后的源码:

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

去掉强制类型转换后:

objc_msgSend(objc_getClass("ZPerson"), sel_registerName("alloc"));
objc_msgSend(person, sel_registerName("sayHello"));

通过上面这段代码,我们发现不管是alloc方法调用还是sayHello方法的调用,都是通过objc_msgSend函数来实现的。因此我们可以说OC方法的本质就是通过objc_msgSend来发送消息。objc_msgSend包含有方法的调用的两个隐藏参数:self(消息接受者)和sel(方法编号)。sel_registerName等同于oc中的@selector(),可以根据传进的方法名得到一个sel

三、objc_msgSend分析

1.源码查找

objc_msgSend上按住command+点击,我们只能进到类似的方法定义的声明,而不能再向下进到方法的实现中去。

OBJC_EXPORT void
objc_msgSend(void /* id self, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

这是因为objc_msgSend是由汇编的代码实现的。首先从性能方面考虑,方法调用需要被快速的处理和响应,而汇编更容易被机器识别。而是由于未知参数的原因(个数未知、类型未知,比如NSLog()),c和c++作为静态语言,并不能满足这一特性。
所以只能通过全局查找objc_msgSend,来寻找它的实现过程。

搜索结果.png

可以看到,根据不同的平台,obcj提供了不同版本的方法实现,我们选择arm64版本进行分析。ENTRY为方法的入口标志,我们将从开始入手分析。

2.流程分析

a.找到_objc_msgSend

ENTRY _objc_msgSend
...
END_ENTRY _objc_msgSend

b.对消息接收者的判断处理

    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
    b.le    LLookup_NilOrTagged //  (MSB tagged pointer looks negative)
#else
    b.eq    LLookup_Nil
#endif

cmp是一个比较指令,p0为方法的第一个参数,也就是方法的接收者。所以这段代码就是判断方法接收者是否为空,然后做相应的处理。

c.获取isa

ldr p13, [x0]       // p13 = isa
GetClassFromIsa_p16 p13     // p16 = class

ldr是数据读取指令,将x0中的数据读取到p13中。这里p13就是isa,根据isa拿到类。注意:我们知道对象方法存在类里,类方法存在元类里。那么我们这时候拿到的就是类或者是元类。那么是怎么拿到的呢?

.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
.endmacro

macro是宏定义。这里的if判断我们进入的是#elif __LP64__,将$0ISA_MASK进行与运算,然后复制给p16。还记得我们之前博客里如何从isa里取值吗,ISA_MASK是不是似相识?将isabitsISA_MASK进行与运算,得到class(Class)(isa.bits & ISA_MASK)

d.CacheLookup查找缓存

再回到objc_msgSend的流程分析中来。

LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

取到isa之后,进入到CacheLookup中来。也就是从缓存中查找。

/********************************************************************
 *
 * CacheLookup NORMAL|GETIMP|LOOKUP
 * 
 * Locate the implementation for a selector in a class method cache.
 *
 * Takes:
 *   x1 = selector
 *   x16 = class to be searched
 *
 * Kills:
 *   x9,x10,x11,x12, x17
 *
 * On exit: (found) calls or returns IMP
 *                  with x16 = class, x17 = IMP
 *          (not found) jumps to LCacheMiss
 *
 ********************************************************************/
.macro CacheLookup
    // p1 = SEL, p16 = isa
    //x16 平移16位找到cache
    ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
//w 低位
    and w11, w11, 0xffff    // p11 = mask
#endif
    and w12, w1, w11        // x12 = _cmd & mask
    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)
//ne noequal
    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
//eq equal
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask
    add p12, p12, w11, UXTW #(1+PTRSHIFT)
                                // p12 = buckets + (mask << 1+PTRSHIFT)

    // 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

3:  // double wrap
    JumpMiss $0
    
.endmacro

ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|maskx16为cacht_t的地址,#CACHE为两个指针的大小(16字节),从 x16 + 0x16 指向的地址里面取出 2个 8位的数,分别存入p10, p11。根据上篇的cache_t分析可知,取出的p10是buckets,p11存的是occupied和mask。

and w12, w1, w11        // x12 = _cmd & mask
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

w是低位的意思。w1也就是p1,也就是第二个参数sel。w11也就是刚取出的p11的低位,也就是mask。w1与上w11也就等同于上篇中根据key和mask取下标cache_hash()。w12为取出的下标。

e.从cache_t中取出buckects递归遍历

    ldp p17, p9, [x12]      // {imp, sel} = *bucket
//1和2形成一个递归
//比较p9和p1
1:  cmp p9, p1          // if (bucket->sel != _cmd)
//如果不相等 就去执行2方法 不再向下执行
    b.ne    2f          //     scan more
//既然执行CacheHit 就说明p1和p9相等, 也就是从缓存里找到我们要找到方法 返回imp 不再向下执行 递归调用结束
    CacheHit $0         // call or return imp

//递归的流程之一
2:  // not hit: p12 = not-hit bucket
//检查p9是否为空, 如果p9为空,说明缓存数组为空  如果为空就去跳转执行 __objc_msgSend_uncached 递归也结束了
    CheckMiss $0            // miss if bucket->sel == 0
//如果p9不为空, 就比较p12和p10 也就是比较取出的bucket和buckets首个元素
    cmp p12, p10        // wrap if bucket == buckets
//如果相等 说明我们已经遍历完了buckets 去跳转执行3方法 递归也结束了
    b.eq    3f
//如果还没遍历到第一个元素, 就继续位移 取出新的imp和sel
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
//b为直接跳转, 直接跳转1方法 用新取出的bucket去比较
//执行到这里可以看出这是一个递归的流程
    b   1b          // loop

结合上面的代码注释,我们可以总结:CacheLookup的查找过程,就是一个遍历已有缓存的过程,如果遍历过程中缓存命中,就去执行CacheHit。如果没有命中,就去执行CheckMiss

f.缓存命中

.macro  CacheHit
.if $0 == NORMAL
    TailCallCachedImp x17, x12  // authenticate and call imp
.elseif $0 == GETIMP
    mov p0, p17
    AuthAndResignAsIMP x0, x12  // authenticate imp and re-sign as IMP
    ret             // return IMP
.elseif $0 == LOOKUP
    AuthAndResignAsIMP x17, x12 // authenticate imp and re-sign as IMP
    ret             // return imp via x17
.else
.abort oops
.endif
.endmacro

我们传递的参数$0的值是NORMAL,所以这是开始验证并调用方法。

g.CheckMiss检查

.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL
    cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

我们传递的参数$0的值是NORMAL。cbz一个判断指令,如果p90,也就是缓存数组为空,可肯定是找不到缓存了,就去执行__objc_msgSend_uncached

h.方法缓存未找到

看一下__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
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgSend_uncached

这里会执行一个MethodTableLookup方法继续查看MethodTableLookup

.macro MethodTableLookup
    
    // push frame
    SignLR
    stp fp, lr, [sp, #-16]!
    mov fp, sp
         
        //保存参数寄存器,为调用c函数做准备
    // 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)]

    // receiver and selector already in x0 and x1
    mov x2, x16
    bl  __class_lookupMethodAndLoadCache3

    // 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中将寄存器中数据都做了保存,并调用了一个__class_lookupMethodAndLoadCache3方法。我们查找__class_lookupMethodAndLoadCache3,发现并不能找到。我们知道汇编调用c方法的时候会在前面加一个下划线,而此时恰好有两条下划线,我们试试去掉一个在去查找呢?果然我们找了_class_lookupMethodAndLoadCache3方法。
_class_lookupMethodAndLoadCache3,又调用了了lookUpImpOrForward去查找,我们将在后面分析。

i.再查找一遍缓存

我们回到CacheLookup继续向下分析代码。

3:  // wrap: p12 = first bucket, w11 = mask
    add p12, p12, w11, UXTW #(1+PTRSHIFT)
                                // p12 = buckets + (mask << 1+PTRSHIFT)

    // 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

3:  // double wrap
    JumpMiss $0

上面的3``````2``````1步骤是重新的又遍历了一遍缓存。由于多线程的原因,可能当前调用的方法这时可能已经在别的线程调用结束了,也就是说现在可能缓存中已经有了方法的缓存了,这时我们再遍历一遍,也算是再给缓存查找一次机会吧。如果执行到JumpMiss,说明是怎的的在缓存里找不到调用方法了,这将直接调用__objc_msgSend_uncached

.macro JumpMiss
.if $0 == GETIMP
    b   LGetImpMiss
.elseif $0 == NORMAL
    b   __objc_msgSend_uncached
.elseif $0 == LOOKUP
    b   __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

方法调用时查找缓存的流程就到这了,结合前篇的cache_t分析效果更好哟!如果有不对的地方,欢迎评论区批评指正。

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

推荐阅读更多精彩内容