OC 方法的本质

1,226 阅读6分钟

探索案例

/********对象声明*********/
@interface LGPerson : NSObject
- (void)sayHello;
+ (void)sayNB;
@end

@interface OStudent : LGPerson
- (void)sayCode;
+ (void)sayGood;
@end

/********测试代码*********/
OStudent *s = [OStudent new];
[s sayCode];           //对象方法
[OStudent sayGood];    //类方法

使用clang命令把oc代码编译成c代码分析

clang -rewrite-objc main.m -o main.cpp

//经过整理,留下方法调用相关代码
OStudent *s = ((OStudent *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("OStudent"), sel_registerName("new"));
/*
objc_msgSend(s,sel_registerName("sayCode"))
*/
objc_msgSend((id)s, sel_registerName("sayCode"));
/*
objc_msgSend(objc_getClass("OStudent"),sel_registerName("sayGood"))
*/
((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("OStudent"), sel_registerName("sayGood"));

  • 都将objc_msgSend强转为(void (*)(id, SEL))(void *),用于适应发送消息的格式
  • 这里的objc_msgSend第一个参数是接收消息的对象,第二个参数是SEL
  • sel_registerName函数的作用是向runtime注册一个方法名;如果方法名已经注册,则放回已经注册的SEL。
  • 经过观察对象方法类方法的区别在于接收者不同
  • 这里也说明了对象方法类方法本质上是没有区别的,区别在于存储的位置不同

objc_msgSend初探

objc_msgSend就是消息发送的实现API,通过源码搜索发现objc_msgSend的具体实现是通过汇编完成,OC中所有的消息发送都会调用该方法,这样也对方法执行速度有了很高的要求,这应该是objc_msgSend选择使用汇编实现的原因。

使用小提示 objc_msgSend使用过程中,如果直接使用objc_msgSend就会报错 解决方法

  1. objc_msgSend 强转成 (void (*)(id, SEL))(void *)((void (*)(id, SEL))(void *)objc_msgSend)(s, @selector(sayCode));
  2. 关闭内存检查
    关闭msgSend类型检查

objc_msgSend梳理

源码中搜索objc_msgSend,

在源码中objc_msgSend有几种架构的实现,这里拿最常见的arm64(objc-msg-arm64.s)举例。

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:
	CacheLookup NORMAL		// calls imp or objc_msgSend_uncached

#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 流程分析

	cmp	p0, #0			// 判断p0是否为空,p0是第一个参数(消息接收对象)
#if SUPPORT_TAGGED_POINTERS // 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:
	CacheLookup NORMAL		// calls imp or objc_msgSend_uncached
  • SUPPORT_TAGGED_POINTERS 在objc2的64位环境下是1,所以执行b.le LNilOrTagged
  • b.le 汇编命令表示,前一个cmp命令对比值小于等于,那么执行标号,否则往下执行
  • 如果消息接受者Nil或者是tagged pointer,就会执行LNilOrTagged标记
  • 继续往下执行说明消息接受者isa指针
  • GetClassFromIsa_p16isa&ISA_MASK操作拿到isa指向对象的指针
  • LGetIsaDone isa处理完成执行的标记,之后进行方法的查找CacheLookup NORMAL
LNilOrTagged
#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
  • b.eq LReturnZero 如果消息接受者为空return 0;这也是给nil发送消息不会有任何反应的原因所在
  • 完成之后执行LGetIsaDone标记,进行方法的查找CacheLookup NORMAL
CacheLookup
.macro CacheLookup
	// p1 = SEL, p16 = isa
	ldp	p10, p11, [x16, #CACHE]	// p10 = buckets, p11 = occupied|mask
#if !__LP64__
	and	w11, w11, 0xffff	// p11 = mask
#endif
	and	w12, w1, w11		// x12 = _cmd & mask
	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
	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
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:	// double wrap
	JumpMiss $0
	
.endmacro
  • CacheLookup是一个宏macro
  • CacheLookup完成了在cache中查找IMP缓存,这种查找方式称之为 快速查找
  • CacheLookup 有三个参数 CacheLookup NORMAL|GETIMP|LOOKUP,这次传入的是NORMAL
  • cache中查找有三种结果:CacheHitCheckMissadd
CacheHit 缓存命中IMP
#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2

// CacheHit: x17 = cached IMP, x12 = address of cached IMP, x1 = SEL
.macro CacheHit
.if $0 == NORMAL
	TailCallCachedImp x17, x12, x1	// authenticate and call imp
.elseif $0 == GETIMP
	mov	p0, p17
	cbz	p0, 9f			// don't ptrauth a nil imp
	AuthAndResignAsIMP x0, x12, x1	// authenticate imp and re-sign as IMP
9:	ret				// return IMP
.elseif $0 == LOOKUP
	// No nil check for ptrauth: the caller would crash anyway when they
	// jump to a nil IMP. We don't care if that jump also fails ptrauth.
	AuthAndResignAsIMP x17, x12, x1	// authenticate imp and re-sign as IMP
	ret				// return imp via x17
.else
.abort oops
.endif
.endmacro

传入的是NORMAL就返回IMP

CheckMiss缓存未命中IMP
.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
  • 出入的是NORMAL,未命中缓存执行的是__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 p16 is the class to search
	
	MethodTableLookup
	TailCallFunctionPointer x17

	END_ENTRY __objc_msgSend_uncached
  • __objc_msgSend_uncached是对没有命中缓存的处理
  • 未命中缓存接下来就是去查找对象中的方法列表MethodTableLookup
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
	
	// restore registers and return
	ldp	q0, q1, [sp, #(0*16)]
	ldp	q2, q3, [sp, #(2*16)]
	ldp	q4, q5, [sp, #(4*16)]
	ldp	q6, q7, [sp, #(6*16)]
	ldp	x0, x1, [sp, #(8*16+0*8)]
	ldp	x2, x3, [sp, #(8*16+2*8)]
	ldp	x4, x5, [sp, #(8*16+4*8)]
	ldp	x6, x7, [sp, #(8*16+6*8)]
	ldr	x8,     [sp, #(8*16+8*8)]

	mov	sp, fp
	ldp	fp, lr, [sp], #16
	AuthenticateLR

.endmacro
  • MethodTableLookup查找对象的方法列表
  • 一堆代码的是做准备工作和收尾工作,为__class_lookupMethodAndLoadCache3函数做铺垫
  • __class_lookupMethodAndLoadCache3在汇编中并没有实现具体实现,实现是用c写的_class_lookupMethodAndLoadCache3
_class_lookupMethodAndLoadCache3

至此方法查找中的第一步快速查找已经结束,接下来的是慢速查找

Super在方法调用中的作用

案例

@interface LGPerson : NSObject
- (void)sayHello;
+ (void)sayNB;
@end

@interface OStudent : LGPerson
- (void)sayCode;
+ (void)sayGood;
@end

@implementation LGPerson

- (void)sayHello {
    NSLog(@"%s",sel_getName(_cmd));
}

+ (void)sayNB {
    NSLog(@"%s",sel_getName(_cmd));
}

@end

@implementation OStudent

- (void)sayCode {
    [super sayHello];
}

+ (void)sayGood {
    [super sayNB];
}

@end

//调用代码
OStudent *s = [OStudent new];
[s sayCode];
[OStudent sayGood];

编译后的c代码后 sayGoodsayCode的实现

//整理后的代码
struct __rw_objc_super { 
	struct objc_object *object; 
	struct objc_object *superClass; 
};
static void _I_OStudent_sayCode(OStudent * self, SEL _cmd) {
    __rw_objc_super

    // 向父类发消息(对象方法)
    struct __rw_objc_super t_super;
    t_super.receiver = self;
    t_super.super_class = class_getSuperclass(objc_getClass("OStudent"));
    objc_msgSendSuper(&t_super, sel_registerName("sayHello"));
}


static void _C_OStudent_sayGood(Class self, SEL _cmd) {
   
    //向父类发消息(类方法)
    struct __rw_objc_super t_ClassSuper;
    t_ClassSuper.receiver = [s class];
    t_ClassSuper.super_class = class_getSuperclass(objc_getMetaClass("OStudent"));// 元类的父类
    objc_msgSendSuper(&t_ClassSuper, sel_registerName("sayNB"));
}
  • 使用super调用父类方法会使用objc_msgSendSuper发送消息
  • objc_msgSendSuper第一个参数是一个结构体指针,第二个参数是SEL
  • __rw_objc_super中成员object表示消息接收者,super_class表示父类对象
objc_msgSendSuper
	ENTRY _objc_msgSendSuper
	UNWIND _objc_msgSendSuper, NoFrame

	ldp	p0, p16, [x0]		// p0 = real receiver, p16 = class
	CacheLookup NORMAL		// calls imp or objc_msgSend_uncached

	END_ENTRY _objc_msgSendSuper
  • objc_msgSendSuper查找父类对象方法缓存
  • 不需要处理isa,只需要直接查找缓存CacheLookup NORMAL

总结

  1. OC 方法调用的本质就是通过对象+SEL找到IMP,这种动态调用方法叫做消息发送
  2. 实现这部分功能的是objc_msgSend家族的函数
  3. 方法查找分为快速查找慢速查找