objc_msgSend过程解析

1、objc_msgSend相关函数

1.1 函数说明

我们知道,OC的方法调用在底层会被编译成objc_msgSend函数进行调用。

objc_msgSend基础的函数主要有以下2个:

OBJC_EXPORT void
objc_msgSend(void /* id self, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

这两个函数主要用于消息的发送。

通过objc/message.h头文件,在arm64上,只有这两个函数可用。

在其他架构上,还有几个变形的函数,分别如下:

(1)objc_msgSend_stret:返回结构体类型的消息发送

/* Struct-returning Messaging Primitives
 *
 * Use these functions to call methods that return structs on the stack. 
 * On some architectures, some structures are returned in registers. 
 * Consult your local function call ABI documentation for details.
 * 
 * These functions must be cast to an appropriate function pointer type 
 * before being called. 
 */
objc_msgSend_stret(void /* id self, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0)
    OBJC_ARM64_UNAVAILABLE;

OBJC_EXPORT void
objc_msgSendSuper_stret(void /* struct objc_super *super, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0)
    OBJC_ARM64_UNAVAILABLE;

(2)objc_msgSend_fpret,objc_msgSend_fp2ret:返回浮点数类型的消息转发

/* Floating-point-returning Messaging Primitives
 * 
 * Use these functions to call methods that return floating-point values 
 * on the stack. 
 * Consult your local function call ABI documentation for details.
 * 
 * arm:    objc_msgSend_fpret not used
 * i386:   objc_msgSend_fpret used for `float`, `double`, `long double`.
 * x86-64: objc_msgSend_fpret used for `long double`.
 *
 * arm:    objc_msgSend_fp2ret not used
 * i386:   objc_msgSend_fp2ret not used
 * x86-64: objc_msgSend_fp2ret used for `_Complex long double`.
 *
 * These functions must be cast to an appropriate function pointer type 
 * before being called. 
 */

1.2 例子

下面以arm64举例说明,声明下面两个类。

@interface XTPerson : NSObject

@property (copy, nonatomic) NSString *name;

@end

@implementation XTPerson
@synthesize name = _name;

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

- (CGRect)myRect {
    NSLog(@"%s", __func__);
    return CGRectMake(0, 0, 20, 20);
}

- (CGFloat)height {
    NSLog(@"%s", __func__);
    return 1.8f;
}

- (NSString *)name {
    NSLog(@"%s", __func__);
    return _name;
}

- (void)setName:(NSString *)name {
    NSLog(@"%s", __func__);
    _name = name;
}

@end

@interface XTWorker : XTPerson
- (void)work;
@end

@implementation XTWorker

- (void)work {
    NSLog(@"worker work");
}

@end

(1)objc_msgSend调用

以arm64为例,在真机上如下调用:

    XTPerson *person = [[XTPerson alloc] init];
    void* (*objc_msgSend_func) (id, SEL, ...);
    objc_msgSend_func = (void* (*) (id, SEL, ...)) objc_msgSend;
    objc_msgSend_func(person, @selector(work));
    [person setName:@"Peter"];
    NSLog(@"%@", objc_msgSend_func(person, @selector(name)));

可以看出进行函数指针的转换后,可以通过函数指针对objc_msgSend函数进行调用。

(2)objc_msgSendSuper调用

下面的例子说明如何去调用worker父类的work方法。

     XTWorker *worker = [[XTWorker alloc] init];
    void* (*objc_msgSendSuper_func) (struct objc_super *, SEL, ...) = (void* (*) (struct objc_super *, SEL, ...))objc_msgSendSuper;
    struct objc_super xtSuper = {worker, [XTPerson class]};
    objc_msgSendSuper_func(&xtSuper, @selector(work));

2、汇编过程

objc_msgSend实际上做的就是从某个sel查找对应的函数地址的过程。

2.1 函数入口

通过对苹果开源的objc源码可以看出,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, 1, x0	// p16 = class
LGetIsaDone:
	// calls imp or objc_msgSend_uncached
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

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

从以上代码可以看出,首先会判断消息的发送者是否为nil,如果是,则直接返回。

2.2  CacheLookup

如果接受者不为nil,将isa赋值到p13中,通过isa获取class对象,并存放在p16中,然后走CacheLookup,只看amr64,实现如下:

	mov	x15, x16			// stash the original isa
LLookupStart\Function:
	// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
	ldr	p10, [x16, #CACHE]				// p10 = mask|buckets
	lsr	p11, p10, #48			// p11 = mask
	and	p10, p10, #0xffffffffffff	// p10 = buckets
	and	w12, w1, w11			// x12 = _cmd & mask
#endif

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

						// do {
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
	cmp	p9, p1				//     if (sel != _cmd) {
	b.ne	3f				//         scan more
						//     } else {
2:	CacheHit \Mode				// hit:    call or return imp
						//     }
3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
	cmp	p13, p10			// } while (bucket >= buckets)
	b.hs	1b

	// wrap-around:
	//   p10 = first bucket
	//   p11 = mask (and maybe other bits on LP64)
	//   p12 = _cmd & mask
	//
	// A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
	// So stop when we circle back to the first probed bucket
	// rather than when hitting the first bucket again.
	//
	// Note that we might probe the initial bucket twice
	// when the first probed slot is the last entry.


#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
	add	p13, p10, w11, UXTW #(1+PTRSHIFT)
						// p13 = buckets + (mask << 1+PTRSHIFT)
#endif
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
						// p12 = first probed bucket

						// do {
4:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
	cmp	p9, p1				//     if (sel == _cmd)
	b.eq	2b				//         goto hit
	cmp	p9, #0				// } while (sel != 0 &&
	ccmp	p13, p12, #0, ne		//     bucket > first_probed)
	b.hi	4b

LLookupEnd\Function:
LLookupRecover\Function:
	b	\MissLabelDynamic

上述过程其实是从cache缓存中查找某个sel对应的函数地址的过程,伪代码大概如下:

p10 = mask|buckets
buckets = p10 & #0xffffffffffff
mask = p10 >> 48
index = cmd & mask
for (i = index; i >= 0; i--) {
    if (buckets[i].sel == search_sel) {
        goto CacheHit
    } else {
        if (buckets[i].sel == NULL) {
            goto MissLabelDynamic
        } else {
            //查找下一个缓存位置
        }
    }
}

for (i = mask; i > index; ++i) {
    if (buckets[i].sel == search_sel) {
        goto CacheHit
    } else {
        if (buckets[i].sel == NULL) {
            goto MissLabelDynamic
        } else {
            //查找下一个缓存位置
        }
    }
}

CacheHit:
XX
MissLabelDynamic:
_objc_msgSend_uncached

问题1:为什么index初始化为mask&cmd

因为对于每个sel,其存放在缓存中的位置序号是按照以下函数计算。

static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
    value ^= value >> 7;
#endif
    return (mask_t)(value & mask);
}

问题2:为什么buckets[index]有值,但是不等于查找的sel时,index--?

因为缓存存放发送哈希冲突时,在arm64时,是通过index--来解决冲突。具体如下:

#if CACHE_END_MARKER
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}
#elif __arm64__
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}
#else
#error unexpected configuration
#endif

2.3 objc_msgSend_uncached

如果缓存找到直接调用函数,如果没有找到,调用_objc_msgSend_uncached。

	STATIC_ENTRY __objc_msgSend_uncached
	UNWIND __objc_msgSend_uncached, FrameWithNoSaves
	
	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band r10 is the searched class

	// r10 is already the class to search
	MethodTableLookup NORMAL	// r11 = IMP
	jmp	*%r11			// goto *imp

	END_ENTRY __objc_msgSend_uncached

其核心代码是MethodTableLookup

.macro MethodTableLookup

	SAVE_REGS MSGSEND

	// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
.if $0 == NORMAL
	// receiver already in a1
	// selector already in a2
.else
	movq	%a2, %a1
	movq	%a3, %a2
.endif
	movq	%r10, %a3
	movl	$$3, %a4d
	call	_lookUpImpOrForward

	// IMP is now in %rax
	movq	%rax, %r11

	RESTORE_REGS MSGSEND

.if $0 == NORMAL
	test	%r11, %r11		// set ne for nonstret forwarding
.else
	cmp	%r11, %r11		// set eq for stret forwarding
.endif

.endmacro

这里会调用lookUpImpOrForward。

该实现是一个C函数,从而退出整个汇编的过程。

从上述的过程中可以看出,汇编代码实现的是从缓存中查找sel对应的impl,如果缓存没有找到,则进入lookUpImpOrForward流程。

2.4 总的流程图

上述整个过程的流程图如下:

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

chenxintao

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值