objc_msgSend 流程之缓存查找

前言

有一定经验的iOS开发者大家都知道OC方法调用的本质就是消息的发送,那么发送消息后底层到底是如何查找到消息的呢?今天我们结合源码分析一下(本次探究源码基于objc781).

注:本文会有少许的汇编代码知识,不熟悉汇编的同学可以自行补充一下简单的汇编知识。

为什么要方法缓存?

通过前面文章cache_t分析的分析,我们知道,当我们的 OC项目在编译完成之后,类的实例方法(方法编号 SEL 和函数指针地址 IMP)会保存在类的cache_t的方法列表中,那么为什么要方法缓存,直接每次查找不好吗?

原来如果我们每次都要去类的方法列表或者父类、根类的方法列表里面去查询函数地址的话,必然会对性能造成极大的损耗,所以OC 为了实现其动态性,将 方法的调用包装成了SEL 寻找 IMP 的过程。

准备工作

依旧是老样子我们定义一个ZGPerson类,代码如下:

@interface ZGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSString *nickName;
- (void)sayHello;
@end

@implementation ZGPerson
- (void)sayHello{
    NSLog(@"%s",__func__);
}
@end

main.m中调用一下sayHello方法

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

我们进入main.m文件路径执行clang命令clang -rewrite-objc main.m -o main.cpp,将main.m编译成c++文件并查看,发现编译后的代码如下

#pragma clang assume_nonnull end

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


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

    }
    return 0;
}

这里我们可以看到调用方法的本质就是通过objc_msgSend方法给类发消息。

objc_msgSend方法分析

我们查看objc_msgSend的源码

/** 
 * Sends a message with a simple return value to an instance of a class.
 * 
 * @param self A pointer to the instance of the class that is to receive the message.
 * @param op The selector of the method that handles the message.
 * @param ... 
 *   A variable argument list containing the arguments to the method.
 * 
 * @return The return value of the method.
 * 
 * @note When it encounters a method call, the compiler generates a call to one of the
 *  functions \c objc_msgSend, \c objc_msgSend_stret, \c objc_msgSendSuper, or \c objc_msgSendSuper_stret.
 *  Messages sent to an object’s superclass (using the \c super keyword) are sent using \c objc_msgSendSuper; 
 *  other messages are sent using \c objc_msgSend. Methods that have data structures as return values
 *  are sent using \c objc_msgSendSuper_stret and \c objc_msgSend_stret.
 */
OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

下面上面的一长串简单的进行解释,当遇到一个方法调用时,编译器会根据调用类型生成一个底层函数:

  • 调用父类方法生成objc_msgSendSuper() 函数
  • 非父类方法生成objc_msgSend()函数
  • 如果返回值是数据结构,则使用 objc_msgSendSuper_stret或者 objc_msgSend_stret

objc_msgSend()是一个有两个默认参数id类型的selfSEL类型的_cmd (op),其中 self指向 消息接收者, _cmd方法选择器。如果需要传入更多的参数,可以拼接在这两个参数的后面。

我们在想进入源码中找objc_msgSend()的实现,发现已经点不进去了,我们在源码中搜索objc_msgSend(),发现在objc-msg-arm64.s中找到了objc_msgSend()汇编实现。

那么objc_msgSend()为什么用汇编实现呢?主要有一下两点原因:

  • 汇编更容易能被机器识别,效率更高(效率很重要)
  • C语言或者C++不能通过一个函数保留未知的参数并跳转任一未知的指针,而汇编可以

objc_msgSend汇编代码分析

我们以arm64架构的汇编代码为例进行分析
首先分析_objc_msgSendENTRY(入口)代码

ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    //p0代表传入的对象,cmp比较,这里是判断传入的对象是否为空
    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS     //是否是`tagged pointer`对象判断
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero    //直接return 0
#endif
    //获取传入对象的isa存入p13
    ldr p13, [x0]       // p13 = isa
    //获取传入对象的class类型存入p16
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    // 调用方法寻找imp或者objc_msgSend_uncached
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend

这里我在里面加了一些注释,这里主要对为什么获取isa进行下解释,是因为不管是对象方法还是类方法,我们都需要通过 isa 的指向 在类或元类的缓存或方法列表中去查找
下面是GetClassFromIsa_p16获取class的代码

.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

这里我们是arm64,直接通过and p16, $0, #ISA_MASK,即isa&mask得到类或元类 的信息

然后是调用方法CacheLookup寻找imp或者objc_msgSend_uncached,通过名字我们就可以猜出是去缓存中查找方法。

CacheLookup源码

.macro CacheLookup

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

这里的指令我们逐条解释:
ldr p11, [x16, #CACHE] // p11 = mask|buckets

注:
#define CACHE (2 * SIZEOF_POINTER)
#define CLASS SIZEOF_POINTER
x16 是我们上一步中获取到的 类信息x16偏移 16字节 就是取到cache_t 结构,存入 p11 中。

and p10, p11, #0x0000ffffffffffff // p10 = buckets

注:
我们首先解释一下_maskAndBuckets的结构,存储情况如下图

_maskAndBuckets

#0x0000ffffffffffff的二进制

可以看出 将p11#0x0000ffffffffffff进行与运算得到_maskAndBuckets0-47位即buckets,赋值给p10

and p12, p1, p11, LSR #48 // x12 = _cmd & mask

注:
LSP表示逻辑右移,将p11(_maskAndBuckets)右移48位得到_maskAndBucketsmask信息,然后与p1(_cmd)进行运算,赋值给p12
这里与我们上篇文章存方法的hash算法是一致的,目的是为了找到_cmdhash下标

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

注:
#define PTRSHIFT 3 // 1<<PTRSHIFT == PTRSIZE
LSL表示逻辑左移p10buckets数组的首地址,将_cmd&mask的结果左移4位,即向前偏移_cmd&mask位,假如_cmd&mask=3,即向前移3位,为什么是左移4位?因为一个bucket中包涵一个imp和一个sel,刚好16个字节,即左移4位。最后的结果(初始的bucket值)赋值给p12

ldp p17, p9, [x12] // {imp, sel} = *bucket

注:
ldp指令ldr/str的衍生, 可以同时读/写两个寄存器, ldr/str只能读写一个
p12selimp ,分别存入p9p17

1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp

注:
我们将上一步获取到的 sel 和我们要查找的 _cmd(进行比较,如果匹配了,就通过 CacheHitimp返回;如果没有匹配,跳转到2

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

注:
CheckMiss $0 如果从最后一个元素往前遍历都找不到缓存,那么走 CheckMiss方法
cmp p12, p10 判断当前查询的 bucket 是否为第一个元素,如果相等,跳转到3,否则--bucket继续向前遍历

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)

注:
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))buckets首地址+mask右移44位,直接定位到buckets的最后一个元素,然后继续向前查找,进行递归循环

在第二次查找时,会重复上面的步骤,只有在最后一步有所不同

3:  // double wrap
    JumpMiss $0

如果第二次查找,查找不到的话就JumpMiss $0
JumpMiss源码

.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

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

可见CheckMissJumpMiss差不多,此时的$0normal,会直接跳转至__objc_msgSend_uncached,即进入慢速查找流程

总结

最后的流程总结如下图


objc_msgSend缓存查找

好了,以上便是本篇文章的全部内容,如有不当之处,还望指正!慢速查找流程,我们后续会接着分析。

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