oc-底层原理之objc_msgSend方法快速查找

oc-底层原理分析之Cache_t一文中我们对方法的缓存进行了探讨,这篇文章我们在来研究一下方法的查找

方法的查找有两条线路:

  1. 快速查找(通过汇编实现)
  2. 慢速查找(通过c实现)(下一篇文章再来探究)

方法快速查找

方法的快速查找实际是通过缓存来查找,在探究之前,我们先来了解一下objc_msgSend,我们要知道方法的查找是在什么时机通过什么入口进入的

objc_msgSend

我们知道objective-c是一门动态语言,所有的方法调用并不是在编译阶段就确定的,当我们通过sel去查找imp的时候是在运行时才具体确定sel所对应的imp地址的,我们来看看下面的例子:
先定义两个类WPersonWTeacher,WTeacher继承自WPerson

@interface WPerson : NSObject
- (void)sayHello;
@end

@implementation WPerson
- (void)sayHello{
    NSLog(@"hello");
}
@end

@interface WTeacher : WPerson
- (void)sayHello;
- (void)sayNB;
@end

@implementation WTeacher
- (void)sayNB{
    NSLog(@"666");
}
@end

如果我们要调用sayNB方法,我们可以使用WTeacher的对象来调用:

WTeacher *teacher = [WTeacher alloc];
[teacher sayNB];

类的结构分析一文中我们通过Clang将类编译成.cpp文件后,我们可以看到类的结构中,方法的调用为:

WTeacher *teacher = ((WTeacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("WTeacher"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)teacter, sel_registerName("sayNB"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)teacter, sel_registerName("sayHello"));

我们可以看到任何方法的调用都是通过objc_msgSend来给对象发送消息的方式实现,那么我们直接调用objc_msgSend

objc_msgSend(teacher,sel_registerName("sayNB"));

我们也能实现方法的调用,但是如果当我们通过WTeacher调用sayHello方法的时候,实际上调用的是父类的sayHello方法,所以我们也可以直接向发送消息:

struct objc_super tsuper;
tsuper.receiver = teacher;
tsuper.super_class = [WPerson class];
    
objc_msgSendSuper(&tsuper, sel_registerName("sayHello"));

你也可以自己尝试一下。我们可以得出一个结论,

使用oc对象或者类来调用方法时实际上是通过objc_msgSend向对象发送消息

objc_msgSend源码分析

objc_msgSend调用方式为:

objc_msgSend(teacher,sel_registerName("sayNB"));

第一个参数为:消息接受者
第二个参数为:消息的sel

接下来我们研究一下objc_msgSend源码:

objc_msgSend源码有多个版本,armarm64i386模拟器,我们这里都以arm64为例讲解

arm64objc_msgSend的源码在objc-msg-arm64.s文件中

objc_msgSend
    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
   // p0是第一个参数:消息接受者
   // 这里先比较p0是否为空,如果为空,则直接返回
    cmp p0, #0  
    //是否支持  TAGGED_POINTERS
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
   //消息接受者为空,直接返回空
    b.eq    LReturnZero
#endif
  //消息接受者不为空
  //p13 获取消息接受者的isa并赋值给p13
    ldr p13, [x0]       // p13 = isa
    //根据获取到的isa获取class并赋值给p16
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    //isa获取结束,开始在cache中查找imp
    CacheLookup NORMAL, _objc_msgSend
CacheLookup

要理解这部分源码,需要先理解什么是cache_t,我们已经在oc-底层原理分析之Cache_t一文中进行了详细的探索,请先阅读这一部分

.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__) 
    // x16存储的是isa,平移16个字节后得到cache_t并赋值给p11
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
   // 获取buckets p11&0x0000ffffffffffff得到后48位,并赋值给p10
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
    // 获取hash,逻辑右移48位得到mask
    // 然后p1&mask 得到hash的key,并赋值给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

  // p12当前存储的是hash key,先逻辑左移4位然后再和p10相与,得到对应的bucket并保存在p12中
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
  //将p12属性的imp和sel分别赋值给p17和p9
    ldp p17, p9, [x12]      // {imp, sel} = *bucket
    //判断当前sel和传入的sel是否相等
1:  cmp p9, p1          // if (bucket->sel != _cmd)
  //如果不相等,跳入2f
    b.ne    2f          //     scan more
  //如果相等,跳入CacheHit
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    // 判断p12和p10是否相等 
    cmp p12, p10        // wrap if bucket == buckets
    //如果相等,跳入3f
    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
  //将p12指向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.

    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
CacheLookup源码详解

首先我们要知道类的结构,如下:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() const {
        return bits.data();
    }
    
    //这里的其他方法以及属性已经略去
}

我们知道objc_class中有以下几个属性:

  1. ISA
  2. superclass
  3. cache
  • ldr p11, [x16, #CACHE]p16存储的是isaCACHE为16个字节,p16平移16字节后就得到cache的地址,所以此时p11存储的是cache地址

  • and p10, p11, #0x0000ffffffffffff:在cache_t中,低48位存储的是buckets,高16位存储的是mask,用cache指针和0x0000ffffffffffff进行与运算以后,就得到低48位。也就是buckets,所以此时p10 = buckets

  • and p12, p1, p11, LSR #48:p11(cache)指针逻辑右移48位得到mask,然后再和p1(sel)相与,得到hash key
    要理解这一步,就需要了解cache的存储,我们 先看cache的insert方法中获取hash key的方法:

    static inline mask_t cache_hash(SEL sel, mask_t mask) 
    {
        return (mask_t)(uintptr_t)sel & mask;
    }
    

    我们理解了插入时如何生产hash key,那么这一 步也不难理解

  • add p12, p10, p12, LSL #(1+PTRSHIFT):PTRSHIFT = 3,p12当前是存储的hash key(实际上相当于index),bucket机构体中,包含两个元素 selimp,占用16个字节,p12逻辑左移4位,相当于 index * 16,然后p10再平移index * 16,得到对应的bucket,此时p12存储的是对应的buckets

  • add p12, p12, p11, LSR #(48 - (1+PTRSHIFT)):在循环查找中,如果当前bucket已经指向了cache的首地址(也就是buckets的地址),那么说明循环结束了,此时需要将p12指向buckets的最后一个元素

通过以上的快速查找流程,如果没有查到对应的imp,还会经过的慢速查找,关于慢速查找,下一篇文章会有介绍

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