Runimte 运行时&方法的本质

458 阅读7分钟

通过上一篇文章的分析,我们对cache_t的底层部分流程有一定的了解,我们是通过insert插入操作开始对缓存进行分析的,那是什么时候开始调用insert的呢?

insert()的闭环流程

  • 我们在insert()方法中下断点,查看调用堆栈信息

截屏2021-07-16 下午6.00.22.png

  • 栈是一种LIFO的数据结构,index = 0 是最后调用的,从0开始依次查看调用信息,发现log_and_fill_cache方法调用了cache.insert方法;我们看一下log_and_fill_cache的实现:

截屏2021-07-16 下午6.03.06.png

  • 调用栈显示,是lookUpImpOrForward调用了log_and_fill_cache
  • 查看objc-cache.mm文件,可以根据注释看到insert()的插入时机最上层是通过objc_msgSend触发的,接下来我们就一起看下objc_msgSend

1626430141996_E09606C1-2D91-467C-A47A-BFFC8B9E99D1.png

runtime的运行时理解

什么是runtime

想要理解消息流程,那么必须先了解runtime的相关知识

编译时

  1. 编译时 顾名思义就是正在编译的时候. 编译器源代码翻译成机器能识别的代码(当然只是⼀般意义上这么说,实际上可能只是翻译成某个中间状态的语⾔)。编译时通过语法分析词法分析等编译时类型检查(静态类型检查)来发现代码中的errorswarning等编译时的错误信息.
  2. 静态检查不会把代码放内存中运⾏起来,⽽只是当作⽂本来扫描检查,⼀些⼈说编译时还分配内存啥的肯定是错误的说法;

运行时

  • 运⾏时就是代码通过dyld被装载到内存中执行的过程运⾏时类型检查就与前⾯讲的编译时类型检查(或者静态类型检查)不⼀样.不是简单的扫描代码.⽽是在内存中做操作和判断.
  • Runtime有两个版本 ⼀个Legacy版本(早期版本) ,⼀个Modern版本(现⾏版本)

runtime有两个版本:

  • Legacy版本为早期版本,对应Objective-C 1.0,用于32位的MacOS X系统中
  • Modern为现行版本,对应Objective-C 2.0,用于iPhoneMacOS X v10.5之后的64位系统中

关于OC语言的更多信息可以在苹果官方文档进行查阅

Runtime调用方式一共有3种

  • OC方法:[p sayNB];
  • NSObject提供的API:isKindofClass...;
  • objc的下层APIobjc_msg_send方法:class_getInstanceSize...;

Runtime3种调用方式对应的发起者信息如下

未命名文件.png

#import <Foundation/Foundation.h>
#import <objc/message.h>

@interface XJPerson : NSObject
- (void)sayNB;
@end

@implementation XJPerson
- (void)sayNB{
    NSLog(@"666");
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        XJPerson * tp = [XJPerson alloc];
        [tp sayNB];
        [tp sayHello];
        NSLog(@"Hello, World!");
    }
    return 0;
}

通过 Clang 生成 .cpp 文件。在 .cpp 文件中找到 main 函数,查看它底层代码。 截屏2021-07-19 下午5.46.36.png

  • 发现编译之后上层的代码,在底层代码中都会有个解释。
  • 是一个调用方法的过程(消息的发送),objc_msgSend(消息的接收者,消息的主体(SEL+主体))

那么我们能否直接调用底层的消息发送方法呢?接下来我们就试一下。注意:我们调用的时候可能会编译失败,需要将Build Settings -> Enable Strict Checking of objc_msgSend Calls 设置成为 NO

XJPerson * tp = [XJPerson alloc];
[tp sayNB];
objc_msgSend(tp, sel_registerName("sayNB"));

程序正常运行!说明我们可以直接调用objc_msgSend()。接下来我们做一个尝试,XJPerson 继承 XJTeacher

@interface XJTeacher : NSObject
- (void)sayHello;
@end

@implementation XJTeacher
- (void)sayHello{
    NSLog(@"666 %s",__func__);
}
@end

@interface XJPerson : XJTeacher
- (void)sayHello;
- (void)sayNB;
@end

@implementation XJPerson
- (void)sayNB{
    NSLog(@"666");
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        XJPerson * tp = [XJPerson alloc];
        XJTeacher * tt = [XJTeacher alloc];
        
        [tp sayNB];
        // objc_msgSend(tp, @selector(sayNB));
        [tp sayHello];
        
        NSLog(@"Hello, World!");
    }
    return 0;
}

运行代码,生成.cpp文件,看看 Runtime 在运行时是怎么处理的。注意:在生成.cpp文件时,需要注释objc_msgSend()代码,否则会生成失败~

截屏2021-07-19 下午6.36.19.png 我们在查看objc_msgSend()的时候,发现有一个objc_msgSendSuper()。难道是子类没有,可以直接给父类发消息?接下来我们验证一下。 截屏2021-07-19 下午6.37.31.png 首先我们先来看看 objc_msgSendSuper() 的声明。 截屏2021-07-19 下午6.40.06.png 发现 objc_msgSendSuper() 声明需要的参数 struct objc_super * _Nonnull superSEL _Nonnull op,还有一些相应参数。SEL 我们知道,objc_super 是什么?我们不知道。接下来我们就一起看看 objc_super 是什么。全局搜索 objc_super

截屏2021-07-19 下午6.41.28.png 可以看出 objc_super 里面有两个参数:receiver 和 super_class。接下来我们就调用一下 objc_msgSendSuper

struct objc_super xj_objc_super;
xj_objc_super.receiver = tp;
xj_objc_super.super_class = XJTeacher.class;
objc_msgSendSuper(&xj_objc_super, @selector(sayHello));

// 输出:
666
666 -[XJTeacher sayHello]
666 -[XJTeacher sayHello]

objc_msgSend 分析

由于OC语言的动态特性,基本上所有方法都是通过objc_msgSend进行处理的,这样一个高频调用的底层方法,从效率层面考虑最好是使用接近机器语言的汇编语言来实现,同时汇编语言基于地址也更加安全,所以系统也是用汇编进行实现的;下面一起来看下objc_msgSend在在libobjc库中的汇编层面的实现吧

  • objc源码中搜索objc_msgSend
  • 关闭所有文件,查看后缀为.s的汇编文件 1626750651942_67D6CB55-7592-420E-A2DE-B6443F615BBD.png 1626751189771_62A6B4E5-3A85-4934-9BC7-64CE7D2908DD.png
  • 接下来我们就看 objc-msg-arm64.s 文件,定位到 ENTRY _objc_msgSend(进入到 objc_msgSend)。 1626751921793_DAD885AC-C315-42B3-BA67-BD482A24B62D.png 开始逐行分析_objc_msgSend汇编部分源码
    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    
// p0代表消息接受者的地址,这里判断消息接收者是否存在
    cmp	p0, #0			// nil check and tagged pointer check

// 判断是否支持Taggedpointer类型
#if SUPPORT_TAGGED_POINTERS

// 如果支持Taggedpointer类型按照LNilOrTagged处理
    b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else

// 如果不支持Taggedpointer类型返回LReturnZero
    b.eq	LReturnZero
    
#endif
    ldr	p13, [x0]		// p13 = isa [x0]存放的是class
//  从 GetClassFromIsa_p16 中获取clsss:p16 = class
    GetClassFromIsa_p16 p13, 1, x0	// p16 = class
    
//  receiver->class 获取class,去class 中找method cache
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

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

把 isa 赋值给 p13,并作为参数带入到 GetClassFromIsa_p16 中。接下来我们就分析 GetClassFromIsa_p16 中做了怎样的处理?

.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */

#if SUPPORT_INDEXED_ISA
    // Indexed isa
    mov	p16, \src			// 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__
.if \needs_auth == 0 // _cache_getImp takes an authed class already
    // 把当前的地址存入 p16(isa 存入 p16)
    mov	p16, \src
.else
    // 64-bit packed isa
    // $0, $1, #ISA_MASK $1就是 p13,就是 isa,与上ISA_MASK得到 class,并赋值给 p16
    ExtractISA p16, \src, \auth_address
.endif
#else
    // 32-bit raw isa
    mov	p16, \src

#endif

分析CacheLookup的实现。

.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
    
    mov	x15, x16			// stash the original isa  // 将x16的值,赋值给x15
LLookupStart\Function:
    // p1 = SEL, p16 = isa  // => isa -> 类的 isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS   
    ldr	p10, [x16, #CACHE]				// p10 = mask|buckets
    lsr	p11, p10, #48			// p11 = mask
    and	p10, p10, #0xffffffffffff	// p10 = buckets
    and	w12, w1, w11			// x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    // x16(isa) 平移 16 就得到了 cache的地址(即为_bucketsAndMaybeMask的地址)
    // 将 _bucketsAndMaybeMask的地址存入 p11 寄存器中。
    ldr	p11, [x16, #CACHE]			// p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
    tbnz	p11, #0, LLookupPreopt\Function
    and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else
    and	p10, p11, #0x0000fffffffffffe	// p10 = buckets = _bucketsAndMaybeMask & #0x0000fffffffffffe
    // 如果_bucketsAndMaybeMask第 0 位不等于 0,就跳转到 LLookupPreopt\Function
    tbnz	p11, #0, LLookupPreopt\Function
#endif
    eor	p12, p1, p1, LSR #7
    // 通过哈希求 index 下标
    and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
    and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
    and	p12, p1, p11, LSR #48		// x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    ldr	p11, [x16, #CACHE]				// p11 = mask|buckets
    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

CACHE的定义

#define CACHE            (2 * __SIZEOF_POINTER__)  // => CACHE = 16

通过对objc_msgSend底层源码分析。我们知道了objc_msgSend的调用会根据不同的架构做不同的处理。今天我们分析了arm64真机环境下,objc_msgSend做了如下几件事:

  1. 通过isa找到class
  2. CacheLookup中通过isa平移找到cache即找到_bucketsAndMaybeMask的地址。
  3. 读取buckets地址,即缓存的首地址。
  4. 获取哈希的下标index

整体流程:isa -> cache(_bucketsAndMaybeMask) -> buckets -> 哈希下标 index

总结

首先我们了解了Runtime编译时和运行时的区别,然后我们知道了 Runtime 的三种调用方式,最后我们通过对objc_msgSend进行分析,知道了objc_msgSend的调用会根据不同的架构做不同的处理,以及在真机环境下整体调用流程:isa -> cache(_bucketsAndMaybeMask) -> buckets -> 哈希下标 index