iOS底层探索

iOS 探索objc_msgSend

2020-09-22  本文已影响0人  Sheisone

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这两个函数,他们的都有两个参数:

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

#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;
}
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中,方法查找有两种实现方式:

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个步骤。接下来我们具体分析下:

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的地址。

同样的,我们知道在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

在前面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

#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中。

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

上一篇下一篇

猜你喜欢

热点阅读