iOS7 Runtime方法本质

372 阅读6分钟

一,runtime简介

  • 参考官方

  1. Objective-C Runtime Programming Guide 
  2. 苹果官方文档
  • runtime简介

Runtime通常叫它运行时,还有一个大家常说的编译时,它们之间的区别是什么
  • 编译时:顾名思义正在编译的时候,啥叫编译呢?就是编译器把源代码翻译成机器能够识别的代码。编译时会进行词法分析,语法分析主要是检查代码是否符合苹果的规范,这个检查的过程通常叫做静态类型检查

  • 运行时代码跑起来,被装装载到内存中。运行时检查错误和编译时检查错误不一样,不是简单的代码扫描,而是在内存中做操作和判断

  • runtiem版本

Runtime有两个版本,一个Legacy版本(早期版本),一个Modern版本(现行版本)

  • 早期版本对应的编程接口:Objective-C 1.0

  • 现行版本对应的编程接口:Objective-C 2.0,源码中经常看到的OBJC2

  • 早期版本用于Objective-C 1.0,32位的Mac OS X的平台

  • 现行版本用于Objective-C 2.0,iPhone程序和Mac OS X v10.5及以后的系统中的64位程序

  • runtime三种调用方式

  1. Objective-C方式,[penson say]
  2. Framework & Serivce方式,isKindOfClass
  3. Runtime API方式,class_getInstanceSize
  • 调用层级分布图如下

45d71482b5c0474f97f9be68e498f9c7_tplv-k3u1fbpfcp-watermark.webp

  • 代码查看runtime
#import <Cocoa/Cocoa.h>

#import <objc/message.h>

@interface NBPerson : NSObject
- (void)sayNB;
- (void)sayHello;

@end

@implementation NBPerson
- (void)sayNB{
    NSLog(@"NB---");
}

- (void)sayHello{
    NSLog(@"Hello---");
}
@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NBPerson * nb = [NBPerson alloc];
        [nb sayNB];
        [nb sayHello];
        NSLog(@"Hello, World!");
        // Setup code that might create autoreleased objects goes here.
    }
    return NSApplicationMain(argc, argv);
}
  • 通过 Clang 生成 .cpp 文件。在 .cpp 文件中找到 main 函数,查看它底层代码。
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        NBPerson * nb = ((NBPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NBPerson"), sel_registerName("alloc"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)nb, sel_registerName("sayNB"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)nb, sel_registerName("sayHello"));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_0g_2qt7mmyn00l8xrhbw4p0rjyc0000gn_T_main_c966b1_mi_2);

    }
    return NSApplicationMain(argc, argv);
}

  • 观察源码发现
  1. 在底层代码中,编译之后上层的代码都会得到有个解释。
  2. 调用方法的过程(消息的发送),objc_msgSend(消息的接收者,消息的主体(SEL+主体))
  • 调用底层api
  • 直接调用如下代码发现编译出错Too many arguments to function call, expected 0, have 2
NBPerson * nb = [NBPerson alloc];    
objc_msgSend(nb,sel_registerName("sayNB"));
  • 设置Build Settings -> Enable Strict Checking of objc_msgSend Calls 为 NO,输出结果。
2021-08-08 14:39:10.378065+0800 objc_msgSend探索[2714:31870] NB---
  • 通过objc_msgSend[perosn sayNB]结果是一样的,同时也验证了方法的本质是消息发送。在用objc_msgSend方式发送消息。
调用父类方法
  • 新建 NBTeacher继承NBPerson
@interface NBTeacher : NBPerson

@end

@implementation NBTeacher

@end

  • 方法调用
 NBTeacher *tc = [NBTeacher alloc];
 [tc sayNB];
  • Clang生成.cpp文件
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        NBTeacher *tc = ((NBTeacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NBTeacher"), sel_registerName("alloc"));
        //子类能够调用父类方法
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)tc, sel_registerName("sayNB"));

    }
    return NSApplicationMain(argc, argv);
}
  • 通过源码我们发现

void objc_moveWeak(void) {}
//这两代码挨着的
void objc_msgSend(void) {}
void objc_msgSendSuper(void) {}

void objc_msgSendSuper_stret(void) {}
void objc_msgSend_fpret(void) {}
void objc_msgSend_stret(void) {}

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

OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
#endif

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

/// Specifies the superclass of an instance. 
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};
  • 可以看出 objc_super 里面有两个参数:receiver 和 super_class。接下来我们就调用一下 objc_msgSendSuper
        NBTeacher *tc = [NBTeacher alloc];
//        [tc sayNB];
        struct objc_super tcd_objc_super;
        tcd_objc_super.receiver = tc;
        tcd_objc_super.super_class = NBPerson.class;
        objc_msgSendSuper(&tcd_objc_super, @selector(sayNB));
        
//输出结果
2021-08-08 15:34:08.709850+0800 objc_msgSend探索[3042:49060] NB---

二,objc_msgSend分析

  • 全局搜索objc_msgSend

截屏2021-08-08 下午3.41.00.png

  • command+单击收起 收起所有代码,然后找到.s文件

F5DE801A-B305-4050-83F5-88A5EC1F32C7.png

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

2DE740C5-3314-4099-869D-0BA8CB42432C.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
// p13 = isa [x0]存放的是class
	ldr	p13, [x0]		// p13 = isa
        // 从 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
	mov	p16, \src// 把当前的地址存入 p16(isa 存入 p16)
.else
	// 64-bit packed isa
	ExtractISA p16, \src, \auth_address
.endif
#else
	// 32-bit raw isa
        // $0, $1, #ISA_MASK $1就是 p13,就是 isa,与上ISA_MASK得到 class,并赋值给 p16
	mov	p16, \src

#endif
  • CacheLookup的实现
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant

// 将x16的值,赋值给x15
	mov	x15, x16			// stash the original isa
LLookupStart\Function:
	// p1 = SEL, p16 = isa
// 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的地址)
	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第 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
  • 通过对objc_msgSend底层源码分析。我们知道了objc_msgSend的调用会根据不同的架构做不同的处理。
  • 今天我们分析了arm64真机环境下,objc_msgSend做了如下几件事:
  1. 通过isa找到class
  2. CacheLookup中通过isa平移找到cache即找到_bucketsAndMaybeMask的地址。
  3. 读取buckets地址,即缓存的首地址。
  4. 获取哈希的下标index

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