美文网首页
objc_msgSend流程分析

objc_msgSend流程分析

作者: 深圳_你要的昵称 | 来源:发表于2020-09-21 16:35 被阅读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
  • ENTRY进入send
  • UNWIND _objc_msgSend, NoFrame 可忽略,哈哈
  • cmp p0, #0 -->cmp是compare比较的意思,p0是第一个入参,#0代表nil,这句意思就是判断p0是否为nil,那第一个入参是什么?根据方法声明:
/********************************************************************
 *
 * 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,可以理解是消息的接收者

  • b.eq LReturnZero 如果没有接收者,则执行LReturnZero,直接终止流程return
section 3
        ldr p13, [x0]       // p13 = isa
        GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend
  • ldr p13, [x0],根据后面的注释知道,p13指向对象的isa指针ldr表示读取寄存器
  • GetClassFromIsa_p16 p13,同理根据注释,p16指向类class
  • LGetIsaDone:找到类class地址完成后
  • CacheLookup NORMAL, _objc_msgSend 跳转到了CacheLookup NORMAL流程
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  
  • ldr p11, [x16, #CACHE] 读取[x16, #CACHE],存入p11
    • [x16, #CACHE] 根据注释知道p16 = isa,应该是根据isa,指针偏移找到cache_t
    • 根据注释p11 = mask|buckets,因为amr64情况下cache_t结构体成员是explicit_atomic<uintptr_t> _maskAndBuckets;
  • 只看CACHE_MASK_STORAGE_HIGH_1664位真机的情况:
  • and p10, p11, #0x0000ffffffffffff
    • p11, #0x0000ffffffffffff --> p11 & #0x0000ffffffffffff,将16进制转10进制,意思就是前16位值为0,后48位值为1,与p11与运算,可以得到后48位的值
    • 将后48位的值存入p10,那么p10 = buckets()
  • and p12, p1, p11, LSR #48
    • p11, LSR #48 LSR表示逻辑右移,将p11逻辑右移48位,得到前16位,那么就是mask,
    • p1, mask-->因为p1 = SEL汇编对应的是_cmd,所以就是_cmd & mask
    • 最后就是p12 = (_cmd & mask),其实对应的就是方法cache_hash,得到哈希关键值key -->哈希下标索引值,
    static inline mask_t cache_hash(SEL sel, mask_t mask) 
    {
        return (mask_t)(uintptr_t)sel & mask;
    }
    
CacheLookup --2
add p12, p10, p12, LSL #(1+PTRSHIFT)  // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

ldp p17, p9, [x12]
  • p12, LSL #(1+PTRSHIFT):首先LSL表示逻辑左移, 搜索PTRSHIFT如下,那么#(1+PTRSHIFT)表示逻辑左移1+3 = 4位,那么p12 = ((_cmd & mask) << 4) -->哈希下标值左移4位,内存平移2^4 = 16 -->平移了一个bucket大小(_sel + _imp = 16)的下标
#if __LP64__
// true arm64

#define SUPPORT_TAGGED_POINTERS 1
#define PTR .quad
#define PTRSIZE 8
#define PTRSHIFT 3  // 1<<PTRSHIFT == PTRSIZE
  • 然后add p12, p10, p12, LSL #(1+PTRSHIFT) -- > 因为p10 = buckets()-->buckets()+平移了一个bucket的下标-->获取到这个bucket的值,存入p12
  • ldp p17, p9, [x12] 读取p12,将_imp->p17, _sel->p9
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. 循环遍历:
  • CheckMiss $0 如果从最后一个元素遍历过来都找到不到,就返回CheckMiss NORMAL,定义如下,进入__objc_msgSend_uncached(慢速查找流程)
.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
  • 否则比较p12(当前的bucket)p10(第一个bucket),如果相等去第3步,不等则ldp p17, p9, [x12, #-BUCKET_SIZE]!-->p12(当前的bucket)内存向前偏移一个bucket大小-->偏移后即前一个位置的bucket,还是将_imp->p17, _sel->p9
  • 然后b 1b进入循环。
  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
  • 重复CacheLookup --3 遍历的流程,唯一区别是当前指向的bucket == buckets时-->JumpMiss $0 -->__objc_msgSend_uncached(慢速查找流程)
  • JumpMiss流程如下:
.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

相关文章

网友评论

      本文标题:objc_msgSend流程分析

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