msgSend底层(一)方法的快速查找 cache

408 阅读4分钟

方法的本质

方法的本质是objc_msgSend(消息发送),而objc_msgSend调用需要消息的接收者sel+参数

@interface ZMPerson : NSObject

- (void)sayNB;
- (void)sayGood:(NSString *)job;

@end

@implementation ZMPerson

- (void)sayNB {
    NSLog(@"%s", __func__);
}

// 调用:
ZMPerson *person = [ZMPerson alloc];
[person sayNB];
[person sayGood: @"coder"];

// C++
ZMPerson *person = ((ZMPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("ZMPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)person, sel_registerName("sayGood:"), (NSString *)&__NSConstantStringImpl__var_folders_7t_93mn_rbs0p1d7bkzy4z_f0s40000gn_T_main_e9af44_mi_1);

@end
  • 可以通过objc_msgSend直接调用方法
    • 导入头文件#import <objc/message.h>
    • 关闭objc_msgSend的检查机制:target->Build Setting->搜索msg->Enable strict Checking of obc_msgSend Calls改为NO
    • 模仿调用 objc_msgSend(person, sel_registerName("sayNB"));
    • 结果等价
  • 调用父类方法
    • 编译为c++,多了个objc_msgSendSuper
static void _I_WSTeacher_sayGood(WSTeacher * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("WSTeacher"))}, sel_registerName("sayGood"));
}
  • 源码查看 objc_msgSendSuper
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
// `objc_msgSendSuper`有两个参数,第二个是`SEL`,第一个是个`objc_super`结构体,结构如下:
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_msgSend汇编

  • objc-msg-arm64.s汇编文件中找到入口

image.png

SUPPORT_TAGGED_POINTERS

它是小对象模式,它的定义是:

#if __arm64__
...
#define SUPPORT_TAGGED_POINTERS 1
...
// true arm64
#else
// arm64_32
#define SUPPORT_TAGGED_POINTERS 0
...
#endif
如果是模拟器,SUPPORT_TAGGED_POINTERS为0,`b.eq LReturnZero`:这个是`p0`和`0`相等,也就是消息接受者不存在的意思,这时候返回`LReturnZero`:也就是 直接返回nil

GetClassFromIsa_p16

// src=p13=isa, needs_auth=1, auth_address=x0
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */

#if SUPPORT_INDEXED_ISA //arm64
	// 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 //将src赋值给p16
.else
	// 64-bit packed isa
	ExtractISA p16, \src, \auth_address //得到p16
.endif
#else
	// 32-bit raw isa
	mov	p16, \src

#endif

.endmacro
  • GetClassFromIsa_p16是一个宏定义,要传入三个参数,由于SUPPORT_INDEXED_ISAarm64中为1,则直接跳过看 __LP64__,这里传入的参数needs_auth=1,所以看ExtractISA这个方法:

ExtractISA

arm64-asm.h文件中 看到这个方法

.macro ExtractISA 
    and $0, $1, #ISA_MASK 
.endmacro

ExtractISA也是个宏定义,and&运算,这里是将$1 & ISA_MASK,再将得到的结果赋值给$0,而$0就是p16,所以这是根据isaISA_MASK拿到class的过程,然后回到最开始GetClassFromIsa_p16 p13, 1, x0,这个步骤就是先拿到class,所以这一行的注释上p16 = class

CacheLookup

函数CacheLookup,并传入三个参数NORMAL, _objc_msgSend __objc_msgSend_uncached,我们进入方法再进行分析:

获取bucket和查找index

image.png 几个主要的步骤

    1. 获取cache_tldr p11, [x16, #CACHE],这里x16p16地址,我们知道p16class,而#CACHE16字节,所以此处是:class地址平移16个字节,刚好取到cache_t
    1. 获取buckets#0x0000fffffffffffemaskShiftp11 & maskShift得到buckets,赋值给p10
    1. 获取第一次查找的indexp1_cmdLSR>>eor^(异或)。首先p12 = p1 ^ (p1 >> 7),再p12 = p12 & mask就得到了要查找的下标index
查找缓存

首先查找 buckets首地址到 index 之间

image.png

  • 先获取到要查找的bucket,这里获取bucket的方式是内存平移,相当于b[i]一样,然后我们进行遍历查找。
  • ldp是同时操作两个寄存器,首先将buckets地址左移一个bucket单位,然后得到的impsel分别赋值给p17p9
  • p1是要存的sel,当和p9不同时,走步骤3,当sel不存在时,走MissLabelDynamic,当第一个查找的bucket>buckets首地址时,buckets地址再左移一个bucket单位继续对比。
  • p1 = p9,则缓存命中,走CacheHit

image.png

  • 这里是对imp进行解码,然后直接返回imp
  • 查找过程如下:

image.png

  • 如何这个判断里没有找到,将继续在下面判断查找

再从最后一个bucket往index遍历查找

image.png

  • 三步走
      1. 获取最后一个bucket
      1. 获取index处的bucket
      1. 从最后一个bucketindex处的bucket遍历。
  • 图解

image.png

  • 如果执行完,还是没有找到,就会走__objc_msgSend_uncached方法:

image.png

整体流程图

image.png

汇编相关符号

汇编符号
cmd比较
b.le小于
b.eq相等
ldr将指令读到目标寄存器
mov将数据存到寄存器
and&(按位与)
tbnz不为0跳转
LSR>>(右移)
eor^(按位异或)
LSL<<(左移)
ldp操作两个寄存器
b.ne不相等
cbz是否存在,不存在跳转
br跳转指令
b.hs大于等于
b.hi大于
ccmp条件比较