美文网首页
iOS Objective-C 方法的本质

iOS Objective-C 方法的本质

作者: just东东 | 来源:发表于2020-09-17 10:37 被阅读0次

1.Runtime简介

1.1 Runtime

Runtime官方文档

作为一名iOS开发人员,说去Runtime一定都很熟悉,Runtime承载了Objective-C的动态特性,也是使Objective-C成为动态语言的根本。Runtime 是一套由C、C++和汇编编写的Api,其目的是为我们的OC提供动态特性,为了执行效率更高,稳定性更强,所以采用更接近于机器语言的C、C++和汇编。

1.2 Runtime Version

Runtime 两个版本分别是objc1(legecy),objc2(Modern),现行的版本主要是objc2,主要就是在底层源码中-new文件中的内容,同时也可以在宏__OBJC2__中得以体现。

1.3 Runtime 的使用方式:

  • Objective-C code @selector;
  • NSObject 的方法 NSSelectorFromString();
  • sel_registerName函数api

2. Objective-C方法的本质

2.1 通过clang查看

执行clang命令

clang -rewrite-objc main.m -o main.cpp
clang看方法编译后的样子
编译后我们可以看到,无论是allocinit还是我们的sayNB方法,除去前面的类型强转都是objc_msgSend,这个方法有两个参数,一个是消息的接收者为id类型,另一个是方法的编号sel

那么如果我们直接调用一个函数呢,下面我们再次通过clang编译后进行查看。

函数和OC方法的调用在编译后的对比

此时我们发现,如果直接调用函数则就是直接调用并没有出现objc_msgSend。C语言函数的调用并没有通过objc_msgSend进行消息发送。因为C语言函数是静态的,编译期间就已经确定了,函数地址也固定,所以函数的调用就是直接去找函数的地址调用即可。Objective-C作为一门动态语言,需要在运行时去动态的查找方法,所以需要消息发送,至此我们应该能够大概清楚一点,OC方法的本质就是消息的发送。

2.2 OC方法发送的几种情况

注意一点:在OC中使用objc_msgSend的时候,需要将Enbale Strict of Checking of objc_msgSend Calls设置为NO。这样才不会编译报错。

设置
  • 向对象s发送sayCode消息,这是我们最常用的,init一个对象,然后调用该对象的方法。
LGStudent *s = [LGStudent alloc];
[s sayCode];

objc_msgSend(s, sel_registerName("sayCode"));
  • LGStudent这个类的原类发送sayNB消息,这个也是我们经常用到的一种方式,类调用它的类方法。
[LGStudent sayNB];
objc_msgSend(objc_getClass("LGStudent"), sel_registerName("sayNB"));
  • 向父类LGPerson发送sayHello消息,这个也是我们平常应用很多的方式,就是调用父类的方法。
LGStudent *s = [LGStudent alloc];
struct objc_super lgSuper;
lgSuper.receiver = s;
lgSuper.super_class = [LGPerson class];
objc_msgSendSuper(&lgSuper, @selector(sayHello));
  • 向父类LGPerson的类,LGPerson的原类发送sayNB消息,这个就是平常我们调用父类的类方法的一种形式。
LGStudent *s = [LGStudent alloc];
struct objc_super myClassSuper;
myClassSuper.receiver = [s class];
myClassSuper.super_class = class_getSuperclass(object_getClass([s class]));// 元类
objc_msgSendSuper(&myClassSuper, sel_registerName("sayNB"));

3.objc_msgSend探索

注: 本文使用环境:

  • objc4-779.1
  • Xcode 11.5 (11E608c)

3.1 查找objc_msgSend的实现

经过全局搜索,我们发现objc_msgSend是用汇编实现的,而且根据不同架构有不同的版本,那么只能用比较抠脚的汇编知识去探索一下了,那么为什么要用汇编实现呢,应该是汇编更接近于机器语言容易被机器识别,OC 的动态特性导致参数和类型都是未知的,C语言中不可能通过写一个函数来保留未知的参数并且跳转到任意的函数指针,所以不能使用CC++

3.2 objc_msgSend实现原理

3.2.1 objc_msgSend开始的位置

注:本文分析的是objc4-779.1 arm64汇编的objc_msgSend的源码实现

首先通过搜索objc_msgSend找到其实现的位置,在objc-msg-arm64.s文件中,找到ENTRY _objc_msgSendENTRY是我们的汇编程序入口。

objc_msgSend源码

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    ldr p13, [x0]       // p13 = isa
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

步骤分析:

cmp p0, #0          // nil check and tagged pointer check

首先对p0进行非空校验,在汇编中我们通常会把第一个参数放入到第0个寄存器,objc_msgSend第一个参数其实就是我们的id self消息接收者。

ldr p13, [x0]       // p13 = isa
GetClassFromIsa_p16 p13     // p16 = class

非空校验通过后,将我们的x0存储到p13寄存器,然后调用GetClassFromIsa_p16这个方法。根据名字我们应该能想到,该方法的用意是通过isa获取类,方法存储在类中,要想找到方法就必须拿到类,无论是类还是原类。

GetClassFromIsa_p16源码

/********************************************************************
 * GetClassFromIsa_p16 src
 * src is a raw isa field. Sets p16 to the corresponding class pointer.
 * The raw isa might be an indexed isa to be decoded, or a
 * packed isa that needs to be masked.
 *
 * On exit:
 *   $0 is unchanged
 *   p16 is a class pointer
 *   x10 is clobbered
 ********************************************************************/

#if SUPPORT_INDEXED_ISA
    .align 3
    .globl _objc_indexed_classes
_objc_indexed_classes:
    .fill ISA_INDEX_COUNT, PTRSIZE, 0
#endif

.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA
    // 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-bit packed isa
    and p16, $0, #ISA_MASK

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

#endif

.endmacro

通过查看GetClassFromIsa_p16源码我们可以发现,其主要的作用就是通过isa与上ISA_MASK拿到类信息。

LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

最后获取到类后调用CacheLookup,下面我们开始分析CacheLookup

3.2.2 CacheLookupCacheHitCheckMissJumpMiss等分析

话不多说,先上源码看看

CacheHitCheckMissJumpMissCacheLookup源码

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

// CacheHit: x17 = cached IMP, x12 = address of cached IMP, x1 = SEL
.macro CacheHit
.if $0 == NORMAL
    TailCallCachedImp x17, x12, x1  // authenticate and call imp
.elseif $0 == GETIMP
    mov p0, p17
    cbz p0, 9f          // don't ptrauth a nil imp
    AuthAndResignAsIMP x0, x12, x1  // authenticate imp and re-sign as IMP
9:  ret             // return IMP
.elseif $0 == LOOKUP
    // No nil check for ptrauth: the caller would crash anyway when they
    // jump to a nil IMP. We don't care if that jump also fails ptrauth.
    AuthAndResignAsIMP x17, x12, x1 // authenticate imp and re-sign as IMP
    ret             // return imp via x17
.else
.abort oops
.endif
.endmacro

.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

.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
    // p1 = SEL, p16 = isa
    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
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

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

__objc_msgSend_uncached源码:

STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band p16 is the class to search
    
    MethodTableLookup
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgSend_uncached

MethodTableLookup源码:

.macro MethodTableLookup
    
    // push frame
    SignLR
    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)]

    // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // receiver and selector already in x0 and x1
    mov x2, x16
    mov x3, #3
    bl  _lookUpImpOrForward

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

.endmacro

我们先从CacheLookup入手,通过查看源码我们可以知道CacheLookup有三种模式,分别是#define NORMAL 0#define GETIMP 1#define LOOKUP 2,其中objc_msgSend使用的是NORMAL模式,从调用处传参就知道了。

源码分析:

  1. CacheLookup:首先将x16也就是我们的类信息,偏移16字节,这个16字节来自一个宏定义#define CACHE (2 * __SIZEOF_POINTER__),通过偏移16字节就是我们的cache_t的初始位置,所以p10就是bucketsp11就是occupied|mask,由于iOS小端模式,低四位w11就是mask,高四位就是occupied

  2. w1与上w11就是sel与上mask就是通过哈希找出方法的位置;

  3. p10也就是buckets里面通过对p12的累加不断寻找我们需要的方法,循环完毕后将结果放入p17p9寄存器中;

  4. CacheHit:比较p1p9的值,如果找到了,则调用CacheHit返回,这是在缓存cache找到的结果,如果没找到就是bl not equal 2f;

  5. CheckMiss:跳转到2f后调用CheckMiss,我们使用的是NORMAL模式,在CheckMissNORMAL对应的是__objc_msgSend_uncached

  6. __objc_msgSend_uncached:中其主要就是继续调用了MethodTableLookup

  7. MethodTableLookup:通过寄存器的各种操作准备查找所必须的条件,然后调用_lookUpImpOrForward

  8. _lookUpImpOrForward: 我们搜索该字符串并没有找到其实现的地方,这个时候我们想到C++方法的调用会在前面加一个,然后我们去掉objc-runtime-new.mm文件中就找到了它的实现,其实这就是方法查找流程中的慢速查找。此处的分析我们放在后面继续进行。

  9. 下面我们回到CacheLookup,在调用CheckMiss完成后比较p12p10就是第一次查找后的位置是不是buckts的首地址,如果是则跳转到3f进行再次循环查找,其实就是在缓存中没找到,然后通过上述的慢速查找流程查找完毕后加入缓存,在次查找一遍。如果不是首地址就loop循环到1f处重新执行上述操作。因为通过慢速查找后buckts的地址可能通过扩容或者新增,致使首地址发生变化?(不太清楚)

  10. 在跳转到3f处查找完毕后来到下面的1处,继续判断查找到的是否是想要的,如果是则通过CacheHit返回,如果不是继续跳转到2f处继续CheckMiss,完毕后继续比较bucketbuckets的地址,相同则跳转到3f处调用JumpMiss,不同则循环回到1f处比较返回。

4.总结

  • Objective-C方法的本质就是消息发送,消息发送司通过objc_msgSend函数实现的
  • objc_msgSend为了执行的快速和稳定性,以及对参数和类型未知性的支持采用汇编实现
  • 方法的查找优先从类的缓存进行查找,也就是我们类的cache,(cache_t类型),如果找到了直接返回,找不到则跳转到由C++实现的lookUpImpOrForward函数进行查找。
  • 方法查找有两种流程,一种是快速流程就是通过汇编在类的缓存里面查找,另一种就是慢速流程lookUpImpOrForward,下一篇文章我们将着重介绍lookUpImpOrForward

相关文章

网友评论

      本文标题:iOS Objective-C 方法的本质

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