objc_msgSend流程分析

2020-09-21  本文已影响0人  深圳_你要的昵称

前言

书接上回cache_t缓存流程分析,我们知道方法的最终insert在_buckets(模拟器)_maskAndBuckets(arm64真机)中,这是方法的存储流程,那么方法的读取流程是怎么样的?今天我们通过方法的调用objc_msgSend一起来探究下方法的读取流程

1.Runtime知识点

我们都知道,OC这门编程语言,与其它语言不同,具有Runtime运行时这一特殊的能力,那么什么是运行时呢?先看看下面这个示例:

@interface LGPerson : NSObject

- (void)sayHello;

@end
----------------------------分割线----------------------------
@implementation LGPerson
- (void)sayHello{
    NSLog(@"LGPerson say : %s",__func__);
}
@end

Objective-C里面,方法的调用大致有三种方式:

  1. OC方式:先初始化一个实例LGPerson person = [[LGPerson alloc] init];,直接调用[person sayHello];
  2. NSObject方式:通过performSelector
    [person performSelector:@selector(sayHello)];
  3. 底层Runtime方式:通过objc_msgSend
    ((id(*)(struct objc_object *, SEL))objc_msgSend)((__bridge struct objc_object *)(person), @selector(sayHello));

调用代码如下:


调用示例.png

由此可见,方法的调用既能通过对象直接调用,也能通过NSObjectperformSelector,还能通过更底层的objc_msgSend,后面2个方式根本就不是类LGPerson里声明的方法,但是却能触发sayHello,很神奇,这个就是Runtime运行时的一个特点。

1.1 Runtime概念

什么是Runtime运行时?得和编译时区分来说:

1.2 Runtime结构图

RunTime结构图.jpg

方法调用

上述示例我们见证了Runtime的特点,那么方法调用时,调用的是底层c/c++的哪个函数呢?我们可以通过clang指令将OC的.m文件编译生成.cpp,看看对应的c++代码,例如:

clang -rewrite-objc main.m -o main.cpp

在生成的main.cpp中,搜索到的main方法就是:

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

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

        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));

        ((id (*)(id, SEL, SEL))(void *)objc_msgSend)((id)person, sel_registerName("performSelector:"), sel_registerName("sayHello"));

        ((id(*)(struct objc_object *, SEL))objc_msgSend)((__bridge struct objc_object *)(person), sel_registerName("sayHello"));

    }
    return 0;
}

上面可见,[person sayHello]performSelector底层都是通过调用objc_msgSend,跟之前搜索objc_objectcache_t一样,在源码工程全局搜索objc_msgSend,找一找方法的实现,根本找不到。既然c/c++层搜不到,那我们进入更底层汇编层,再看看,发现了

image.png
汇编走流程

下面我们以真机arm64为例,看看objc_msgSend汇编代码的大致流程。代码很长,我们分为一段段的看:

section 1
#if SUPPORT_TAGGED_POINTERS
    .data
    .align 3
    .globl _objc_debug_taggedpointer_classes
_objc_debug_taggedpointer_classes:
    .fill 16, 8, 0
    .globl _objc_debug_taggedpointer_ext_classes
_objc_debug_taggedpointer_ext_classes:
    .fill 256, 8, 0
#endif

其中SUPPORT_TAGGED_POINTERS宏定义的解释:

// Define SUPPORT_TAGGED_POINTERS=1 to enable tagged pointer objects
// Be sure to edit tagged pointer SPI in objc-internal.h as well.
#if !(__OBJC2__  &&  __LP64__)
#   define SUPPORT_TAGGED_POINTERS 0
#else
#   define SUPPORT_TAGGED_POINTERS 1
#endif

因为是真机__LP64__,所以值为0,后面的情况都不考虑SUPPORT_TAGGED_POINTERS

section 2
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame

cmp p0, #0           // nil check and tagged pointer check
b.eq    LReturnZero
/********************************************************************
 *
 * id objc_msgSend(id self, SEL _cmd, ...);
 * IMP objc_msgLookup(id self, SEL _cmd, ...);
 * 
 * objc_msgLookup ABI:
 * IMP returned in x17
 * x16 reserved for our use but not used
 *
 ********************************************************************/

p0就是self,可以理解是消息的接收者

section 3
        ldr p13, [x0]       // p13 = isa
        GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend
CacheLookup流程

搜索CacheLookup,得到.macro CacheLookup,这是定义的地方,详细代码也分片段释义:

CacheLookup --1
.macro CacheLookup

LLookupStart$1:

    // p1 = SEL, p16 = isa
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
    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  
CacheLookup --2
add p12, p10, p12, LSL #(1+PTRSHIFT)  // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

ldp p17, p9, [x12]
#if __LP64__
// true arm64

#define SUPPORT_TAGGED_POINTERS 1
#define PTR .quad
#define PTRSIZE 8
#define PTRSHIFT 3  // 1<<PTRSHIFT == PTRSIZE
CacheLookup --3 遍历
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

3:  // wrap: p12 = first bucket, w11 = mask
    add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
  1. 比较bucket->sel 和_cmd,不等去第2步;相等CacheHit $0(缓存命中,返回)
  2. 循环遍历:
.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL
    cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
  1. add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))-->p11(maskBuckets())右移48-(1+3)=44位,再跟第一次通过哈希算法的得到的下标p12,再次进行哈希算法 -->得到的是cache_t中的最后一个bucket。
CacheLookup --4
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
.macro JumpMiss
.if $0 == GETIMP
    b   LGetImpMiss
.elseif $0 == NORMAL
    b   __objc_msgSend_uncached
.elseif $0 == LOOKUP
    b   __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

此时$0 = NORMAL,进入__objc_msgSend_uncached流程-->慢速查找流程(后面分析)

总结

以上通过对objc_msgSend汇编代码的流程分部解读,大致了解了,方法调用是如何从cache_t中遍历寻找imp的一个过程,流程图如下:

objc_msgSend.jpg
上一篇下一篇

猜你喜欢

热点阅读