objc_msgSend解析----方法缓存

OC方法执行流程

对OC的runtime机制稍有了解的都知道,OC调用方法,实际上是通过objc_msgSend进行的。其调用方法的基本步骤为:

  1. 判断调用者(receiver 或者 self)是否为空。
    • 如果为空,则直接返回。
    • 如果不为空,进行步骤2。
  2. 从当前类的方法缓存中查找该方法的实现。
    • 如果查找到,则调用缓存中的imp。
    • 如果没有查找到,则进步骤3。
  3. 调用_class_lookupMethodAndLoadCache3方法查找
    • 从本class以及继承体系中逐级向上查找并填充到缓存中。如果继承关系中也没有寻找,则:
    • 调用_class_resolveMethod方法,进行补救。但是方法不缓存。
    • 转发该方法。
  4. 抛出异常。

这里,着重分析方法缓存。

方法缓存

在源码中,可以找到类的定义:

1
2
3
4
5
6
7
8
9
10
11

struct objc_object {
private:
isa_t isa;
}
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
}

从定义中不难看出,方法缓存的字段cache存储在类中,其定义为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

struct bucket_t {
private:
// IMP-first is better for arm64e ptrauth and no worse for arm64.
// SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
MethodCacheIMP _imp;
cache_key_t _key;
#else
cache_key_t _key;
MethodCacheIMP _imp;
#endif
}

struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
}

一个类的所有方法缓存,都在cache_t中的_buckets数组中。

那么,objc_msgSend是怎么查找这个方法缓存的呢?

这就需要借助源码来进一步分析(源码可以去官网下载,或者直接调试,断点objc_msgSend方法)。

这里直接调试,断点objc_msgSend函数来进行分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
//MsgObject.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface MsgObject : NSObject

-(void)sl_test;

@end

NS_ASSUME_NONNULL_END

1
2
3
4
5
6
7
8
9
10
//MsgObject.m
#import "MsgObject.h"

@implementation MsgObject

-(void)sl_test{
NSLog(@"@_@");
}

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//main.m
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "MsgObject.h"
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.

MsgObject *m = [MsgObject new];

[m sl_test];

[m sl_test];//断点

appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

运行程序,触发断点,进入objc_msgSend方法内部:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
libobjc.A.dylib`objc_msgSend:
-> 0x1855b8140 <+0>: cmp x0, #0x0 ; =0x0
0x1855b8144 <+4>: b.le 0x1855b81ac ; <+108>
0x1855b8148 <+8>: ldr x13, [x0]
0x1855b814c <+12>: and x16, x13, #0xffffffff8
0x1855b8150 <+16>: ldp x10, x11, [x16, #0x10]
0x1855b8154 <+20>: and w12, w1, w11
0x1855b8158 <+24>: add x12, x10, x12, lsl #4
0x1855b815c <+28>: ldp x9, x17, [x12]
0x1855b8160 <+32>: cmp x9, x1
0x1855b8164 <+36>: b.ne 0x1855b816c ; <+44>
0x1855b8168 <+40>: br x17
0x1855b816c <+44>: cbz x9, 0x1855b8440 ; _objc_msgSend_uncached
0x1855b8170 <+48>: cmp x12, x10
0x1855b8174 <+52>: b.eq 0x1855b8180 ; <+64>
0x1855b8178 <+56>: ldp x9, x17, [x12, #-0x10]!
0x1855b817c <+60>: b 0x1855b8160 ; <+32>
0x1855b8180 <+64>: add x12, x12, w11, uxtw #4
0x1855b8184 <+68>: ldp x9, x17, [x12]
0x1855b8188 <+72>: cmp x9, x1
0x1855b818c <+76>: b.ne 0x1855b8194 ; <+84>
0x1855b8190 <+80>: br x17
0x1855b8194 <+84>: cbz x9, 0x1855b8440 ; _objc_msgSend_uncached
0x1855b8198 <+88>: cmp x12, x10
0x1855b819c <+92>: b.eq 0x1855b81a8 ; <+104>
0x1855b81a0 <+96>: ldp x9, x17, [x12, #-0x10]!
0x1855b81a4 <+100>: b 0x1855b8188 ; <+72>
0x1855b81a8 <+104>: b 0x1855b8440 ; _objc_msgSend_uncached
0x1855b81ac <+108>: b.eq 0x1855b81e4 ; <+164>
0x1855b81b0 <+112>: mov x10, #-0x1000000000000000
0x1855b81b4 <+116>: cmp x0, x10
0x1855b81b8 <+120>: b.hs 0x1855b81d0 ; <+144>
0x1855b81bc <+124>: adrp x10, 161523
0x1855b81c0 <+128>: add x10, x10, #0x270 ; =0x270
0x1855b81c4 <+132>: lsr x11, x0, #60
0x1855b81c8 <+136>: ldr x16, [x10, x11, lsl #3]
0x1855b81cc <+140>: b 0x1855b8150 ; <+16>
0x1855b81d0 <+144>: adrp x10, 161523
0x1855b81d4 <+148>: add x10, x10, #0x2f0 ; =0x2f0
0x1855b81d8 <+152>: ubfx x11, x0, #52, #8
0x1855b81dc <+156>: ldr x16, [x10, x11, lsl #3]
0x1855b81e0 <+160>: b 0x1855b8150 ; <+16>
0x1855b81e4 <+164>: mov x1, #0x0
0x1855b81e8 <+168>: movi d0, #0000000000000000
0x1855b81ec <+172>: movi d1, #0000000000000000
0x1855b81f0 <+176>: movi d2, #0000000000000000
0x1855b81f4 <+180>: movi d3, #0000000000000000
0x1855b81f8 <+184>: ret
0x1855b81fc <+188>: nop

整个方法的汇编,就是这个样子的。我们逐条指令分析(重点分析方法缓存查找部分):

1
2
3
4
0x1855b8140 <+0>:   cmp    x0, #0x0                  ; =0x0 
0x1855b8144 <+4>: b.le 0x1855b81ac ; <+108>
0x1855b8148 <+8>: ldr x13, [x0]
0x1855b814c <+12>: and x16, x13, #0xffffffff8

函数一开始,通过cmp指令判断X0是否为nil,即判断self是否为nil,如果为nil,则跳到0x1855b81ac处执行。(实际上不只是nil,taggedpointer也会走这里)。0x1855b81ac处为处理self为空或者为taggedpointer的情况,这里不做研究。

ldr指令获取isa指针。事实上,自从引入taggerpoint之后,isa指针已经不是单纯的指针了,所以需要对其进行处理。而and指令,就是获取了真正的isa指针。

1
0x1855b8150 <+16>:  ldp    x10, x11, [x16, #0x10]

上面几条指令,已经获取了isa指针,并存储在X16寄存器中,这条指令,是获取cache的。

其中,X10中存储的是_buckets,而X11中存储的是_mask_occupied。低32位,是_mask,高32位是_occupied

1
2
0x1855b8154 <+20>:  and    w12, w1, w11
0x1855b8158 <+24>: add x12, x10, x12, lsl #4

这里,w1是_cmd的低32位,而w11是_mask,所以这句,实际上是w12 = _cmd & _mask,旨在获取第一个要查找的index

add x12, x10, x12, lsl #4这条指令的意思是将X12寄存器的值左移4位(即乘以16),然后与X10寄存器中的值相加,并把结果存放在X12寄存器中。上面已经分析出,X10是_buckets数组,所以这条指令相当于&_buckets[W12]

1
2
3
4
0x1855b815c <+28>:  ldp    x9, x17, [x12]
0x1855b8160 <+32>: cmp x9, x1
0x1855b8164 <+36>: b.ne 0x1855b816c ; <+44>
0x1855b8168 <+40>: br x17

从上面可以得出,X12=&_buckets[W12],所以ldp指令就把该地址处存储的数据取出,分别存放在X9x17中。由上面的bucket_t结构体定义可以看出,对于arm64,该处存储的方法的impsel

cmp x9, x1指令,说明了X9存储的是key,用key与sel比较,如果相等,则br x17,所以X17中的是imp。这点,似乎和上面的定义有点出入。

1
2
3
4
5
6
7
8
9
10
11
12
struct bucket_t {
private:
// IMP-first is better for arm64e ptrauth and no worse for arm64.
// SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
MethodCacheIMP _imp;
cache_key_t _key;
#else
cache_key_t _key;
MethodCacheIMP _imp;
#endif
}

源码中对bucket_t的定义,应该是imp在前,key在后的。此处有疑问。

虽然位置不对,但其实并不影响我们分析。继续往下看:

1
0x1855b816c <+44>:  cbz    x9, 0x1855b8440           ; _objc_msgSend_uncached

还是和key进行比较,如果key为0,则表示,没有缓存。就跳到0x1855b8440处执行。

1
2
3
4
5
0x1855b8170 <+48>:  cmp    x12, x10
0x1855b8174 <+52>: b.eq 0x1855b8180 ; <+64>
0x1855b8178 <+56>: ldp x9, x17, [x12, #-0x10]!
0x1855b817c <+60>: b 0x1855b8160 ; <+32>
0x1855b8180 <+64>: add x12, x12, w11, uxtw #4

此处,用X12和X10进行比较,上面已经说了,X12是当前index的位置,而X10是_buckets数组的起始位置,旨在判断当前位置是否在数组起始位置。如果是,则跳转到0x1855b8180处,而0x1855b8180处的指令add x12, x12, w11, uxtw #4,是将X12(可以看成是一个指针)的值赋为数组的最后一个元素,即将指针移到数组末尾。

1
2
0x1855b8178 <+56>:  ldp    x9, x17, [x12, #-0x10]!
0x1855b817c <+60>: b 0x1855b8160 ; <+32>

这两句,是一个loop循环。翻译为伪C代码:

1
ldp    x9, x17, [x12, #-0x10]!	;====>{X9,X17} = *X12		--X12
1
2
3
4
5
6
7
8
9
10
0x1855b8180 <+64>:  add    x12, x12, w11, uxtw #4
0x1855b8184 <+68>: ldp x9, x17, [x12]
0x1855b8188 <+72>: cmp x9, x1
0x1855b818c <+76>: b.ne 0x1855b8194 ; <+84>
0x1855b8190 <+80>: br x17
0x1855b8194 <+84>: cbz x9, 0x1855b8440 ; _objc_msgSend_uncached
0x1855b8198 <+88>: cmp x12, x10
0x1855b819c <+92>: b.eq 0x1855b81a8 ; <+104>
0x1855b81a0 <+96>: ldp x9, x17, [x12, #-0x10]!
0x1855b81a4 <+100>: b 0x1855b8188 ; <+72>

这段汇编,是不是感觉很熟悉。因为在前 出现过一次,这其实也是一个循环。翻译一下:

1
2
3
4
5
6
7
8
9
10
11
X12 += w11;//指针加法,步长为16Byte
do{
{x9,x17} = *x12;
if(x9 == x1)
{
x17();//调用imp
}else(x9 != x1 && x9 == 0)
{
_objc_msgSend_uncached();
}
}while(X12 != X10;X12--)

这整个过程,就是方法缓存的查找过程。