美文网首页iOS底层探索
iOS 探索objc_msgSend

iOS 探索objc_msgSend

作者: Sheisone | 来源:发表于2020-09-22 23:08 被阅读0次

    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

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

    相关文章

      网友评论

        本文标题:iOS 探索objc_msgSend

        本文链接:https://www.haomeiwen.com/subject/wymwyktx.html