Runtime objc_msgSend

279 阅读6分钟

Runtime

1. Runtime介绍

从编译时和链接时到运行时,Objective-C语言会尽可能多地推迟决策。只要有可能,它就会动态地执行操作。这意味着该语言不仅需要编译器,还需要运行时系统来执行编译后的代码。运行时系统充当Objective-C语言的一种操作系统。这就是使语言有效的原因。

本文档着眼于NSObject类以及Objective-C程序如何与运行时系统交互。特别是,它检查了在运行时动态加载新类并将消息转发到其他对象的范例。它还提供有关在程序运行时如何查找有关对象的信息的信息。

Objective-C运行时 描述了Objective-C运行时支持库的数据结构和功能。您的程序可以使用这些接口与Objective-C运行时系统进行交互。例如,您可以添加类或方法,或获取已加载类的所有类定义的列表。

2. Runtime Legacy and Modern Versions

Objective-C运行时有两个版本-“现代”和“旧版”。现代版本是在Objective-C 2.0中引入的,其中包括许多新功能。旧版运行时的编程接口在“ Objective-C 1运行时参考”中进行了介绍; 《 Objective-C运行时参考》中介绍了现代版本的运行时的编程接口。

最值得注意的新功能是现代运行时中的实例变量是“非脆弱的”:

  • 在旧版运行时中,如果更改类中实例变量的布局,则必须重新编译从其继承的类。
  • 在现代运行时中,如果更改类中实例变量的布局,则不必重新编译从其继承的类。

3.Runtime 交互

Objective-C程序在三个不同的级别与运行时系统进行交互:

  1. 通过Objective-C源代码;
  2. 通过Foundation框架的NSObject类中定义的方法;
  3. 通过直接调用运行时函数。
3.1 Objective-C Source Code

在大多数情况下,运行时系统会在后台自动运行。您只需编写和编译Objective-C源代码即可使用它。 当您编译包含Objective-C类和方法的代码时,编译器会创建实现语言动态特性的数据结构和函数调用。数据结构捕获在类和类别定义以及协议声明中找到的信息;它们包括在Objective-C编程语言中定义类和协议中讨论的类和协议对象,以及方法选择器,实例变量模板以及从源代码中提取的其他信息。如消息中所述,主要的运行时功能是发送消息的功能。它由源代码消息表达式调用。

Person *p1 = [[Person alloc] init];

编译后C++后

Person *p1 = objc_msgSend( (objc_msgSend((id)objc_getClass("Person"), sel_registerName("alloc")) , sel_registerName("init"));

使用objc_msgSend函数测试

// 1. 初始化实例对象
Person *p1 = [Person alloc];
// 2. 调用实例方法
[p1 eat]; 
// 3. 直接调用父类方法
NSLog(@"%@",[p1 description]);

// 1. 初始化实例对象, Person 类没有 alloc 方法,最后从NSObject的元类里面找    
Person *p2 = (__bridge Person *)(__bridge void *)objc_msgSend([Person class], sel_registerName("alloc"));
// 2. 调用实例方法
objc_msgSend(p2, @selector(eat));
// 3. 直接调用父类方法
struct objc_super p2SuperClass = {p2, [NSObject class]};
NSLog(@"%@",objc_msgSendSuper(&p2SuperClass, @selector(description)));

output:

0x100477df0 eat
<Person: 0x100477df0>
0x10060b4b0 eat
<Person: 0x10060b4b0>
3.2 NSObject Methods

Cocoa中的大多数对象都是NSObject类的子类,因此大多数对象都继承了它定义的方法。 (值得注意的例外是NSProxy类;有关更多信息,请参见消息转发。)因此,其方法将建立每个实例和每个类对象固有的行为。但是,在少数情况下,NSObject类仅定义了如何完成操作的模板。它本身并没有提供所有必要的代码。

例如,NSObject类定义了一个描述实例方法,该方法返回一个描述该类内容的字符串。这主要用于调试-GDB print-object命令打印从此方法返回的字符串。 NSObject对该方法的实现不知道该类包含什么,因此它返回一个包含对象名称和地址的字符串。 NSObject的子类可以实现此方法以返回更多详细信息。例如,Foundation类NSArray返回其包含的对象的描述列表。

一些NSObject方法只是查询运行时系统以获取信息。这些方法允许对象执行自省。此类方法的示例是class方法,它要求对象标识其类。 isKindOfClass:和isMemberOfClass:测试对象在继承层次结构中的位置; responsesToSelector:指示对象是否可以接受特定消息; conformsToProtocol:指示对象是否声称要实现特定协议中定义的方法;和methodForSelector ::提供方法实现的地址。像这样的方法使对象能够自我反省。

3.3 Runtime Functions

运行时系统是一个动态共享库,具有一个公共接口,该公共接口由位于目录/usr/include/objc中的头文件中的一组函数和数据结构组成。这些功能中的许多功能使您可以使用普通C语言来复制编写Objective-C代码时编译器所做的工作。其他构成了通过NSObject类的方法导出的功能的基础。这些功能使开发与运行时系统的其他接口并产生扩展开发环境的工具成为可能。在Objective-C中进行编程时不需要它们。但是,编写Objective-C程序时,某些运行时函数有时可能会有用。所有这些功能在“ Objective-C运行时参考”中都有记录。

Snip20200919_20

4. objc_msg_send 方法解析

4.1 GetClassFromIsa_p16 从 isa 获取到对象的 Class,返回值p16
/********************************************************************
 * GetClassFromIsa_p16 src
 * src is a raw isa field. Sets p16 to the corresponding class pointer.
 * The raw isa might be an indexed isa to be decoded, or a
 * packed isa that needs to be masked.
 * On exit:
 *   $0 is unchanged
 *   p16 is a class pointer
 *   x10 is clobbered
 ********************************************************************/
 
.macro GetClassFromIsa_p16 /* src */
// 64 位 arm 架构 获取 Class 对象 
#if __LP64__
	// 64-bit packed isa
	// p16 = isa & ISA_MASK
	and	p16, $0, #ISA_MASK  
#endif

.endmacro
4.2 CheckMiss
  1. GETIMP LGetImpMiss
  2. NORMAL __objc_msgSend_uncached
  3. LOOKUP __objc_msgLookup_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
.else
.abort oops
.endif
.endmacro
4.3 JumpMiss
  1. GETIMP LGetImpMiss
  2. NORMAL __objc_msgSend_uncached
  3. LOOKUP __objc_msgLookup_uncached
.macro JumpMiss
.if $0 == GETIMP
	b	LGetImpMiss
.elseif $0 == NORMAL
	b	__objc_msgSend_uncached
.elseif $0 == LOOKUP
	b	__objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
4.4 CacheHit

如果命中缓存,那么x17 = cached IMP ,x12 = address of cached IMP

// CacheHit: x17 = cached IMP, x12 = address of cached IMP, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
	TailCallCachedImp x17, x12, x1, x16	// authenticate and call imp
.elseif $0 == GETIMP
	mov	p0, p17
	cbz	p0, 9f			// don't ptrauth a nil imp
	AuthAndResignAsIMP x0, x12, x1, x16	// 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, x16	// authenticate imp and re-sign as IMP
	ret				// return imp via x17
.else
.abort oops
.endif
.endmacro
4.5 CacheLookup NORMAL|GETIMP|LOOKUP 在方法缓存中寻找IMP

/********************************************************************
 *
 * CacheLookup NORMAL|GETIMP|LOOKUP <function>
 *
 * Locate the implementation for a selector in a class method cache.
 *
 * When this is used in a function that doesn't hold the runtime lock,
 * this represents the critical section that may access dead memory.
 * If the kernel causes one of these functions to go down the recovery
 * path, we pretend the lookup failed by jumping the JumpMiss branch.
 *
 * Takes:
 *	 x1 = selector
 *	 x16 = class to be searched
 *
 * Kills:
 * 	 x9,x10,x11,x12, x17
 *
 * On exit: (found) calls or returns IMP
 *                  with x16 = class, x17 = IMP
 *          (not found) jumps to LCacheMiss
 *
 ********************************************************************/

#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2

.macro CacheLookup
	//
	// Restart protocol:
	//
	//   As soon as we're past the LLookupStart$1 label we may have loaded
	//   an invalid cache pointer or mask.
	//
	//   When task_restartable_ranges_synchronize() is called,
	//   (or when a signal hits us) before we're past LLookupEnd$1,
	//   then our PC will be reset to LLookupRecover$1 which forcefully
	//   jumps to the cache-miss codepath which have the following
	//   requirements:
	//
	//   GETIMP:
	//     The cache-miss is just returning NULL (setting x0 to 0)
	//
	//   NORMAL and LOOKUP:
	//   - x0 contains the receiver
	//   - x1 contains the selector
	//   - x16 contains the isa
	//   - other registers are set as per calling conventions
	//
LLookupStart$1:
#define SUPERCLASS       __SIZEOF_POINTER__
#define CACHE            (2 * __SIZEOF_POINTER__) 
// p1 = SEL, p16 = isa
// 1. p11 = x16 + CACHE (2 * 8) 拿到 cache 的地址
	ldr	p11, [x16, #CACHE]		// p11 = mask|buckets 
// 2. p10 = buckets 
// p10 = p11 & #0x0000ffffffffffff 取后面的48位 获取到buckets
and	p10, p11, #0x0000ffffffffffff
// 3. x12 = _cmd & mask 获取到mask
// p1 = _cmd & ( p11 >> 48 == mask)
and	p12, p1, p11, LSR #48	

#define PTRSHIFT 3
// 4. p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// _cmd & mask 获取缓存的哈希值
add	p12, p10, p12, LSL #(1+PTRSHIFT)
//	 5. 读取 bucket ,p9 = sel  p 17 = imp          
ldp	p17, p9, [x12]		// {imp, sel} = *bucket
// 6. 比较 p1(_cmd) 和 sel 是否相等
1:	cmp	p9, p1			// if (bucket->sel != _cmd)
// 7.0 如果不相等,那么跳转到 2f
b.ne	2f			//     scan more
// 8. 如果相等,那么返回$0 == GETIMP
CacheHit $0			// call or return imp
// 7.1 没有命中
2:	// not hit: p12 = not-hit bucket
	CheckMiss $0			// miss if bucket->sel == 0
	// 7.2 查看bucket 是不是数组首地址
	cmp	p12, p10		// wrap if bucket == buckets
	// 7.3 相等的话 跳转到 function 3
	b.eq	3f
	// 7.4 得到前一个bucket {imp, sel} = *--bucket
	ldp	p17, p9, [x12, #-BUCKET_SIZE]!
	// 7.5 重新进入 函数1 
	b	1b			// loop
// 8.1 p12 = first bucket, w11 = mask
3:	// wrap: p12 = first bucket, w11 = mask
add	p12, p12, p11, LSR #(48 - (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

LLookupEnd$1:
LLookupRecover$1:
3:	// double wrap
	JumpMiss $0

.endmacro

4.6 id objc_msgSend(id self, SEL _cmd, ...);
/********************************************************************
 *
 * id objc_msgSend(id self, SEL _cmd, ...);
 * IMP objc_msgLookup(id self, SEL _cmd, ...);
 * 
 * objc_msgLookup ABI:
 * IMP returned in x17 
 * x16 reserved for our use but not used
 *
 ********************************************************************/
 // 1. 进入 _objc_msgSend
	ENTRY _objc_msgSend 
	UNWIND _objc_msgSend, NoFrame
// 2. cmp 比较指令 检查p0 是否是 nil  nil check and tagged pointer check
	cmp	p0, #0			
/* 3. (MSB tagged pointer looks negative) tagged pointer < 0
   如果是 tagged 就跳转到 LGetIsaDone, 
   如果是 nil 就跳转到 LReturnZero
*/
   b.le	LNilOrTagged   
// 4. p13 = isa, 读取x0的地址,赋予p13, p13即isa的地址
	ldr	p13, [x0]
// 5. p16 = class ,获取到类对象(元类)
	GetClassFromIsa_p16 p13		
LGetIsaDone:
	// calls imp or objc_msgSend_uncached
	CacheLookup NORMAL, _objc_msgSend
LReturnZero:
	// x0 is already zero  退出 _objc_msgSend, 这样就是我们向 nil 对象发送消息却不会崩溃的原因
	END_ENTRY _objc_msgSend
4.6 __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
4.7 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)]

	// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER) 
	// receiver and selector already in x0 and x1
	mov	x2, x16
	mov	x3, #3
	// 跳转到
	bl	_lookUpImpOrForward

	// 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 内容之后会定位到 _lookUpImpOrForward。真正的方法查找流程核心逻辑是位于 _lookUpImpOrForward 里面的。 但是我们全局搜索 _lookUpImpOrForward 会发现找不到,这是因为此时我们会从 汇编 跳入到 C/C++。所以去掉一个下划线就能找到了

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;
    .....
}

Apple Runtime Doc