iOS objc_msgSend分析

201 阅读3分钟

什么是objc_msgSend我我们感觉既熟悉又陌生,我们一步一步探索他

首先objc_msgSend这个是怎么来的

先定义一个LGPerson类

然后main.m 里面实现一下

#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@property (nonatomic, copy) NSString *name;
- (void)sayHello;
- (void)sayNB;
@endNS_ASSUME_NONNULL_END

int main(int argc, const char * argv[]) {    @autoreleasepool {        LGPerson *person = [[LGPerson alloc] init];;        [person sayNB];    }    return 0;}

这里定义了一个person 的对象,实现了[person sayNB];对象方法

利用 cland -rewrite-objc main.m 看一下底层源码实现

可以看到 对象方法的实现是由((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB")); 这句代码实现,惊不惊喜意不意外

objc_msgSend是用汇编写的

汇编的优势:1:C语音中不可能通过写一个函数来保留未知的参数并且跳转到一个任意的函数指针。C语言没有满足做这件事情的必要特性

2,Objc_msgSend汇编更接近底层,效率更高

通过这段代码可以看到方法的实现是通过objc_msgSend 关联对象以及内部的方法名来具体的实现


int main(int argc, const char * argv[]) {    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));    }    return 0;}


现在知道objc_msgSend的由来,我们通过底层汇编来分析一下objc_msgSend的内部的具体流程实现

arm64里面有31位寄存器,这31位中每一个都是64位,其中 x0-x7为其存放参数

cmd p0;为第一个参数,判断self接受者是否存在,如果为空则代表接受者不存在 LReturnZero,

如果存在则标记x0关联isa,isa的存在价值是可以通过isa知道元类,获取元类里面的方法



GetClassFromIsa_p16 //p16 = class ; 这句代码是为了 让isa & mask 来获取真正的Class 目的就是取面具 

通过isa找到接受者之后,来读取他的方法

CacheLookUp 读取方法

方法的读取方法有三种:

NORMAL  ;一般的方法差早

GETIMP  ;快速直接得到IMP

LOOKUP ;慢速遍历查找


详述一下CacheLookup  cache中存在的情况

ldp p10, p11, [x16, #CACHE] ;通过地址偏移16位找到cache 

#if !__LP64__

and w11, w11, 0xffff // p11 = mask  ;-->这个是 拿到32位mask 与_cmd进行与运算

and w12, w1, w11 ;-->w1 & w11来得到 w12 (当前的hash下标)

add p12, p10, p12, LSL #(1+PTRSHIFT) ;->通过地址拿到当前的buckets

ldp p17, p9, [x12] ; ->拿到buckets内部相应的 imp sel

cmp p9, p1 ;->判断是否对应_cmd 如果对应说明 cache中存在 则直接返回内存中的 imp 


如果Cache中找不到的话 则

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			// loop3:	// wrap: p12 = first bucket, w11 = mask	add	p12, p12, w11, UXTW #(1+PTRSHIFT)		                        // p12 = buckets + (mask << 1+PTRSHIFT)	// 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


一块拼一下里面的CheckMiss ,cache中不存在的情况 CacheLookup NORMAL

 __objc_msgSend_uncached

.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


细品 __objc_msgSend_uncached 则重点是MethodTableLookup(里面貌似也就这么一句重要的)

.endmacro
	STATIC_ENTRY __objc_msgSend_uncached	UNWIND __objc_msgSend_uncached, FrameWithNoSaves	// THIS IS NOT A CALLABLE C FUNCTION	// Out-of-band p16 is the class to search
	MethodTableLookup
	TailCallFunctionPointer x17	END_ENTRY __objc_msgSend_uncached


那么接着品  MethodTableLookup

.macro MethodTableLookup	// push frame	SignLR	stp	fp, lr, [sp, #-16]!	mov	fp, sp	// save parameter registers: x0..x8, q0..q7	sub	sp, sp, #(10*8 + 8*16)	stp	q0, q1, [sp, #(0*16)]	stp	q2, q3, [sp, #(2*16)]	stp	q4, q5, [sp, #(4*16)]	stp	q6, q7, [sp, #(6*16)]	stp	x0, x1, [sp, #(8*16+0*8)]	stp	x2, x3, [sp, #(8*16+2*8)]	stp	x4, x5, [sp, #(8*16+4*8)]	stp	x6, x7, [sp, #(8*16+6*8)]	str	x8,     [sp, #(8*16+8*8)]	// receiver and selector already in x0 and x1	mov	x2, x16

	bl	__class_lookupMethodAndLoadCache3	// IMP in x0	mov	x17, x0

一系列的内存偏移查找在内存中寻找方法(class_data_bits_t-> class_rw_t -> class_ro_t -> method_list_t) 通过捕捉 得到__class_lookupMethodAndLoadCache3 

继续品__class_lookupMethodAndLoadCache3

id objc_msgSend_uncached(id receiver, SEL op, struct objc_class *cls){  IMP  imp =   _class_lookupMethodAndLoadCache3(receiver, op, cls);  return imp(receiver, op, ....);}

找到对应方法并缓存到cache 中,然后返回IMP


汇编方法从查找戛然而止,不过楞感觉缺点什么,欢迎路过高手,请多多指教