OC底层原理之-objc_msgSend方法查找(上)

1,411 阅读3分钟

前言

之前的文章我们已经对方法存储类的cache_t做了分析(cache_t分析传送门)。我们上篇文章提到有个问题,就是如果cache_t中已经存在该方法,再次调用该方法的时候,不会走cache_t的写入方法。今天我们就来探究下原因。

通过clang分析方法调用

查看方法调用最直观的方式就是用clang。下面我们在Person写如下代码,在ViewController写如下代码

@interface Person : NSObject
- (void)eatFood;
- (void)goToWork;
+ (void)goToBed;
@end

@implementation Person
- (void)eatFood {
    NSLog(@"eat");
}
- (void)goToWork {
    NSLog(@"work");
}
+ (void)goToBed {
    NSLog(@"bed");
}
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    Person *person = [Person alloc];
    [person eatFood];
    [person goToWork];
    [Person goToBed];
    NSLog(@"%@--->%p", person, &person);
}

我们使用clang命令输出.cpp文件 我们打开ViewController.cpp文件。搜索方法名eatFood,搜到如下代码:

通过上面我们发现代码转成c++后,方法调用的本质就变成了objc_msgSend发送.(id)person是接受者sel_registerName("eatFood")类似于sel

我们做下验证,在源码中写如下代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        Person *person = [Person alloc];
        objc_msgSend(person, sel_registerName("eatFood"));
        objc_msgSend(person, sel_registerName("goToWork"));
        objc_msgSend(objc_getClass("Person"), sel_registerName("goToBed"));
    }
    return 0;
}

上面是我们用objc_msgSend调用Person的对象方法以及类方法,我们运行下 发现打印正确,说明方法调用在底层就是使用objc_msgSend进行的。下面我们要分析下objc_msgSend是如何找到方法并调用的。

objc_msgSend的解析

我们在.cpp文件发现objc_msgSend,那么objc_msgSend的底层实现应该在汇编里。底层实现使用汇编的好处:1.效率高,速度快。2.类型的不确定性。

初探objc_msgSend

我们探究的是真机也就是arm64,在源码中搜索objc_msgSend,查看objc-msg-arm64.s文件,找到ENTRY _objc_msgSend(ENTRY汇编指令是入口,这里也就是_objc_msgSend入口),END_ENTRY _objc_msgSend指出口,也就是_objc_msgSend方法结束。 我们总览整个方法

ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame

	cmp	p0, #0			// nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
	b.eq	LReturnZero
#endif
	ldr	p13, [x0]    	// p13 = isa
	GetClassFromIsa_p16 p13		// p16 = class
LGetIsaDone:
	// calls imp or objc_msgSend_uncached
	CacheLookup NORMAL, _objc_msgSend

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
	b.eq	LReturnZero		// nil check

	// tagged
	adrp	x10, _objc_debug_taggedpointer_classes@PAGE
	add	x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
	ubfx	x11, x0, #60, #4
	ldr	x16, [x10, x11, LSL #3]
	adrp	x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
	add	x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
	cmp	x10, x16
	b.ne	LGetIsaDone

	// ext tagged
	adrp	x10, _objc_debug_taggedpointer_ext_classes@PAGE
	add	x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
	ubfx	x11, x0, #52, #8
	ldr	x16, [x10, x11, LSL #3]
	b	LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

LReturnZero:
	// x0 is already zero
	mov	x1, #0
	movi	d0, #0
	movi	d1, #0
	movi	d2, #0
	movi	d3, #0
	ret

	END_ENTRY _objc_msgSend

下面我们开始对重要的地方进行分析

objc_msgSend解析

上面我们看到了objc_msgSend整个汇编实现,下面我们对objc_msgSend方法主要过程简单分析一下

  • ldr p13, [x0]-->让p13等于isa指针
  • GetClassFromIsa_p16 p13-->通过isa指针获取Class
  • LGetIsaDone:-->通过isa获取Class完毕
  • CacheLookup NORMAL, _objc_msgSend-->开始缓存查找 到这一步我们就进入CacheLookup进行缓存查找了

CacheLookup解析

总览方法

LLookupStart$1:

   // p1 = SEL, p16 = isa
   ldr	p11, [x16, #CACHE]				// p11 = mask|buckets

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
   and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
   and	p12, p1, p11, LSR #48		// x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
   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


   add	p12, p10, p12, LSL #(1+PTRSHIFT)
   	             // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

   ldp	p17, p9, [x12]		// {imp, sel} = *bucket
1:	cmp	p9, p1			// if (bucket->sel != _cmd)
   b.ne	2f			//     scan more
   CacheHit $0			// call or return imp
   
2:	// not hit: p12 = not-hit bucket
   CheckMiss $0			// miss if bucket->sel == 0
   cmp	p12, p10		// wrap if bucket == buckets
   b.eq	3f
   ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket
   b	1b			// loop

3:	// wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
   add	p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
   				// p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
   add	p12, p12, p11, LSL #(1+PTRSHIFT)
   				// p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif

   // Clone scanning loop to miss instead of hang when cache is corrupt.
   // The slow path may detect any corruption and halt later.

   ldp	p17, p9, [x12]		// {imp, sel} = *bucket
1:	cmp	p9, p1			// if (bucket->sel != _cmd)
   b.ne	2f			//     scan more
   CacheHit $0			// call or return imp
   
2:	// not hit: p12 = not-hit bucket
   CheckMiss $0			// miss if bucket->sel == 0
   cmp	p12, p10		// wrap if bucket == buckets
   b.eq	3f
   ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket
   b	1b			// loop

LLookupEnd$1:

下面我们对CacheLookup的方法过程进行分析

  • LLookupStart$1:-->很好解释,就是开始查找
  • ldr p11, [x16, #CACHE]-->p16现在是Class的isa指针,让p16指针平移16位,得到p11,现在p11就是cache_t的第一个bucket和mask混合指针(类的结构讲过)由于是真机所以cache_t中包含mask和bucket混合指针,bucket里面又包含sel和imp。在真机中maxMask是1左移64 - 48 = 16位再-1,值是0-15为1。bucketsMask是1左移48 - 4 = 44位后再-1。值是0-43为1
  • and p10, p11, #0x0000ffffffffffff-->其中0x0000ffffffffffff换成2进制是0-47位都为1,48-64位都为0。它意思就是将混合指针与0x0000ffffffffffff,这里取bucket给p10(我们在cache_t写入的时候,bucket是0-43个1,它跟0x0000ffffffffffff与不影响bucket值,__ p10第一个bucket __)
  • and p12, p1, p11, LSR #48-->是将p11向右平移48位,根据上面解释此时得到的是mask(mask是整个cache_t的大小),再将mask跟p1(sel)进行与运算,这就是cache_t写入时的哈希算法。此时p12得到的是下标(p12是传入sel的下标)
  • add p12, p10, p12, LSL #(1+PTRSHIFT)-->p10上面说了是bucket的首位,将其左移p12*4位(p12是传进来方法,通过哈希算法得到的下标,每个bucket包含sel以及imp,所以每个bucket是16字节,左移4位,就是左移16字节,下标乘于16,就拿到cache_t这个下标的bucket。)

下面是1方法

  • ldp p17, p9, [x12]-->通过bucket的结构体得到{imp, sel} = *bucket
  • cmp p9, p1-->是取的sel跟p1传进来的_cmd(就是传进来的sel)不相等
  • b.ne 2f-->如果不相等,进入2
  • CacheHit $0-->如果相等,返回imp

下面是2的方法:上面查找不相等,下面的方法是递归查找

  • CheckMiss $0-->如果从最后一个元素遍历过来都找到不到,就返回CheckMiss
  • cmp p12, p10-->我们知道p10是第一个bucket,p12是算的下标,这意思(判断下标发现不是第一个)
  • b.eq 3f-->如果下标是第一个,走3
  • ldp p17, p9, [x12, #-BUCKET_SIZE]!-->如果不是第一个,就向前取bucket,循环一次对内存偏移-1,把取的bucket给p17
  • b 1b-->执行1

下面是3的方法:上面发现是第一个bucket

  • add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))-->p11右移48-(1+3)=44位,再跟第一次通过哈希算法的得到的下标p12,再次进行哈希算法。这次得到的这个下标是cache_t的最后一位

后面再执行1,2方法。

我们上面说了,如果完整的遍历一遍没找到该方法,就会执行CheckMiss。从上面我们是不是可以看到cache_t在查找过程中有些bucket会执行2遍查找那么CheckMiss里面又是什么呢?

CheckMiss分析

.macro CheckMiss
	// miss if bucket->sel == 0
.if $0 == GETIMP
	cbz	p9, LGetImpMiss
.elseif $0 == NORMAL
	cbz	p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
	cbz	p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

这个方法分三种情况: * 1.获取imp,调用LGetImpMiss * 2.正常情况未找到,调用_objc_msgSend_uncached * 3.查找缓存没找到,调用_objc_msgLookup_uncached 到此我们对objc_msgSend缓存方法讲完了。

总结

这节课我们只要探寻了objc_msgSend底层实现,主要是在cache_t如何查找方法的,这里面都是汇编,算法也多,需要好好的去理解,下面贴张汇编的命令对照解释,方便理解 我们只是讲了发送方法是怎么去cache_t查找,这就是文章开头说的,如果方法在cache_t中,再次调用,不再走cache_t的写入方法的原因。因为方法查找找到直接返回imp,进行调用,就不再做cache_t的写入后面通过lldb进行查看调用,发现方法找不到会调用_objc_msgSend_uncached,之前说的会调用_objc_msgLookup_uncached是错误的,在此指正!

最后

最后我们整理张objc_msgSend的流程图