iOS 探索objc_msgSend

iOS开发中,我们常常会调用各种方法,既包括对象方法也包括类方法,那我们方法调用内部到底是如何实现的呢?我们今天就来一起探索一下。

一、objc_msgSendobjc_msgSendSuper

首先,创建工程,并新建一个LPPerson类,并添加一个对象方法和一个类方法。并在main.m中完成调用:

@interface LPPerson : NSObject
@property (nonatomic, copy) NSString *lgName;
@property (nonatomic, strong) NSString *nickName;

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


@interface LPSon : LPPerson

@end


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

然后我们使用clang编译器,将main.m编译成main.cpp看下其内部结构。因为代码很多,并且main在最后,所以我们直接滑到最后即可:

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


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

    }
    return 0;
}

可以看到,不管是对象方法还是类方法,包括alloc方法他们都是调用了一个叫做objc_msgSend的函数。它的字面意思就是消息发送,在Objc源码中进行全局查找:

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

我们看到有objc_msgSendobjc_msgSendSuper这两个函数,他们的都有两个参数:

  • 第一个参数:表示消息接收者
  • 第二个参数SEL:表示需要执行的方法

既然我们调用方法就是执行了消息发送,那我们是不是可以直接调用objc_msgSend或者objc_msgSendSuper呢?
我们实验一下:

  • 1、首先导入#import <objc/message.h>
  • 2、在main.m中添加以下代码:
#import <objc/message.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        LPPerson *person = [LPPerson alloc];
        [person sayHello];
        objc_msgSend(person,sel_registerName("sayHello"));
        [LPPerson sayHi];
        objc_msgSend(objc_getClass("LPPerson"),sel_registerName("sayHi"));
    }
    return 0;
}
  • 3、但是发现报错了:
image.png

这是因为系统默认开启的方法检查,我们需要手动关闭。在target下选中当前target,选择buildSetting,然后搜索msg,将Enable Strict Checking of objc_msgSend Calls设置为NO即可:

image.png

现在直接运行:

2020-09-22 16:13:47.379526+0800[44411:14029752] -[LPPerson sayHello]
2020-09-22 16:13:47.380326+0800[44411:14029752] -[LPPerson sayHello]
2020-09-22 16:13:47.380457+0800[44411:14029752] +[LPPerson sayHi]
2020-09-22 16:13:47.380536+0800[44411:14029752] +[LPPerson sayHi]

结果证明,直接通过objc_msgSend调用方法是可以的,objc_msgSendSuper也是一样的,又兴趣的同学可以自己试验一下。

总结:方法调用的本质就是消息发送,具体是调用runtime中objc_msgSendobjc_msgSendSuper函数来实现的。

那么objc_msgSendobjc_msgSendSuper中又是如何查找方法selimp呢?接下里我们就来从源码中一探究竟,因为objc_msgSendobjc_msgSendSuper内部逻辑实际是一样的,所以我们接下来主要分析objc_msgSend原理。

二、objc_msgSend原理

进入源码中,我们可以发现objc_msgSend是使用汇编实现的,这是因为汇编主要的特性是:
速度快:汇编更容易被机器识别。
方法参数的动态性:汇编调用函数时传递的参数是不确定的,那么发送消息时,直接调用一个函数就可以发送所有的消息:
而在iOS中,方法查找有两种实现方式:

  • 快速查找,从cache中查找,也就是我们前面讲到的cache_t中存储的缓存
  • 慢速查找,从methodList中查找以及消息转发,下一篇我们会讲到

Objc源码中搜索objc_msgSend,前面提到了objc_msgSend是基于汇编的,所以我们直接以.s结尾的文件,然后找到ENTRY _objc_msgSend即可:

image.png

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
        ///P0是objc_msgSend的第一个参数,即消息接受者,这里需要判断消息接受者是否为空
    cmp p0, #0          // nil check and tagged pointer check
 ///判断是支持tagged_pointer
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
//再次判断消息接受者是否为空
#else
    b.eq    LReturnZero
#endif
        ///获取当前消息接受者的isa
    ldr p13, [x0]       // p13 = isa
        ///获取当前消息接受者的class
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
        ///缓存中寻找imp
    CacheLookup NORMAL, _objc_msgSend

接下来,我们继续查看CacheLookup的源码:
全局搜索CacheLookup,同样找.s结尾的文件,如下图所示:

image.png

然后进入源码中:

.macro CacheLookup
LLookupStart$1:

    // p1 = SEL, p16 = isa    
        //第一步:通过内存平移16字节获取当前的mask_buckets
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        //第二步:获取buckets    通过p11 & 0x0000ffffffffffff 得到后48位 buckets
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
         //第三步:获取hash 搜索下标:逻辑右移48位 得到mask;然后p1 & mask给p12 得到hash存储的key
    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是获取到的下标,然后逻辑左移4位,再由p10(buckets)平移,得到对应的bucket保存到p12中
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
     ///第五步:1、将p12属性imp 和 sel分别赋值为p17 和 p9
    ldp p17, p9, [x12]      // {imp, sel} = *bucket
///第五步:2、判断当前bucket的sel和传入的sel是否相等
1:  cmp p9, p1          // if (bucket->sel != _cmd)
///第五步:3、如果不相同,则跳入2f
    b.ne    2f          //     scan more
///第五步:4、如果相同,命中缓存,直接返回imp
    CacheHit $0         // call or return imp
///第五步:5、 没有找到 进入2f 
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
///第五步:6、如果p12(在第四步获取到的bucket) == p10(在第二步获取到的buckets),说明p12指针已经到了buckets的首地址了。
    cmp p12, p10        // wrap if bucket == buckets
///第五步:7、如果相等 跳入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
///第五步:8、再将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.
///第五步:9、然后在继续查找,直到找到或者再次 bucket 与 buckets再次相等,跳出循环。
    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


上述流程大概分为5个步骤。接下来我们具体分析下:

  • 第一步:获取mask_buckets
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
......
}

前面我们已经分析过objc_class,知道其内部结构,所以我们在拿到当前类的首地址后,因为isasuperclass各占8个字节,所以我们在拿到当前类的首地址后,我们平移16个字节,即可获取到cache的地址。

  • 第二步:获取buckets

同样的,我们知道在arm64也就是真机中,cache的首地址是_maskAndBuckets,我们查看_maskAndBuckets的源码:

{
    uintptr_t buckets = (uintptr_t)newBuckets;
    uintptr_t mask = (uintptr_t)newMask;
    
    ASSERT(buckets <= bucketsMask);
    ASSERT(mask <= maxMask);
    //maskShift 是 48 
    //将mask左移48位只留下16位,剩余的补0,
    _maskAndBuckets.store(((uintptr_t)newMask << maskShift) | (uintptr_t)newBuckets, std::memory_order_relaxed);
    _occupied = 0;
}

通过源码我们可以发现,mask有左右48位,所以·高16位 | 低48位 = mask | buckets
因此,我们将p11 & 0x0000ffffffffffff获取到低48位,即buckets

  • 第三步:获取hash 搜索下标

在前面cache_t我们有分析到,方法存储到cache中,是使用hash算法存储,其中开始下标则是 sel & mask

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

所以我们要拿到下标,就需要分别拿到masksel

  • mask:上面有看到在_maskAndBucketsmask左移48位,所以我们要取到mask,只需要_maskAndBuckets右移48位即可

  • selobject_msgSend中传入的两个参数,第一个是消息接受者,即isa,也就是P0。第二个就是sel,即P1

  • 第四步:根据下标找到对应的bucket
#if __arm64__

#if __LP64__
// true arm64

#define SUPPORT_TAGGED_POINTERS 1
#define PTR .quad
#define PTRSIZE 8
#define PTRSHIFT 3  // 1<<PTRSHIFT == PTRSIZE
// "p" registers are pointer-sized
#define UXTP UXTX
...

搜索源码找到PTRSHIFT,发现它是一个宏定义,值是3。而我们知道,buckets是一个数组,如果想得到数组中的元素 我们可以根据首地址进行指针平移获取到对应下标的值。

将第三步获取的P12开始下标 逻辑左移4位 或者 可以理解为 bucket是有selimp两个属性组成,每个属性都是8个字节的大小,所以bucket的大小是16。

buckets指针平移上一步得到的值,然后将平移后的bucket存到p12中。

  • 第五步:根据bucket中的sel查找
    • 1、将bucket中的属性属性impsel分别赋值为p17p9
    • 2、判断当前bucketsel和传入的sel是否相等:如果相等返回对应imp=>p17;不相等进入2f。
    • 3、此时是不相等,2f部分,这是一个循环。由于汇编中的查找是向上查找,所以p12-1获取到上一个bucket指针。如果当前p12 bucketbuckets的首地址(第一个元素)相等,那么就直接跳入3f部分。
    • 4、此时是p12 bucketbuckets的首地址(第一个元素)相等,3f部分。
    • 5、maskbuckets数组的个数减一,将mask左移4位,
    • 6、将buckets首地址地址平移上一步的结果,就到了buckets的最后一位,再将buckets最后一位的指针地址赋值给p12
    • 7、然后在继续进行比较sel,如果有相等就返回相应的imp,如果没有相等则就继续向上查询。
    • 8、 如果p12又一次指到的首地址,那么说明整个buckets中不存在方法sel,则退出循环,并返回
      具体流程可以参考下图:
      objc_msgSend流程分析.png

觉得不错记得点赞哦!听说看完点赞的人逢考必过,逢奖必中。ღ( ´・ᴗ・` )比心

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