objc_msgSend 流程分析(快速查找)

在上一篇 cache_t 原理分析 分析了 cache 的写入流程,在写入之前还有 cache 的读取流程

runtime

runtime 即我们常说的 运行时,是 OC 底层的一套 C/C++ 的 API,编译器最终都会将 OC 代码转化为运行时代码,我们通过 clang -rewrite-objc xxx.m可以看到编译后的 .cpp 文件。

运行时 是相对于 编译时 而言

  • 运行时:代码跑起来,被装载到内存中的过程,如果代码有问题,会崩溃,是一个动态阶段

  • 编译时:是将源代码编译成计算机能识别的代码的过程,主要是检查语法、词法分析,是一个静态的阶段

交互方式

  • Objective-C 代码:例如 [person say666];

  • NSObject 方法: 例如 isKindOfClassisMemberOfClass

  • Runtime API:例如 class_getInstanceSize

compiler 就是我们熟知的编译器,即 LLVMruntime system libarary 就是底层库

方法的本质

准备环境
  • 创建一个 LCPerson 类继承自 NSObject,添加一个实例方法
@interface LCPerson : NSObject

-(void)say666;
    
@end

@implementation LCPerson

-(void)say666 {
    NSLog(@"say666");
}

@end
  • main 函数中创建 LCPerson 的实例,并调用实例方法
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        LCPerson *person = [LCPerson alloc];
        [person say666];
        
    }
    return 0;
}
  • 通过 Clang 命令,将 main.m 文件编译成 main.cpp 文件,找到 main 函数的实现代码
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

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

    }
    return 0;
}

可以看到,LCPerson 类调用 alloc 类方法以及对象调用 say666 实例方法,最终都会转化为 objc_msgSend 消息发送,由此,我们可以判断方法的本质就是消息发送。

验证
  • 我们可以通过调用 objc_msgSend 方式调用,查看与对象直接调用结果是否一致
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        LCPerson *person = [LCPerson alloc];
        [person say666];
        
        objc_msgSend(person, sel_registerName("say666"));
        
    }
    return 0;
}
  • 打印结果

可以看到 [person say666] 与 objc_msgSend(person, sel_registerName("say666")) 是一致的

注意:
直接调用 objc_msgSend 的方法会报错,需要导入头文件 #import <objc/message.h>,此时编译还是会报错,需要将严厉检查机制设置为 NO

调用父类方法

我们再创建一个 LCSon 类,继承自 LCPerson 类,并声明 -(void)say666 实例方法,不实现

@interface LCSon : LCPerson

-(void)say666;
    
@end

@implementation LCSon

@end
  • 通过调用 objc_msgSendSuper 方式调用,查看与子类对象直接调用结果
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        LCSon *son = [LCSon alloc];
        [son say666];
        
        struct objc_super lcsuper;
        lcsuper.receiver = son;
        lcsuper.super_class = [LCPerson class];
        
        objc_msgSendSuper(&lcsuper, sel_registerName("say666"));
        
    }
    return 0;
}
  • 打印结果

我们发现 [son say666]objc_msgSendSuper 执行的都是父类中 say666 的实现,由此我们可以猜测,方法调用,首先是在类中查找,如果类中没有找到,会到类的父类中查找。

拓展-objc_msgSendSuper

上面我们用到了 objc_msgSendSuper 调用,我们看下它的源码定义

OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
#endif

有两个参数(objc_super 结构体,sel 方法名),其结构体类型是objc_super定义的结构体对象

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

从源码我们可以知道 objc_super 有两个成员 receiver(消息接收者)以及 super_class (父类)。这个是告诉编译器,优先从父类中查找方法实现;直接调用方法是优先从子类中找方法实现。

objc_msgSend

在 objc-781 源码中查找 objc_msgSend,发现都是用汇编实现的,汇编的特性

  • 快:更容易被机器识别
  • 参数的动态性:汇编调用函数时传递的参数是不确定的

消息查找机制

  • 快速查找:通过 cache 缓存查找
  • 慢速查找:methodList 中查找,查找不到会走消息转发流程

源码分析

在 objc4-781 源码中我们在 objc-msg-arm64.s 文件找到 ENTRY _objc_msgSend 入口部分

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    //p0 是传入的第一个参数:消息的接收者。
    //cmp(compare) p0与nil比较,如果p0为空,那么就直接返回
    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
    //小对象类型
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    //消息接收者(p0)为空,返回空
    b.eq    LReturnZero
#endif
    //从x0寄存器指向的地址 取出消息接收者的 isa,存入 p13寄存
    ldr p13, [x0]       // p13 = isa
    //在 64 位架构下,p16 = isa(p13) & ISA_MASK,拿出shiftcls信息,得到class信息
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    //如果有isa,走到CacheLookup 即缓存查找流程,也就是所谓的sel-imp快速查找流程
    CacheLookup NORMAL, _objc_msgSend

#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 _objc_msgSend
1. 消息接收者(receiver)是否为空
  • 如果支持 tagged pointer,跳转至 LNilOrTagged
    • 如果小对象为空,直接返回空(LReturnZero)
    • 不为空,继续下面流程
  • 如果不是小对象
    • 消息接收者(p0)为空,返回空(LReturnZero)
    • receiver 中取出 isa 存入 p13 寄存器
    • 通过 GetClassFromIsa_p16 得到 class 信息

GetClassFromIsa_p16 的汇编代码如下

.macro GetClassFromIsa_p16 /* src */
//此处用于watchOS
#if SUPPORT_INDEXED_ISA 
    // Indexed isa
//将isa的值存入p16寄存器
    mov p16, $0         // optimistically set dst = src 
    //判断是否是 nonapointer isa
    tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f  // done if not non-pointer isa 
    // isa in p16 is indexed
//将_objc_indexed_classes所在的页的基址 读入x10寄存器
    adrp    x10, _objc_indexed_classes@PAGE 
//x10 = x10 + _objc_indexed_classes(page中的偏移量) x10基址 根据 偏移量 进行 内存偏移
    add x10, x10, _objc_indexed_classes@PAGEOFF
//从p16的第ISA_INDEX_SHIFT位开始,提取 ISA_INDEX_BITS 位 到 p16寄存器,剩余的高位用0补充
    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
//p16 = class = isa & ISA_MASK(位运算 & 即获取isa中的shiftcls信息)
    and p16, $0, #ISA_MASK 

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

#endif

.endmacro
2. CacheLookup 缓存查找

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
    //#define CACHE (2 * __SIZEOF_POINTER__),其中 __SIZEOF_POINTER__表示pointer的大小 ,即 2*8 = 16
    //p11 = mask|buckets,从 x16(isa)中平移16字节,得到 cache,存入p11。objc_class 结构,偏移 isa(8字节)+ superClass(8字节)就是 cache(mask高16位 + buckets低48位)
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    //p11(cache) & 0x0000ffffffffffff ,高16位抹零,得到 buckets,存入p10寄存器
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
    //p11(cache)右移48位,得到 mask,mask&p1(sel,msgSend的第二个参数 cmd),就会得到 sel-imp的下标 index,存入p12
    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

    //define PTRSHIFT 3
    //p12是下标,p10是 buckets 的首地址,下标左移16位(1<<4)得到实际内存的偏移量,通过buckets的首地址偏移,获取bucket存入p12寄存器
    //LSL #(1+PTRSHIFT)-- 实际含义就是得到一个bucket占用的内存大小 -- 相当于mask = occupied -1-- _cmd & mask -- 取余数
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
    //从x12(即p12)中取出 bucket 分别将imp和sel 存入 p17(存储imp) 和 p9(存储sel)
    ldp p17, p9, [x12]      // {imp, sel} = *bucket
//比较 sel 与 p1
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    //不相等,跳转到2f
    b.ne    2f          //     scan more
    //相等,即 cacheHit 缓存命中,直接返回imp
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    //如果一直都找不到, 因为是normal ,跳转至__objc_msgSend_uncached
    CheckMiss $0            // miss if bucket->sel == 0
    //判断p12(下标对应的bucket) 是否 等于 p10(buckets数组第一个元素)
    cmp p12, p10        // wrap if bucket == buckets
    //如果等于,则跳转至第3f
    b.eq    3f
    //从x12(即p12 buckets首地址)- 实际需要平移的内存大小BUCKET_SIZE,得到得到第二个bucket元素,imp-sel分别存入p17-p9,即向前查找
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    //跳转至第1步,继续对比 sel 与 cmd
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    //人为设置到最后一个元素,p11(mask)右移44位 相当于mask左移4位,直接定位到buckets的最后一个元素,缓存查找顺序是向前查找
    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.
    //再查找一遍缓存,拿到x12(即p12)bucket中的 imp-sel 分别存入 p17-p9
    ldp p17, p9, [x12]      // {imp, sel} = *bucket
//比较 sel 与 p1(传入的参数cmd)
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    //如果不相等,即走到2f
    b.ne    2f          //     scan more
    //如果相等 即 CacheHit,直接返回imp
    CacheHit $0         // call or return imp

//如果一直找不到,即CheckMiss
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    //判断p12(下标对应的bucket) 是否 等于 p10(buckets数组第一个元素)表示前面已经没有了,但是还是没有找到
    cmp p12, p10        // wrap if bucket == buckets
    ////如果等于,跳转至第3步
    b.eq    3f
    //从x12(即p12 buckets首地址)- 实际需要平移的内存大小BUCKET_SIZE,得到得到第二个bucket元素,imp-sel分别存入p17-p9,即向前查找
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    //跳转至第1步,继续对比 sel 与 cmd
    b   1b          // loop

LLookupEnd$1:
LLookupRecover$1:
3:  // double wrap
    //跳转至JumpMiss 因为是normal ,跳转至__objc_msgSend_uncached
    JumpMiss $0

.endmacro

源码解析

    1. 获取 cache

通过 isa 首地址平移 16 字节(在 objc_class 结构中,isa 占8字节,superClass 占8字节) ,即 p11 = cache

    1. 计算下标
    • 通过上一步获取的 cache &(与上)掩码(0x0000ffffffffffff)的到 buckets,(cache 首地址存储的是 ‘高16位存mask,低48位存buckets’ ),
      即 p10 = buckets
    • cache 右移48位,得到 mask,即 p11 = mask
    • p1 是 objc_msgSend 的第二个参数(_cmd),p1 & mask,通过哈希算法,得到需要查找存储 sel-imp 的 bucket 下标 index(在存储 sel-imp 时,也是通过同样哈希算法计算哈希下标进行存储,所以读取也需要通过同样的方式读取)
    1. 根据 下标buckets 去除对应的 bucket
    • 一个 bucket 实际占用16字节(sel 占8字节,imp 占8字节),左移4位(2^4 = 16)
    • 通过 首地址 + 实际偏移量,获取哈希下标 index 对应的 bucket
    1. 根据获取的 bucket,取出其中的 imp 存入p17,即 p17 = imp,取出 sel 存入 p9,即 p9 = sel
    1. 第一次递归循环
    • 比较获取的 bucketselobjc_msgSend 的第二个参数的 _cmd (即p1)是否相等
    • 相等,直接跳转至 CacheHit,即缓存命中,返回 imp
    • 不相等,分两种情况
      • 一直都找不到,直接跳转至 CheckMiss,进入慢速查找流程
      • 如果根据 index 获取的bucket 等于buckets 的第一个元素,则人为的将当前 bucket 设置为 buckets 的最后一个元素(通过buckets首地址+mask右移44位(等同于左移4位)直接定位到 bucker 的最后一个元素),然后继续进行递归循环(第一个递归循环嵌套第二个递归循环)
      • 如果当前 bucket 不等于 buckets 的第一个元素,则继续向前查找,进入第一次递归循环
    1. 第二次递归循环
    • 重复 5 的操作,与第一次递归唯一区别是,如果当前的 bucket 还是等于 buckets 的第一个元素,则直接跳转至 JumpMiss,进入慢速查找流程

整体流程图

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