objc_msgSend流程分析

先来个流程图:


objc_msgSend逻辑.png

一、运行时runtime

1、什么是runtime

我们都知道OC调用方法是通过runtime运行时机制发送消息来进行调用的,但是什么是runtime,什么是运行时呢,这次我们来理解下。

a、Runtime :

本身只是用C、C++、汇编编写的一套为OC提供运行时功能的api

b、运行时:

  就是将代码装载在内存在在需要的是进行调用就叫做运行时

c、编译时:

与运行时相对应的就是编译时,所谓编译时就是将语法翻译成机器能识别的语言,当我们command+B,就会发现在我们工程的produce里面有个.app格式的文件,那个就是编译成的可执行文件,运行的时候就是将这个可执行文件加载到内存中

关于OC和runtime的关系,我们可以看一下这个图:
其实OC是分两个版本
1、Objective-C 1.0,已经废弃了不用了
2、Objective-C 2.0,现在在使用的

image.png

从图中我们可以看到OC其实是对runtime进行了一层封装,归纳起来runtime有三种使用方式:
1、Objective-C code:例如@selector()
2、NSObject的方法:例如NSSelectorFromString()
3、自己的函数api:例如sel_registerName

二、探索方法的本质

下面我们来探索下方法的本质:

代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
//        test();
        BYTestCache_t *test = [BYTestCache_t alloc];
        Class tes = [BYTestCache_t class];
        [test by_eat5111];

        [test by_run1];
        
    }
    return 0;
}

首先我们通过xcode调试工具调出其汇编代码进行分析:
打开终端--cd到main.m目录下,用clang命令进行编译

clang -rewrite-objc main.m -o main.cpp

然后我们打开main.cpp文件拉到最下面,上面一堆代码都是为了运行环境进行的配置,暂时不用看
我们会发现[test by_eat5111]被编译成:

        ((void (*)(id, SEL))(void *)objc_msgSend)((id)test, sel_registerName("by_eat5111"));

去掉类型转换后,简化一下就可以表示成:

objc_msgSend(test,sel_registerName("by_eat5111"));

其中第一个参数test就是消息接受者,后面的sel_registerName我们可以在objc源码中搜到它的函数声明

OBJC_EXPORT SEL _Nonnull sel_registerName(const char * _Nonnull str)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

很明显这里是根据一个C字符串返回一个SEL,其实就等同于OC里面的@selector(),这两个参数都没什么太大疑问,然后我们来从源码里看一看能否找到objc_msgSend的实现。

注意:
这里我们发现objc_msgSend函数是由两个隐藏函数,self,和SEL

image.png

但最终,你无法在源码里面找到对应的C函数实现,因为在objc源码里面,是通过汇编来实现的,并且对应不同架构有不同的版本,这里我们就关注arm64版本的实现。

为啥苹果要用汇编来实现某些函数呢?主要原因是因为对于一些调用频率太高的函数或操作,使用汇编来实现能够提高效率。

在汇编里面,函数的入口格式是ENTRY + 函数名
入口,我们找到objc_msgSend的入口


image.png

汇编逻辑

然后梳理下汇编的逻辑:
汇编代码:

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    //对比p0寄存器是否为空,其中x0-x7是参数,x0可能会是返回值
    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
//如果是LNilOrTagged返回空
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    //p13是指针,[x0]是消息接收者
    ldr p13, [x0]       // p13 = isa
    //指针处理
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    //开始缓存查找指针
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    b.eq    LReturnZero     // nil check

    // tagged
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
    ubfx    x11, x0, #60, #4
    ldr x16, [x10, x11, LSL #3]
    adrp    x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
    add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
    cmp x10, x16
    b.ne    LGetIsaDone

    // ext tagged
    adrp    x10, _objc_debug_taggedpointer_ext_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
    ubfx    x11, x0, #52, #8
    ldr x16, [x10, x11, LSL #3]
    b   LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

LReturnZero:
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    ret
    //汇编中函数结束标示就是END_ENTRY + 函数名
    END_ENTRY _objc_msgSend

然后查找一下CacheLookup,看看缓存怎么查询的,注意,这里的NORMAL是参数

.macro CacheLookup

    // p1 = SEL, p16 = isa --- [x16意思是偏移16个字节得到cache_t,逻辑就是根据SEL从散列表buckets里面寻找方法
    ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
    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))
    //这一句递归,指针偏移的意思,找到了就返回{imp, sel} = *bucket
    ldp p17, p9, [x12]      // {imp, sel} = *bucket
    //这个地方是p9和P1进行对比
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    //b是跳转的意思 .ne是notEquel的意思  也就是如果p9和p1不匹配就跳转下面2,如果匹配就往下走
    b.ne    2f          //     scan more
//如果找到就调用并返回CacheHit,缓存命中,传的参数是$0,也就是CacheLookup的参数NORMAL
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket 没找到就进行CheckMiss操作,传的参数是$0,也就是CacheLookup的参数NORMAL
    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
    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

如果命中了,找到了方法我们就调用CacheHit函数,然后我们先看一下CacheHit函数


#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2

// CacheHit: x17 = cached IMP, x12 = address of cached IMP
.macro CacheHit
.if $0 == NORMAL//传进来的参数是NORMAL,所以调用TailCallCachedImp,直接将方法缓存起来然后进行调用就OK了
    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

如果没命中,就调用CheckMiss函数,我们再看一下CheckMiss函数:


.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL//传进来的是NORMAL,所以走这里
    cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

因为传进来的是NORMAL,所以再调用__objc_msgSend_uncached函数
我们查看一下__objc_msgSend_uncached,发现事直接进入的

    STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves
    
    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band r10 is the searched class
    //开始搜索方法列表
    // r10 is already the class to search
    MethodTableLookup NORMAL    // r11 = IMP
    jmp *%r11           // goto *imp

    END_ENTRY __objc_msgSend_uncached

__objc_msgSend_uncached中调用了MethodTableLookup,
再看一下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)]

    // 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函数,而这个函数在当前的汇编代码里面是找不到实现的。你去objc源码进行全局搜索,也搜不到,这里经过大佬指点,如果是一个C函数,在底层汇编里面如果需要调用的话,苹果会为其加一个下划线,因此上面的的函数删去一个下划线,_class_lookupMethodAndLoadCache3,你就可以在源码里面找到它对应的C函数,它是objc-runtime-new.mm里面的一个C函数

/***********************************************************************
* _class_lookupMethodAndLoadCache.
* Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp().
* This lookup avoids optimistic cache scan because the dispatcher 
* already tried that.
**********************************************************************/
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

而这个函数里面最终是调用了lookUpImpOrForward函数
并且lookUpImpOrForward里面调到了cache_fill,cache_fill里面调用的又是cache_fill_nolock,也就是上一篇的分析
iOS实例方法cache_t的缓存逻辑

总结:

1、OC方法经过xcode编译会变成一个objc_msgSend函数,函数是由汇编实现的,进入到汇编代码,
2、我们会发现在objc_msgSend中通过CacheLookup去查找函数的指针,并且在CacheLookup里面会递归从buckets里面查找方法,如果找到指针直接调用CacheHit 触发TailCallCachedImp进行执行方法,如果没找到就触发了CheckMiss,然后走慢速查找流程
3、在Checkmiss里面又调用了__objc_msgSend_uncached函数,在__objc_msgSend_uncached里面调用了MethodTableLookup,
4、在MethodTableLookup里面调用了__class_lookupMethodAndLoadCache3,该函数是由C语言编写的,
5、然后通过lookUpImpOrForward调用了cache_fill_nolock逻辑。

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