OC – Runimte & objc_msgSend()

前言

上一篇文章我们对 cache_t 进行了分析。了解了 cache_t 的结构、以及方法在底层的调用流程。这篇文章我们就一起来分析一些 Runtime API objc_msgSend

Runtime 运行时分析

1. Runtime 概述

编译时 顾名思义就是正在编译的时候,就是编译器帮你把源代码翻译成机器能识别的代码。(当然这只是一意义上这么说,实际上可能只是翻译成某个中间状态的语音。)

运行时 顾名思义就是在代码运行起来后,被装载到内存中。(当你的代码保存在磁盘中,没有写入内存之前都是“死”代码,只有写入内存中才变成“活”代码。而且运行时类型检查与编译时类型检查(或者静态类型检查)是不一样的,不是简单的扫描代码,而是在内存中做了一些操作和判断。

Runtime 有两个版本:一个 Legacy 版本(早期版本);一个 Modern 版本(现行版本)。

  • 早期版本对应的编程接口:Objective-C 1.0
  • 现行版本对应的编程接口:Objective-C 2.0
  • 早期版本用于 Objective-C 1.0,32 位的 Mac OS X 的平台上
  • 现行版本用于 iPhone 程序和 Mac OS v10.5 及以后系统中的64 位程序。

Objective-C Runtime Programming Guide & 苹果官方文档

2. Runtime 调用的三种方式

    1. Objective-C 方法调用;
    1. NSObject提供的API;
    1. Runtime 底层API,objc_msg_send方法;

Runtime 三种调用方式对应的发起者信息图

image.png

接下来我们一起通过代码查看 Runtime 调用的三种方式

添加如下代码:

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

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

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

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

通过 Clang 生成 .cpp 文件。在 .cpp 文件中找到 main 函数,查看它底层代码。

image.png

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

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

TPerson * tp = [TPerson alloc];
[tp sayNB];
objc_msgSend(tp, sel_registerName("sayNB"));
复制代码

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

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

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

@interface TPerson : TTeacher
- (void)sayHello;
- (void)sayNB;
@end

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

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

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

image.png

我们在查看objc_msgSend()的时候,发现有一个objc_msgSendSuper()。难道是子类没有,可以直接给父类发消息?接下来我们验证一下。

image.png

首页我们先来看看 objc_msgSendSuper() 的声明。

image.png
发现 objc_msgSendSuper() 声明需要的参数 struct objc_super * _Nonnull superSEL _Nonnull op,还有一些相应参数。SEL 我们知道,objc_super 是什么?我们不知道。接下来我们就一起看看 objc_super 是什么。全局搜索 objc_super

image.png
可以看出 objc_super 里面有两个参数:receiversuper_class。接下来我们就调用一下 objc_msgSendSuper

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

// 输出:
2021-06-27 21:26:56.619214+0800 001-运行时感受[12241:1255935] 666
2021-06-27 21:26:56.619880+0800 001-运行时感受[12241:1255935] 666 -[TTeacher sayHello]
2021-06-27 21:26:56.619961+0800 001-运行时感受[12241:1255935] 666 -[TTeacher sayHello]
复制代码

objc_msgSend 分析

我们打开源码,搜索全局 objc_msgSend

image.png
这么多文件我们怎么看呢?怎么知道那个是汇编代码呢?接着我们一波骚操作快速定位。

image.png

汇编文件的后缀是 .s,我们就找.s文件查看。

image.png

接下来我们就看 objc-msg-arm64.s 文件,定位到 ENTRY _objc_msgSend(进入到 objc_msgSend)。

image.png

image.png
接下来我们就一行一行的进行分析。

    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

Tips: 这篇文章整理的有些冲忙,如有遗漏的地方后续继续补充。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享