Runtime底层原理探究(一) --- 消息发送机制(快速查找)

1,219 阅读9分钟

OC是一门动态语言所有的方法都是由运行时进行。 Objective-C 语言将决定尽可能的从编译和链接时推迟到运行时。只要有可能,Objective-C 总是使用动态 的方式来解决问题。这意味着 Objective-C 语言不仅需要一个编译器,同时也需要一个运行时系统来执行 编译好的代码。这儿的运行时系统扮演的角色类似于 Objective-C 语言的操作系统,Objective-C 基于该系统来工作。 Runtime的作用是 能动态产生/修改一个类,一个成员变量,一个方法

Runtime调用有三种方式

  1. NSObject(peformselector)
  2. Selector(底层会转换为objc_msgSend())
  3. Runtime的Api

objc_msg_send()

我们知道OC的函数调用是消息发送机制,那么消息发送机制是如何实现的呢。

Animals * animal = [[Animals alloc]init];
[animal eat];

将该文件编译成c++文件通过
clang-rewrite-objc 文件名 -o test.c++
命令 一共9w多行代码只需看最后

// -(void) eat;
/* @end */

#pragma clang assume_nonnull end

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

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_b2_hs7ds2bd5zz7d752kk495bhw0000gn_T_main_f668c6_mi_0);

        Animals * animal = ((Animals *(*)(id, SEL))(void *)objc_msgSend)((id)((Animals *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Animals"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)animal, sel_registerName("eat"));

    }
    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

objc_msgSend(void /* id self, SEL op, ... */ ) 当类初始化时候 显示获取 id self,即 (id)objc_getClass("Animals"),就是根据类名取获取这个类,然后alloc,init就是 #selector(alloc) 其底层实现是 sel_registerName("alloc/init"),其目的就是为了查找该类里面有没该方法 第二句同理target是已经生产的animal selector是 eat方法 sel_registerName("eat")去类的内存布局中查找eat方法

objc_msgsend 底层实现有两种方法一中是快速查找一种是慢速查找 快速是通过汇编从响应的缓存里面找到,慢速是通过c,c++以及汇编一起完成的。

之所以使用汇编的原因是 :

  1. c里面不会写一个函数保留未知的参数跳转到任意的指> 针,c无法实现,汇编可以通过寄存器直接实现
  2. 快,下层编译

快速查找

快速查找直接通过 汇编 + 缓存 来进行查找的 缓存是来自于类、

///ps: 类继承于对象从这里也可以看出来类其实也是一个对象
struct objc_class: objc_objcet {
  // class ISA;
  Class superclass;
  cache_t cache; /// 
  classs_data_bits_t bitgs; /// 类里面所有的数据
  
  class_rw_t *data() {
      return bits.data()
  }
}

类结构里的 cacle_t 缓存 存储方法的Selector(在iOS中SEL就是可以根据一个SEL选择对应的方法IMP。SEL只是描述了一个方法的格式)IMP(一个函数指针,这个被指向的函数包含一个接收消息的对象id(self 指针), 调用方法的选标 SEL (方法名),以及不定个数的方法参数,并返回一个id。也就是说 IMP 是消息最终调用的执行代码,是方法真正的实现代码 。我们可以像在C语言里面一样使用这个函数指针。)。IMP和Selector会组成一张哈希表,通过哈希直接查找非常快,当查找第一个方法的时候第一步找到cache,如果里面有他会直接返回。如果没有会经历一个复杂的过程(慢速查找)。找到了会在里面存一份方便下次进行查找,这次主要介绍快速找找的过程通过OC源码

刚刚的方法通过Xcode调试调试汇编页面

在源码里搜索_objc_msgsend

先把完整的汇编源码贴上,可以往下看,然后在回来看

********************************************************************
 *
 * 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
 *
 ********************************************************************

    .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

    ENTRY _objc_msgSend ///************************************** 1.进入objcmsgSend
    UNWIND _objc_msgSend, NoFrame
    MESSENGER_START
    /// x0 recevier
    // 消息接收者  消息名称
    cmp x0, #0          // nil check and tagged pointer check
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative) //// ****************************************************2.isa 优化
    ldr x13, [x0]       // x13 = isa
    and x16, x13, #ISA_MASK // x16 = class  
LGetIsaDone: ///**************************************************** 3.isa优化完成
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached ///*******************************************4.执行 CacheLookup NORMAL

LNilOrTagged:
    b.eq    LReturnZero     // nil check

    /// tagged
    mov x10, #0xf000000000000000
    cmp x0, x10
    b.hs    LExtTag
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
    ubfx    x11, x0, #60, #4
    ldr x16, [x10, x11, LSL #3]
    b   LGetIsaDone

LExtTag:
    // ext tagged
    adrp    x10, _objc_debug_taggedpointer_ext_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
    ubfx    x11, x0, #52, #8
    ldr x16, [x10, x11, LSL #3]
    b   LGetIsaDone
    
LReturnZero:
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    MESSENGER_END_NIL
    ret

    END_ENTRY _objc_msgSend


    ENTRY _objc_msgLookup
    UNWIND _objc_msgLookup, NoFrame

    cmp x0, #0          // nil check and tagged pointer check
    b.le    LLookup_NilOrTagged //  (MSB tagged pointer looks negative)
    ldr x13, [x0]       // x13 = isa
    and x16, x13, #ISA_MASK // x16 = class  
LLookup_GetIsaDone:
    CacheLookup LOOKUP      // returns imp

LLookup_NilOrTagged:
    b.eq    LLookup_Nil // nil check

    /// tagged
    mov x10, #0xf000000000000000
    cmp x0, x10
    b.hs    LLookup_ExtTag
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
    ubfx    x11, x0, #60, #4
    ldr x16, [x10, x11, LSL #3]
    b   LLookup_GetIsaDone

LLookup_ExtTag: 
    adrp    x10, _objc_debug_taggedpointer_ext_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
    ubfx    x11, x0, #52, #8
    ldr x16, [x10, x11, LSL #3]
    b   LLookup_GetIsaDone

LLookup_Nil:
    adrp    x17, __objc_msgNil@PAGE
    add x17, x17, __objc_msgNil@PAGEOFF
    ret
    END_ENTRY _objc_msgLookup
    STATIC_ENTRY __objc_msgNil
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    ret
    
    END_ENTRY __objc_msgNil


    ENTRY _objc_msgSendSuper
    UNWIND _objc_msgSendSuper, NoFrame
    MESSENGER_START

    ldp x0, x16, [x0]       // x0 = real receiver, x16 = class
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

    END_ENTRY _objc_msgSendSuper

    // no _objc_msgLookupSuper

    ENTRY _objc_msgSendSuper2
    UNWIND _objc_msgSendSuper2, NoFrame
    MESSENGER_START

    ldp x0, x16, [x0]       // x0 = real receiver, x16 = class
    ldr x16, [x16, #SUPERCLASS] // x16 = class->superclass
    CacheLookup NORMAL

    END_ENTRY _objc_msgSendSuper2

    
    ENTRY _objc_msgLookupSuper2
    UNWIND _objc_msgLookupSuper2, NoFrame

    ldp x0, x16, [x0]       // x0 = real receiver, x16 = class
    ldr x16, [x16, #SUPERCLASS] // x16 = class->superclass
    CacheLookup LOOKUP

    END_ENTRY _objc_msgLookupSuper2


.macro MethodTableLookup
    
    // push frame
    stp fp, lr, [sp, #-16]!
    mov fp, sp

    // save parameter registers: x0..x8, q0..q7
    sub sp, sp, #(10*8 + 8*16)
    stp q0, q1, [sp, #(0*16)]
    stp q2, q3, [sp, #(2*16)]
    stp q4, q5, [sp, #(4*16)]
    stp q6, q7, [sp, #(6*16)]
    stp x0, x1, [sp, #(8*16+0*8)]
    stp x2, x3, [sp, #(8*16+2*8)]
    stp x4, x5, [sp, #(8*16+4*8)]
    stp x6, x7, [sp, #(8*16+6*8)]
    str x8,     [sp, #(8*16+8*8)]

    // receiver and selector already in x0 and x1
    mov x2, x16
    bl  __class_lookupMethodAndLoadCache3/// *********************************************6.方法为_class_lookupMethodAndLoadCache3调用的汇编语言

    // imp in x0
    mov x17, x0
    
    // restore registers and return
    ldp q0, q1, [sp, #(0*16)]
    ldp q2, q3, [sp, #(2*16)]
    ldp q4, q5, [sp, #(4*16)]
    ldp q6, q7, [sp, #(6*16)]
    ldp x0, x1, [sp, #(8*16+0*8)]
    ldp x2, x3, [sp, #(8*16+2*8)]
    ldp x4, x5, [sp, #(8*16+4*8)]
    ldp x6, x7, [sp, #(8*16+6*8)]
    ldr x8,     [sp, #(8*16+8*8)]

    mov sp, fp
    ldp fp, lr, [sp], #16

.endmacro

    STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band x16 is the class to search
    
    MethodTableLookup /// ********************************************** 5.查找IMP
    br  x17

    END_ENTRY __objc_msgSend_uncached


    STATIC_ENTRY __objc_msgLookup_uncached
    UNWIND __objc_msgLookup_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band x16 is the class to search
    
    MethodTableLookup
    ret

    END_ENTRY __objc_msgLookup_uncached


    STATIC_ENTRY _cache_getImp

    and x16, x0, #ISA_MASK
    CacheLookup GETIMP

LGetImpMiss:
    mov x0, #0
    ret

    END_ENTRY _cache_getImp
  1. LLookup_NilOrTagged///针对内存里寄存器进行赋值处理isa优化。
  2. LGetIsaDone isa /// isa处理完毕
  3. CacheLookup Normal 调用当前imp或者发送objcmsgsend_uncache

1.CacheLookup Normal

先贴源码

/********************************************************************
 *
 * CacheLookup NORMAL|GETIMP|LOOKUP
 * 
 * Locate the implementation for a selector in a class method cache.
 *
 * Takes:
 *   x1 = selector
 *   x16 = class to be searched
 *
 * Kills:
 *   x9,x10,x11,x12, x17
 *
 * On exit: (found) calls or returns IMP
 *                  with x16 = class, x17 = IMP
 *          (not found) jumps to LCacheMiss
 *
 ********************************************************************/

#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2

.macro CacheHit /// cachehit
.if $0 == NORMAL /// normal ///call imp
    MESSENGER_END_FAST
    br  x17         // call imp
.elseif $0 == GETIMP
    mov x0, x17         // return imp
    ret
.elseif $0 == LOOKUP
    ret             // return imp via x17
.else
.abort oops
.endif
.endmacro

.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz x9, LGetImpMiss
.elseif $0 == NORMAL
    cbz x9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz x9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.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

.macro CacheLookup
    // x1 = SEL, x16 = isa
    ldp x10, x11, [x16, #CACHE] // x10 = buckets, x11 = occupied|mask
    and w12, w1, w11        // x12 = _cmd & mask
    add x12, x10, x12, LSL #4   // x12 = buckets + ((_cmd & mask)<<4)

    ldp x9, x17, [x12]      // {x9, x17} = *bucket
1:  cmp x9, x1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: x12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp x12, x10        // wrap if bucket == buckets
    b.eq    3f
    ldp x9, x17, [x12, #-16]!   // {x9, x17} = *--bucket
    b   1b          /// loop

3:  // wrap: x12 = first bucket, w11 = mask 
    add x12, x12, w11, UXTW #4  // x12 = buckets+(mask<<4)

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

    ldp x9, x17, [x12]      // {x9, x17} = *bucket
1:  cmp x9, x1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: x12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp x12, x10        // wrap if bucket == buckets
    b.eq    3f
    ldp x9, x17, [x12, #-16]!   // {x9, x17} = *--bucket
    b   1b          /// loop

3:  // double wrap
    JumpMiss $0
    
.endmacro

CacheLookup 有三种 NORMAL GETIMP LOOKUP

  1. CacheHit call imp /// 查找缓存
  2. CheckMiss - /// 找不到的处理 发送_objc_msgSend_uncached
  3. add /// 如果缓存里没有找到但是其他地方找到了这时候就可以add到缓存里面去

CacheHit 也是一个宏,如果$0 == Normal 则进行call imp 操作这是找到了操作。如果找不到的话,则执行check miss,check miss也是一个宏 $0 == Normal 会发送 objcmsgsend_uncache,这个时候整个流程就出来了。 CacheHit的意义就是要么查找IMP要么发送objcmsgsenduncache方法

2. _objc_msgSend_uncache

如果走到这里说明CacheHit并没有找到对应的方法而执行了_objc_msgSend_uncache /// 没有缓存去慢速查找imp

  STATIC_ENTRY __objc_msgSend_uncached
  UNWIND __objc_msgSend_uncached, FrameWithNoSaves

  // THIS IS NOT A CALLABLE C FUNCTION
  // Out-of-band x16 is the class to search
  
  MethodTableLookup /// 重点 方法列表
  br  x17  // call imp 

  END_ENTRY __objc_msgSend_uncached


  STATIC_ENTRY __objc_msgLookup_uncached
  UNWIND __objc_msgLookup_uncached, FrameWithNoSaves

  // THIS IS NOT A CALLABLE C FUNCTION
  // Out-of-band x16 is the class to search
  
  MethodTableLookup
  ret

  END_ENTRY __objc_msgLookup_uncached

MethodTableLookup 方法列表这个方法是关键, 因为 br x 17 是设置imp,而 MethodTableLookup 在之前调用说明他是在慢速查找。

3. MethodTableLookup

.macro MethodTableLookup
  
  // push frame
  stp fp, lr, [sp, #-16]!
  mov fp, sp

  // save parameter registers: x0..x8, q0..q7
  sub sp, sp, #(10*8 + 8*16)
  stp q0, q1, [sp, #(0*16)]
  stp q2, q3, [sp, #(2*16)]
  stp q4, q5, [sp, #(4*16)]
  stp q6, q7, [sp, #(6*16)]
  stp x0, x1, [sp, #(8*16+0*8)]
  stp x2, x3, [sp, #(8*16+2*8)]
  stp x4, x5, [sp, #(8*16+4*8)]
  stp x6, x7, [sp, #(8*16+6*8)]
  str x8,     [sp, #(8*16+8*8)]

  // receiver and selector already in x0 and x1
  mov x2, x16
  bl  __class_lookupMethodAndLoadCache3 /// 重点查找IMP

  // imp in x0
  mov x17, x0
  
  // restore registers and return
  ldp q0, q1, [sp, #(0*16)]
  ldp q2, q3, [sp, #(2*16)]
  ldp q4, q5, [sp, #(4*16)]
  ldp q6, q7, [sp, #(6*16)]
  ldp x0, x1, [sp, #(8*16+0*8)]
  ldp x2, x3, [sp, #(8*16+2*8)]
  ldp x4, x5, [sp, #(8*16+4*8)]
  ldp x6, x7, [sp, #(8*16+6*8)]
  ldr x8,     [sp, #(8*16+8*8)]

  mov sp, fp
  ldp fp, lr, [sp], #16

.endmacro

__class_lookupMethodAndLoadCache3 这个方法顾名思义是 查找方法列表并缓存,到了这里了我们发现源码里面并没有看到这个方法的定义。因为

__class_lookupMethodAndLoadCache3 方法为_class_lookupMethodAndLoadCache3调用的汇编语言 通过_class_lookupMethodAndLoadCache3 来到c++文件

快速查找总结

oc的方法调用本质是进行objc _ msgSend调用,而objcmsgSend进行实现的时候有两种方式一种是快速查找一种是慢速查找。快速查找是oc先去类结构里的cache_ t的类面去查找,里面是由 c c++ 和汇编一起完成的,采用会变得原因是他可以做到c语言无法完成的原因是c里面不会写一个函数保留未知的参数跳转到任意的指针,c无法实现,汇编可以通过寄存器直接保留,而且速度快,进入objc_ msg_ send的时候

  1. 首先会执行LLookup_NilOrTagged对isa进行优化,优化完毕后会执行LLookup_GetIsaDone.
  2. 之后执行CacheLookup进行缓存查找,缓存查找会分三步CacheHit如果是Normal,则直接返回imp说明缓存中有并返回(br x17),如果是GETIMP,LOOKUP,则会继续往下走。如果没有的话则执行checkmiss,Normal LOOKUP操作 发送objc_msgSend_uncahced消息,objc_msgSend_uncahced进入该方法后说明没有缓存进入慢速查找IMP,里面有一个MethodTableLookup,之所以这句是关键代码适应br x 17是为imp进行设置,所以该代码是关键,这方法里面执行**__class_lookupMethodAndLoadCache3**,因为该方法是**_class_lookupMethodAndLoadCache3**调用的汇编语言所以就查找到这个方法,该方法是c++文件里的代码

到了这里就已经进入到c++ 文件里面。下篇文章具体分析慢速查找流程

ps:以上为个人理解,如果有误欢迎指正。一起进步