注册
iOS

objc_msgSend解析----方法缓存


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

  1. 判断调用者(receiver 或者 self)是否为空。

    • 如果为空,则直接返回。

    • 如果不为空,进行步骤2。

  2. 从当前类的方法缓存中查找该方法的实现。

    • 如果查找到,则调用缓存中的imp。

    • 如果没有查找到,则进步骤3。

  3. 调用_class_lookupMethodAndLoadCache3方法查找

    • 从本class以及继承体系中逐级向上查找并填充到缓存中。如果继承关系中也没有寻找,则:

    • 调用_class_resolveMethod方法,进行补救。但是方法不缓存。

    • 转发该方法。

  4. 抛出异常。

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

方法缓存

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


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存储在类中,其定义为:


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函数来进行分析:

//MsgObject.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface MsgObject : NSObject

-(void)sl_test;

@end

NS_ASSUME_NONNULL_END
//MsgObject.m
#import "MsgObject.h"

@implementation MsgObject

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

@end
//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方法内部:

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    

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

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指针。

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

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

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

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]

    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。这点,似乎和上面的定义有点出入。

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在后的。此处有疑问。

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

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

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

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(可以看成是一个指针)的值赋为数组的最后一个元素,即将指针移到数组末尾。

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

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

ldp    x9, x17, [x12, #-0x10]!	;====>{X9,X17} = *X12		--X12
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>

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

X12 += w11;//指针加法,步长为16Byte
do{
{x9,x17} = *x12;
if(x9 == x1)
{
x17();//调用imp
}else(x9 != x1 && x9 == 0)
{
_objc_msgSend_uncached();
}
}while(X12 != X10;X12--)

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

0 个评论

要回复文章请先登录注册