阅读 83

7-- Runtime & objc_msgSend

cache 读写流程

下图是cache的读写流程,当我们读取cache的时候,需要调用cache_getImp image.png 本文的重点在objc_msgSend,是发起读取cache的地方.

runtime

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

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

runtime调用的三种方式:

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

image.png

@interface MLTeacher : NSObject
- (void)sayHello;
@end
@implementation MLTeacher
- (void)sayHello{
    NSLog(@"666 %s",__func__);
}
@end
@interface MLPerson : MLTeacher
- (void)sayHello;
- (void)sayNB;
@end
@implementation MLPerson
- (void)sayNB{
    NSLog(@"666");
}
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MLPerson *person = [MLPerson alloc];
        MLTeacher *teach = [MLTeacher alloc];
        [person sayNB];
        [person sayHello];
        NSLog(@"Hello, World!");
    }
    return 0;
}

复制代码

通过命令clang -rewrite-objc main.m -o main.cpp 生成cpp文件

image.png 可以发现sayNB之类的方法调用都是通过objc_msgSend来实现的。

那么我们是否能够直接调用objc_msgSend方法呢?我们调用的时候可能会编译失败,需要将Build Settings -> Enable Strict Checking of objc_msgSend Calls 设置成为 NO, 如下图 image.png 引入头文件#import <objc/message.h>代码如下

MLPerson *person = [MLPerson alloc];
objc_msgSend(person, @selector(sayNB));
复制代码

如果我们重载方法,调用supercpp的实现会是什么样呢?

image.png

static void _I_MLPerson_sayHello(MLPerson * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MLPerson"))}, sel_registerName("sayHello"));
}
复制代码

发现cpp的实现是通过调用objc_msgSendSuper实现的. 通过上述的分析,以及对runtime的体验,方法的本质就是objc_msgSend.

objc_msgSend 分析

当在源码中全局搜索objc_msgSend的时候,发现objc_msgSend的实现是汇编 如下是在arm64objc_msgSend的汇编实现 image.png 此时有个疑问是为什么使用汇编实现的,而不是通过c++的代码实现的?

line 54:cmp p0 #0 //p0 是receiver的地址,对p0 与 0 比较,如果为nil直接返回 line 56 或者 line 58

line 60: 将[x0]的内容读取到p13, [x0]是 isa

line 61: p16 赋值为 isa,下图为GetClassFromIsa_p16源码 image.png image.png

实际就是指针地址与ISA_MASK, 获取到isa的地址,获取isa的地址是要获取cache, 先从cache中找到IMP. line 64: CacheLookup

1. .macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
2. 
3. 	mov	x15, x16 // stash the original isa, x16:isa
4. LLookupStart\Function:
5. 	// p1 = SEL, p16 = isa
6. #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
7. 	ldr	p10, [x16, #CACHE] // p10 = mask|buckets, CACHE: 16; p10 = x16 + 16, cache_t
8. 	lsr	p11, p10, #48			// p11 = mask
9. 	and	p10, p10, #0xffffffffffff	// p10 = buckets
10. 	and	w12, w1, w11			// x12 = _cmd & mask
11. #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
12. 	ldr	p11, [x16, #CACHE] // p11 = mask|buckets, CACHE: 16; p10 = x16 + 16, cache_t
13. #if CONFIG_USE_PREOPT_CACHES
14. #if __has_feature(ptrauth_calls)
15. 	tbnz	p11, #0, LLookupPreopt\Function
16. 	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
17. #else
18. 	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
19. 	tbnz	p11, #0, LLookupPreopt\Function
20. #endif
21. 	eor	p12, p1, p1, LSR #7
22. 	and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
23. #else
24. 	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
25. 	and	p12, p1, p11, LSR #48		// x12 = _cmd & mask
26. #endif // CONFIG_USE_PREOPT_CACHES
27. #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
28. 	ldr	p11, [x16, #CACHE]				// p11 = mask|buckets
29. 	and	p10, p11, #~0xf			// p10 = buckets
30. 	and	p11, p11, #0xf			// p11 = maskShift
31. 	mov	p12, #0xffff
32. 	lsr	p11, p12, p11			// p11 = mask = 0xffff >> p11
33. 	and	p12, p1, p11			// x12 = _cmd & mask
34. #else
35. #error Unsupported cache mask storage for ARM64.
36. #endif
37. 
38. 	add	p13, p10, p12, LSL #(1+PTRSHIFT)
39. 						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
40. 
41. 						// do {
42. 1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
43. 	cmp	p9, p1				//     if (sel != _cmd) {
44. 	b.ne	3f				//         scan more
45. 						//     } else {
46. 2:	CacheHit \Mode				// hit:    call or return imp
47. 						//     }
48. 3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
49. 	cmp	p13, p10			// } while (bucket >= buckets)
50. 	b.hs	1b
51. 
52. 	// wrap-around:
53. 	//   p10 = first bucket
54. 	//   p11 = mask (and maybe other bits on LP64)
55. 	//   p12 = _cmd & mask
56. 	//
57. 	// A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
58. 	// So stop when we circle back to the first probed bucket
59. 	// rather than when hitting the first bucket again.
60. 	//
61. 	// Note that we might probe the initial bucket twice
62. 	// when the first probed slot is the last entry.
63. 
64. 
65. #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
66. 	add	p13, p10, w11, UXTW #(1+PTRSHIFT)
67. 						// p13 = buckets + (mask << 1+PTRSHIFT)
68. #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
69. 	add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
70. 						// p13 = buckets + (mask << 1+PTRSHIFT)
71. 						// see comment about maskZeroBits
72. #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
73. 	add	p13, p10, p11, LSL #(1+PTRSHIFT)
74. 						// p13 = buckets + (mask << 1+PTRSHIFT)
75. #else
76. #error Unsupported cache mask storage for ARM64.
77. #endif
78. 	add	p12, p10, p12, LSL #(1+PTRSHIFT)
79. 						// p12 = first probed bucket
80. 
81. 						// do {
82. 4:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
83. 	cmp	p9, p1				//     if (sel == _cmd)
84. 	b.eq	2b				//         goto hit
85. 	cmp	p9, #0				// } while (sel != 0 &&
86. 	ccmp	p13, p12, #0, ne		//     bucket > first_probed)
87. 	b.hi	4b
88. 
89. LLookupEnd\Function:
90. LLookupRecover\Function:
91. 	b	\MissLabelDynamic
92. 
93. #if CONFIG_USE_PREOPT_CACHES
94. #if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16
95. #error config unsupported
96. #endif
97. LLookupPreopt\Function:
98. #if __has_feature(ptrauth_calls)
99. 	and	p10, p11, #0x007ffffffffffffe	// p10 = buckets
100. 	autdb	x10, x16			// auth as early as possible
101. #endif
102. 
103. 	// x12 = (_cmd - first_shared_cache_sel)
104. 	adrp	x9, _MagicSelRef@PAGE
105. 	ldr	p9, [x9, _MagicSelRef@PAGEOFF]
106. 	sub	p12, p1, p9
107. 
108. 	// w9  = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
109. #if __has_feature(ptrauth_calls)
110. 	// bits 63..60 of x11 are the number of bits in hash_mask
111. 	// bits 59..55 of x11 is hash_shift
112. 
113. 	lsr	x17, x11, #55			// w17 = (hash_shift, ...)
114. 	lsr	w9, w12, w17			// >>= shift
115. 
116. 	lsr	x17, x11, #60			// w17 = mask_bits
117. 	mov	x11, #0x7fff
118. 	lsr	x11, x11, x17			// p11 = mask (0x7fff >> mask_bits)
119. 	and	x9, x9, x11			// &= mask
120. #else
121. 	// bits 63..53 of x11 is hash_mask
122. 	// bits 52..48 of x11 is hash_shift
123. 	lsr	x17, x11, #48			// w17 = (hash_shift, hash_mask)
124. 	lsr	w9, w12, w17			// >>= shift
125. 	and	x9, x9, x11, LSR #53		// &=  mask
126. #endif
127. 
128. 	ldr	x17, [x10, x9, LSL #3]		// x17 == sel_offs | (imp_offs << 32)
129. 	cmp	x12, w17, uxtw
130. 
131. .if \Mode == GETIMP
132. 	b.ne	\MissLabelConstant		// cache miss
133. 	sub	x0, x16, x17, LSR #32		// imp = isa - imp_offs
134. 	SignAsImp x0
135. 	ret
136. .else
137. 	b.ne	5f				// cache miss
138. 	sub	x17, x16, x17, LSR #32		// imp = isa - imp_offs
139. .if \Mode == NORMAL
140. 	br	x17
141. .elseif \Mode == LOOKUP
142. 	orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
143. 	SignAsImp x17
144. 	ret
145. .else
146. .abort  unhandled mode \Mode
147. .endif
148. 
149. 5:	ldursw	x9, [x10, #-8]			// offset -8 is the fallback offset
150. 	add	x16, x16, x9			// compute the fallback isa
151. 	b	LLookupStart\Function		// lookup again with a new isa
152. .endif
153. #endif // CONFIG_USE_PREOPT_CACHES
154. 
155. .endmacro
复制代码

上述汇编的流程如下:

  1. Line 3: x15, x16isa存储到x15
  2. Line 12: ldr p11, [x16, #CACHE], 其中CACHE = 16, p11 = x16+16, 由class结构可知, p11cache_t的首地址
  3. Line 18: and p10, p11, #0x0000fffffffffffe, p10 = p11 & 00x0000fffffffffffe, 0x0000fffffffffffebuckets的掩码, 得到p10 = buckets 首地址.
  4. Line 18: p11, #0, LLookupPreopt\Function, 不为0就跳转到LLookupPreopt
  5. Line 38: add p13, p10, p12, LSL #(1+PTRSHIFT), p12为当前sel, impindex, p13 = buckets + (_cmd & mask) << (1 + PTRSHIFT). PTRSHIFT3. 右移4位的原因,是要知道buckets内存平移需要平移几个单位. 即buckets[i]. p13为当前要查找的bucket.
  6. Line 42 ~ 44: 1:ldp p17, p9, [x13], #-BUCKET_SIZE, 查找到当前位置存不存在bucket. 如果找到就跳转到Cachehit,缓存命中,否则的话,就循环查找. 如果找到,就callimp.

从上述源码中我们可以大概了解到objc_msgSend的流程为如下:

  1. 通过receiver获取到isa
  2. isa获取cache
  3. cache(包含buckets & mask), 通过buckets的掩码得到buckets.
  4. 通过mask掩码得到mask(mask 为当前bucketscapacipty -1).
  5. sel imp进行hash, 得到第一次查找的index
  6. buckets + index整个缓存中的第几个bucket
  7. 得到查找到的sel imp与传进来的sel == _cmd进行比较, 如果相等,缓存命中,调用imp
  8. 如果不想等, buckets + index --,向前查找,跳转到step 7
  9. 如果整个buckets遍历结束,都没有找到, 跳转到objc_msgSend_uncached
  10. objc_msgSend_uncached会在后续的文章更新...

image.png

补充

bt:查看堆栈

LLVM 源码地址

文章分类
iOS
文章标签