iOS底层 - objc_msgSend快速查找流程分析

iOS开发底层探究之路

上篇文章我们对cache原理进行了分析,在摸清cache是如何将方法信息存放进去后,我们来研究研究怎么取出所存储的方法信息,那么本文将从objc_msgSend入手,探究如何快速获取到方法信息。

编译时 & 运行时(Runtime)

  • 编译时:顾名思义就是正在编译的时候,就是编译器帮你把源代码翻译成机器能识别的代码。(当然只是一般意义上这么说,实际上可能只是翻译某个中间状态语言)做一些词法分析语法分析及帮你检查代码错误的过程。可理解为一个静态过程。
  • 运行时:就是代码在内存中跑起来的整个过程。因为编译过的代码只是保存在磁盘上,并没有装入内存中,而且运行时所做的一些检查判断工作与编译时不一样。可理解为一个动态过程。
编译时&运行时及代码结构图

上图可以看出,代码层与Runtime底层库之间有一个编译层,在我们代码运行时候,我们所进行的操作,比如调用方法,给属性赋值等操作时,上层代码与在经过编译后是不一样,就像之前碰到过的alloc方法,编译器处理过就变为objc_alloc,及isKindOfClass方法变成了objc_opt_isKindOfClass
我们可以利用上层代码使用到底层Runtime:

  • [person SomeMethod] :对象方法调用
  • isKindOfClassFrameworks&Service
  • class_getInstaceSizeRuntime API

例子探究方法本质

创建一个类LGPerson,添加两个方法sayHellosayNB:

@interface LGPerson : LGTeacher
- (void)sayHello;
- (void)sayNB;
@end

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

只实现sayNB方法,暂不实现sayHello方法。在main.m文件的 main方法中初始化person对象并调用sayHellosayNB方法:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
       LGPerson *person = [LGPerson alloc];
       [person sayNB];
       [person sayHello];
       NSLog(@"Hello, World!");
    }
    return 0;
}

此时,只编译项目的话,发现能编译成功,接着运行的话,项目就会崩溃,并且报错未在LGPerson类中找到sayHello方法实现。那么我们就能证明编译时代码不运行起来,也就不会检查是否方法有实现了,运行时会去检查是否方法已经实现了,也就是说在运行时找不到方法实现的话程序会崩溃
objc_msgSend初探
接着我们使用clang编译main.cpp文件,通过查看main函数中方法调用的实现:

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

可以看出,不管是类方法还是实例方法,方法调用的本质就是objc_msgSend方法的调用。为验证这个,我们可以直接调用objc_msgSend方法,传入正确参数即可:

LGPerson *person = [LGPerson alloc];
objc_msgSend(person,sel_registerName("sayNB"));
[person sayNB];

1.引入头文件#import <objc/message.h>
2.target --> Build Settingenable strict checking of obc_msgSend callsYES 改为NO,不然会报错。

打印结果如下发现直接调用方法和利用objc_msgSend方法调用结果一致:

objc_msgSend与sayNB运行结果

方法isa走位再次探究
再添加一个LGPerson的父类LGTeacher类,并且让LGTeacher类实现sayHello方法:

@interface LGTeacher : NSObject
- (void)sayHello;
@end
@implementation LGTeacher
- (void)sayHello{
    NSLog(@"666");
}
@end
@interface LGPerson : LGTeacher
- (void)sayHello;
- (void)sayNB;
@end
@implementation LGPerson
- (void)sayNB{
    NSLog(@"666");
}
@end

此时编译并运行程序,发现程序不会奔溃,并能正常打印输出结果。我们可以猜测对象方法在此找不到就会去父类里面查找的猜想。
我们也可直接利用objc_msgSendSuper方法来调用父类方法:

objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
LGPerson *person = [LGPerson alloc];
LGTeacher *teacher = [LGTeacher alloc];
[person sayHello];

struct objc_super lgsuper;
 lgsuper.receiver = person;
 lgsuper.super_class = [LGTeacher class];
 objc_msgSendSuper(&lgsuper, sel_registerName("sayHello"));

发现直接person调用sayHello方法和利用objc_msgSendSuper方法,都能正常获取到方法的实现,正常输出打印结果。

objc_msgSend方法快速查找流程分析

objc4-781源码可执行工程中全局搜索objc_msgSend,在汇编文件
objc_msg_arm64.s中找到objc_msgSend的入口ENTRY

    ENTRY _objc_msgSend
//--- 无窗口
    UNWIND _objc_msgSend, NoFrame
//--- 判断当前p0和空对比,判断是否为nil,p0为当前方法第一个参数即消息接受者receiver
    cmp p0, #0          // nil check and tagged pointer check
//---支持taggedpointer(小对象类型)
#if SUPPORT_TAGGED_POINTERS
//--- le 小于 进入 LNilOrTagged 流程
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
//--- eq 等于0,直接返回 空
    b.eq    LReturnZero
#endif
//--- p0即receiver 到这里说明存在
//--- 根据对象拿出isa, 即从寄存器x0指向的地址 取出isa,存入 p13 寄存器
    ldr p13, [x0]       // p13 = isa
//--- 在拿到isa后再去获取当前class信息,利用 isa(p13) & ISA_MASK,拿出相应的isa中的shiftcls信息,即获得calss信息
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
//--- 缓存查找,也就是所谓的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
  • 首先判断当前消息接收者receiver是否存在,如果不存在就直接返回空
  • 判断是否支持Tagged_Pointers,支持的话跳转到LNilOrTagged
    • 如果等于0,则返回
    • 如果不为0,则对小对象的isa处理,并进入LGetIsaDone步骤
  • 如果不是小对象,且receiver存在,则获取到当前对象的isa,然后通过GetClassFromIsa_p16 获取到当前类信息
    arm64汇编根据isa获取类信息

在获取isa流程完毕后,接着就进入CacheLookup,进行查找过程:

.macro CacheLookup
LLookupStart$1:

    // p1 = SEL, p16 = isa
//--- #define CACHE            (2 * __SIZEOF_POINTER__) 即2 * 8 = 16
//--- p11 = mask|buckets -- 从x16(isa)中平移 16 字节,取出cache 存入p11寄存器 -- 因为isa(8字节)  superClass(8字节)  cache(16字节) (真机maskAndBuckets mask高16位 + buckets低48位)
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//--- 将p11 & 0x0000ffffffffffff 获取到buckets 信息 高位mask处 抹0
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
//--- p11(cache) 右移48位,前面48补0得到mask信息,然后p1(_cmd-sel)& 得到的mask信息,即获取到sel-imp 的下标index 存入p12
//--- cache存入sel-imp时候,也是按哈希算法 sel & mask下标存入的。
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask
//--- 非64位真机
#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

//--- p10 buckets数组首地址,p12 下标(_cmd & mask)<< 4,相当于 下标*16,再加上buckets首地址,获取到存放当前_cmd的bucket(不确定的,不要做对比确认的),存入 p12寄存器
//--- bucket(16) = sel(8) + imp(8),一个bucket占用的大小为16,首地址偏移 获取 当前的bucket
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) -- PTRSHIFT 等于 3

//--- 从x12中获取出当前bucket对应的sel 及 imp,分别存入 p17 和 p9
    ldp p17, p9, [x12]      // {imp, sel} = *bucket
//--- 比较 sel 与 p1(传入的sel)
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
//--- 如果当前的bucket里没有存放过sel\imp信息,就退出循环,因为 经过 hash算法 sel & mask 得到的下标位置 没有存放任何sel\imp信息的话,也就是说此位置没被别的占用,说明就没有缓存次方法sel\imp信息,退出循环
    CheckMiss $0            // miss if bucket->sel == 0
//--- 判断p12(当前下标对应的bucket首地址)是否等于p10(buckets数组第一个bucket的首地址)
    cmp p12, p10        // wrap if bucket == buckets
//--- 如果相等,跳转到第三步 3f 定位到最后一个bucket地址
    b.eq    3f
//--- 如果bucket != buckets
//--- 通过对此位置每次进行BUCKET_SIZE大小递减,即向前查找的方式,每次将对应的sel和imp存入p17 和 p9
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
//--- 获取到最新的bucket对应的sel和imp,然后回到 第一步,继续与参数_cmd比较
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//--- 人为设置查找位置为最后一个bucket
//--- mask = capacity - 1。
//--- p11(mask)右移44位,获取到最后一个bucket的位置
    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.
//--- 此时查找位置为最后一个bucket ,比较sel与p1(_cmd)
    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
//--- 比较此时bucket是否回到了第一个位置
//--- 判断是否是第一个bucket位置,还没找到,说明里面buckets里面根本就没找到 跳到下面 3f JumpMiss
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
//--- 向前查找规则,找到每一个bucket,然后回到第一步,继续进行sel 与 _cmd 对比
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

LLookupEnd$1:
LLookupRecover$1:
3:  // double wrap
//--- 找不到,只能去方法列表查找了
    JumpMiss $0

.endmacro
  • 此过程首先根据isa信息,经过地址偏移16获取到cache信息,因为isasuperClass分别为8字节大小,真机环境下获取到的cache 信息中为mask|buckets即 前面cache源码分析文章看到的_maskAndBuckets 数据。然后分别利用_maskAndBuckets数据分别经过获取到 bucketsmask , 利用哈希算法 _cmd & mask 获取下标。然后根据buckets首地址偏移 下标 * 16bucketsize = sel + imp = 16),p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) ,其中PTRSHIFT = 3(_cmd & mask) << (1+PTRSHIFT) 相当于下标 左移 4 ,即乘以 16,为 相对于buckets首地址的偏移量。所以最后加上buckets首地址的话,就获得了当前_cmd 对应的bucket

  • 接下来将当前获取到的bucket中的sel_cmd对比,如果相等,说明找到了,直接返回对应的imp,如果不想等,进入下一步2f

  • 首先判断如果此时的bucket中的sel = 0,即不存在sel、imp 信息,则说明根本就没有对此方法进行过缓存, 结束搜索流程

  • 如果当前bucketsel 不等于_cmd ,则说明此位置被占用了,所以找别的位置,先判断当前bucket是否是buckets中的第一个bucket, 如果是的话,进入3f流程,将查找位置移到buckets最后一个,然后以BUCKET_SIZE(16)大小递减,遵循向前查找的规则,遍历所有bucket,每获取到一个bucket,进行sel_cmd比较。

  • 如果当前bucket 不是第一个bucket的话,遵循向前查找的规则,比较每一个bucketsel_cmd,如果遍历到第一个还是没找到,那么继续将查找位置移到最后一个bucket位置,继续向前查找,最后第二次到第一个bucket时候,就说明所有的bucket都不符合,所以结束查找流程。

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