6.iOS底层之方法调用(objc_msgSend)

4,734 阅读7分钟

一、运行时runtime

1.1.什么是runtime?

runtime是一套由c、c++、汇编混合写成的,为oc提供运行时功能的api。

1.2.runtime版本

Objective-C运行时系统有两个版本:

  • 早期版本(legacy 1.0
  • 现行版本(modern 2.0

现行版本主要是Objective-C 2.0,目前主要研究的就是这个.

1.3.交互方式

Objective-C 程序有三种途径和运行时系统交互:

  • Objective-C代码: @selector()
  • NSObject的方法: NSSelectorFromString()
  • runtime函数: sel_registerName()

二、方法的本质探索

2.1、方法初探

有如下代码

 Person *person = [Person alloc];
 [person sayHello];
 
 run();

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

Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));

run();

去掉强制类型转换后:

objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
objc_msgSend(person, sel_registerName("sayHello"));

run();
  • 不管是alloc方法 还是sayHello方法 ,底层都是通过objc_msgSend函数来实现的。因此OC方法的本质就是通过objc_msgSend来发送消息。
  • objc_msgSend包含有方法的调用的两个隐藏参数:id self(消息接收者)和SEL sel(方法编号)。
  • sel_registerName等同于oc中的@selector()
  • c函数run 方法直接执行了,并没有通过 objc_msgSend 进行消息发送
2.2、方法调用(消息发送)的几种情况

实例方法的调用

LGStudent *s = [LGStudent new];
[s sayCode];     
//等同于如下
objc_msgSend(s, sel_registerName("sayCode"));

类方法的调用

id cls = [LGStudent class];
void *pointA = &cls;
[(__bridge id)pointA sayNB];
//等同于如下
objc_msgSend(objc_getClass("LGStudent"), sel_registerName("sayNB"));

父类实例方法的调用

struct objc_super lgSuper;
lgSuper.receiver = s;
lgSuper.super_class = [LGPerson class];
objc_msgSendSuper(&lgSuper, @selector(sayHello));

父类类方法的调用

struct objc_super myClassSuper;
myClassSuper.receiver = [s class];
myClassSuper.super_class = class_getSuperclass(object_getClass([s class]));// 元类
objc_msgSendSuper(&myClassSuper, sel_registerName("sayNB"));

使用 objc_msgSend 的时候,要需要将Xcodebuild setting中的 Enbale Strict of Checking of objc_msgSend Calls 设置为 NO。这样才不会报警告。(搜索objc_msgSend

三、objc_msgSend分析

3.0、objc_msgSend的快速查找流程是用汇编实现的,主要原因有
  • c语言不可能通过写一个函数来保留未知的参数并且跳转到一个人任意的函数指针。c语言没有满足做这件事情的必要特性。
  • 性能更高,汇编是更接近系统底层的语言。
3.1、_objc_msgSend

打开objc源码,搜索 objc_msgSend , 直接来到 objc-msg-arm64.sENTRY _objc_msgSend 中,源码如下

 ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame //没窗口
    //对比p0寄存器是否为空,其中x0-x7是参数,x0可能会是返回值
    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
//如果是LNilOrTagged返回空
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    //ldr是数据读取指令,将x0中的数据读取到p13中
    ldr p13, [x0]       // p13 = isa
    //根据isa拿到类
    GetClassFromIsa_p16 p13     // p16 = class  GetClassFromIsa_p16是一个宏,取面具,isa & ISA_MASK,得到当前类
LGetIsaDone:
    //开始缓存查找指针
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    b.eq    LReturnZero     // nil check

    // tagged
    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]
    adrp    x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
    add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
    cmp x10, x16
    b.ne    LGetIsaDone

    // 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
// SUPPORT_TAGGED_POINTERS
#endif

LReturnZero:
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    ret
    //汇编中函数结束标示就是END_ENTRY + 函数名
    END_ENTRY _objc_msgSend

此时对 isa 处理已经完成,已经找到当前类,接下来就是去缓存里面找方法,如果有直接返回对应的 impCacheLookup 的参数分为三种,NORMAL(正常的去查找) 、 GETIMP(直接返回 IMP) 和 LOOKUP(主动的慢速去查找)。 。

3.2、GetClassFromIsa_p16

GetClassFromIsa_p16源码如下

.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA   //苹果手表Watch支持
    // Indexed isa
    mov p16, $0         // optimistically set dst = src
    tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f  // done if not non-pointer isa
    // isa in p16 is indexed
    adrp    x10, _objc_indexed_classes@PAGE
    add x10, x10, _objc_indexed_classes@PAGEOFF
    ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
    ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:

#elif __LP64__   //64位系统
    // 64-bit packed isa
    and p16, $0, #ISA_MASK //p16 = $0 & #ISA_MASK

#else
    // 32-bit raw isa
    mov p16, $0

#endif

.endmacro

isa & ISA_MASK,得到当前类

3.3、CacheLookup 缓存查找

CacheLookup源码

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

.macro CacheLookup

    // p1 = SEL, p16 = isa --- 
    // x16代表 class,#CACHE 是一个宏定义 #define CACHE (2 * __SIZEOF_POINTER__),代表16个字节
    // class 平移 CACHE(也就是16个字节)得到 cache_t,然后将 cache_t里面的 buckets 和 occupied|mask 赋值给 p10和p11
    // 为什么 occupied|mask 两个值给了一个寄存器呢?因为 occupied|mask 都是只占4字节,而一个寄存器是8字节,这样赋值给一个寄存器节省内存
    ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
    and w11, w11, 0xffff    // p11 = mask
#endif

    and w12, w1, w11        // x12 = _cmd & mask   得到当前方法 hash 表的下标

    add p12, p10, p12, LSL #(1+PTRSHIFT)   // LSL 左移
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))  // 拿到buckets
   
    ldp p17, p9, [x12]      // {imp, sel} = *bucket   拿到bucket中的imp sel
    //这个地方是p9和P1进行对比  判断是否匹配到缓存
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    //b是跳转的意思 .ne是notEquel的意思  也就是如果p9和p1不匹配就跳转下面2,如果匹配就往下走
    b.ne    2f          //     scan more
//如果找到就调用并返回CacheHit,缓存命中,传的参数是$0,也就是CacheLookup的参数NORMAL
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket 没找到就进行CheckMiss操作,传的参数是$0,也就是CacheLookup的参数NORMAL
    CheckMiss $0            // miss if bucket->sel == 0
    //比较p12和p10 也就是比较取出的bucket和buckets首个元素
    cmp p12, p10        // wrap if bucket == buckets
    //如果相等 说明我们已经遍历完了buckets 去跳转执行3方法 
    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, w11, UXTW #(1+PTRSHIFT)
                                // p12 = buckets + (mask << 1+PTRSHIFT)

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.
//再查找一遍缓存 由于多线程的原因,可能当前调用的方法这时可能已经在别的线程调用结束了,也就是说现在可能缓存中已经有了方法的缓存了,这时我们再遍历一遍,也算是再给缓存查找一次机会吧
    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

3:  // double wrap
    JumpMiss $0
    
.endmacro

这一步是查找方法缓存,如果命中缓存就走CacheHit,没找到走CheckMiss

3.4、CheckMiss 没命中缓存

CheckMiss源码

.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

当前传的参数是NORMAL,走__objc_msgSend_uncached方法

3.5、__objc_msgSend_uncached

__objc_msgSend_uncached源码

STATIC_ENTRY __objc_msgSend_uncached
   UNWIND __objc_msgSend_uncached, FrameWithNoSaves
   //开始搜索方法列表
   MethodTableLookup
   TailCallFunctionPointer x17
   
   END_ENTRY __objc_msgSend_uncached

TailCallFunctionPointer x17若找到了 IMP 会放到 x17 寄存器中,然后把 x17 的值传递给 TailCallFunctionPointer 宏调用方法

3.6、MethodTableLookup 查找方法列表
.macro MethodTableLookup
  
  // push frame
  SignLR
  // 后面要跳转函数,意味着lr的变化,所以开辟栈空间后需要把之前的fp/lr值存储到栈上便于复位状态
  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 方法
  bl	__class_lookupMethodAndLoadCache3

  // IMP in x0
  mov	x17, x0
  
  // restore registers and return
  ...
  
  mov	sp, fp
  ldp	fp, lr, [sp], #16
  AuthenticateLR

.endmacro

__class_lookupMethodAndLoadCache3C函数,我们去掉一个下划线就能找到了(通过下断点+显示汇编的方式)

3.7、_class_lookupMethodAndLoadCache3

_class_lookupMethodAndLoadCache3源码

/***********************************************************************
* _class_lookupMethodAndLoadCache.
* Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp().
* This lookup avoids optimistic cache scan because the dispatcher 
* already tried that.
**********************************************************************/
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
   return lookUpImpOrForward(cls, sel, obj, 
                             YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

从这里开始,便从汇编进入到了C/C++。也就是真正的方法慢速查找流程。

四、总结

  • 方法的本质就是消息发送,消息发送是通过 objc_msgSend 以及其派生函数来实现的。
  • objc_msgSend是使用汇编写的,主要是速度够快,够灵活(C语言做不到写一个函数来保留未知的参数并且跳转到任意的函数指针)
  • objc_msgSend首先通过汇编快速查找方法缓存,如果查找不到则通过CheckMiss-> __objc_msgSend_uncached-> MethodTableLookup方法列表查找,最后通过_class_lookupMethodAndLoadCache3进行慢速查找并且缓存