iOS 底层原理探索 之 Runtime运行时&方法的本质

1,042 阅读8分钟
写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。

目录如下:

  1. iOS 底层原理探索 之 alloc
  2. iOS 底层原理探索 之 结构体内存对齐
  3. iOS 底层原理探索 之 对象的本质 & isa的底层实现
  4. iOS 底层原理探索 之 isa - 类的底层原理结构(上)
  5. iOS 底层原理探索 之 isa - 类的底层原理结构(中)
  6. iOS 底层原理探索 之 isa - 类的底层原理结构(下)

以上内容的总结专栏


细枝末节整理


Runtime

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

版本和平台

Objective-C 运行时有两个版本—— modernlegacy。现代版本是在 Objective-C 2.0 中引入的,包括许多新功能。旧版运行时的编程接口在Objective-C 1 Runtime Reference 中描述;Objective-C 运行时参考中描述了现代版运行时的编程接口。

OS X v10.5 及更高版本上的 iPhone 应用程序和 64 位程序使用运行时的现代版本。其他程序(OS X 桌面上的 32 位程序)使用运行时的旧版本。

编译时

有运行时,就对应的会有编译时,顾名思义就是正在编译的时候 . 那啥叫编译呢?就是编译器帮你把源代码翻译成机器能识别的代码。

与运行时交互

Objective-C 程序在三个不同的层次上与运行时系统交互:通过 Objective-C 源代码;通过NSObjectFoundation 框架的类中定义的方法;并通过直接调用运行时函数。

Objective-C 源代码

大多数情况下,运行时系统会在幕后自动运行。您只需通过编写和编译 Objective-C 源代码来使用它。

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

NSObject 方法

Cocoa 中的大多数对象都是NSObject该类的子类,因此大多数对象都继承了它定义的方法。(值得注意的例外是NSProxy类;有关更多信息,请参阅消息转发。)因此,它的方法建立了行为这是每个实例和每个类对象所固有的。然而,在少数情况下,NSObject该类只是定义了一个模板来说明应该如何做;它本身不提供所有必要的代码。

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

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

运行时函数

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

整理

runtime.001.jpeg

我们可以通过以上三种方式来调起 runtime。上图中的调用关系是自上而下的调用。

探索

案例代码1

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //实例对象
        SMPerson *person = [SMPerson alloc];
        // 调用方法 OC
        [person doWork:@"it"];
        // 调用方法 Framework
        [person performSelector:@selector(gotoTalk)];
    }
    return 0;
}

xcrun 一下后,我们得到了 可以看到被编译成了:

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

        ((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)person, sel_registerName("doWork:"), (NSString *)&__NSConstantStringImpl__var_folders_w3_ptw7ymvs5pjf98xqvjnp86wc0000gn_T_main_2c42e2_mi_0);

        ((id (*)(id, SEL, SEL))(void *)objc_msgSend)((id)person, sel_registerName("performSelector:"), sel_registerName("say666"));
    }
    return 0;
}

我们可以看到在 OC 层面, 任何方法的执行,在底层都是一个消息发送的过程 会 调用 objc_msgSend 这个函数, 有两个参数, 一个是 方法的接受者, 一个是 消息主体 ( sel + 参数 )

如果我们想要在代码中使用 objc_msgSend 需要如下设置:

  1. xcode设置:target -> Build Settings -> App Clang Perprocessing -> Enable Strict Checking of objc_msgSend calls 设置为NO,意思是关闭编译器对objc_msgSend的检查,让你在代码里面可以自由的使用objc_msgSend。
  2. 在文件中导入runtime库,即 #import <objc/message.h>

接下来我们再看一个例子:

案例代码2

SMTeacher *teacher = [SMTeacher alloc];
        
[teacher gotoTalk];
        
struct objc_super sm_objc_super;
sm_objc_super.receiver = teacher;
sm_objc_super.super_class = SMPerson.class;
     
objc_msgSendSuper( &sm_objc_super, @selector(gotoTalk) );

控制台输出

SMObjcBuild[7319:365018] 12 it
SMObjcBuild[7319:365018] -[SMPerson gotoTalk]
SMObjcBuild[7319:365018] -[SMPerson gotoTalk]
SMObjcBuild[7319:365018] -[SMPerson gotoTalk]

通过以上两个案例代码中,我们分别通过 person 实例直接调用 方法,以及通过 实例 使用 performSelector 调用方法, 最后 还使用了 objc_msgSendSuper 向 SMPerson 的 子类 SMTeacher 发送了消息,最终 均可以输出 方法 打印的内容。

结论

  1. 在 OC 层面 我们调用的方法, 经过编译后,对应的是 objc_msgSend 函数的调用;
  2. super_class 设置的是 receiver,设置为谁,调用方法时候第一响应者就是谁;
  3. 通过这个探索过程,我们得出结论 方法调用的本质是消息发送

扩展

objc_msgSend汇编分析

  1. cmp p0, #0 : p0为此次的消息接受者,拿来和0比较,判断消息接受者是否为0,如果没有消息接受者,则此次 objc_msgSend 没有意义。

  2. #if SUPPORT_TAGGED_POINTERS 判断是否为 SUPPORT_TAGGED_POINTERS 类型,如果是,则执行 b.le LNilOrTagged, 否则, 执行 b.eq LReturnZero,即返回此次消息为空。

  3. ldr p13, [x0]x0存入到p13,x0receiver,即类,即类的首地址,即isa,也就是说p13=isa

  4. 进入 GetClassFromIsa_p16 带入参数 src=p13, needs_auth=1, auth_address=x0. 判断是不是 SUPPORT_INDEXED_ISA (32位isa),不满足此条件,接下来会进入 __LP64__ (这份源码里指的是Mac OS X)分支。

  5. 由于_need_auth=1,进入分支 ExtractISA p16, \src, \auth_address ,此ExtractISA 为宏,操作是将\src(isa)、#ISA_MASK做与操作,得到了Class,结果存入到p16中。

  6. LGetIsaDone:获取isa完成。接下来执行CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

下一篇,我们接着 第6步的内容 继续 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, 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

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

GetClassFromIsa_p16

.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */

#if SUPPORT_INDEXED_ISA
	// Indexed isa
	mov	p16, \src			// optimistically set dst = src
	tbz	p16, #ISA_INDEX_IS_NPI_BIT, 1f	// done if not non-pointer isa
	// isa in p16 is indexed
	adrp	x10, _objc_indexed_classes@PAGE
	add	x10, x10, _objc_indexed_classes@PAGEOFF
	ubfx	p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
	ldr	p16, [x10, p16, UXTP #PTRSHIFT]	// load class from array
1:

#elif __LP64__
.if \needs_auth == 0 // _cache_getImp takes an authed class already
	mov	p16, \src
.else
	// 64-bit packed isa
	ExtractISA p16, \src, \auth_address
.endif
#else
	// 32-bit raw isa
	mov	p16, \src

#endif

.endmacro

ExtractISA

.macro ExtractISA
	and	$0, $1, #ISA_MASK
#if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_STRIP
	xpacd	$0
#elif ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
	mov	x10, $2
	movk	x10, #ISA_SIGNING_DISCRIMINATOR, LSL #48
	autda	$0, x10
#endif
.endmacro

CacheLookup

.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
	//
	// Restart protocol:
	//
	//   As soon as we're past the LLookupStart\Function 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\Function,
	//   then our PC will be reset to LLookupRecover\Function 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
	//

	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
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
	tbnz	p11, #0, LLookupPreopt\Function
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
	tbnz	p11, #0, LLookupPreopt\Function
#endif
	eor	p12, p1, p1, LSR #7
	and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
	and	p12, p1, p11, LSR #48		// x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	ldr	p11, [x16, #CACHE]				// p11 = mask|buckets
	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	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)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
						// p13 = buckets + (mask << 1+PTRSHIFT)
						// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	add	p13, p10, p11, LSL #(1+PTRSHIFT)
						// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#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

#if CONFIG_USE_PREOPT_CACHES
#if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16
#error config unsupported
#endif
LLookupPreopt\Function:
#if __has_feature(ptrauth_calls)
	and	p10, p11, #0x007ffffffffffffe	// p10 = buckets
	autdb	x10, x16			// auth as early as possible
#endif

	// x12 = (_cmd - first_shared_cache_sel)
	adrp	x9, _MagicSelRef@PAGE
	ldr	p9, [x9, _MagicSelRef@PAGEOFF]
	sub	p12, p1, p9

	// w9  = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
#if __has_feature(ptrauth_calls)
	// bits 63..60 of x11 are the number of bits in hash_mask
	// bits 59..55 of x11 is hash_shift

	lsr	x17, x11, #55			// w17 = (hash_shift, ...)
	lsr	w9, w12, w17			// >>= shift

	lsr	x17, x11, #60			// w17 = mask_bits
	mov	x11, #0x7fff
	lsr	x11, x11, x17			// p11 = mask (0x7fff >> mask_bits)
	and	x9, x9, x11			// &= mask
#else
	// bits 63..53 of x11 is hash_mask
	// bits 52..48 of x11 is hash_shift
	lsr	x17, x11, #48			// w17 = (hash_shift, hash_mask)
	lsr	w9, w12, w17			// >>= shift
	and	x9, x9, x11, LSR #53		// &=  mask
#endif

	ldr	x17, [x10, x9, LSL #3]		// x17 == sel_offs | (imp_offs << 32)
	cmp	x12, w17, uxtw

.if \Mode == GETIMP
	b.ne	\MissLabelConstant		// cache miss
	sub	x0, x16, x17, LSR #32		// imp = isa - imp_offs
	SignAsImp x0
	ret
.else
	b.ne	5f				// cache miss
	sub	x17, x16, x17, LSR #32		// imp = isa - imp_offs
.if \Mode == NORMAL
	br	x17
.elseif \Mode == LOOKUP
	orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
	SignAsImp x17
	ret
.else
.abort  unhandled mode \Mode
.endif

5:	ldursw	x9, [x10, #-8]			// offset -8 is the fallback offset
	add	x16, x16, x9			// compute the fallback isa
	b	LLookupStart\Function		// lookup again with a new isa
.endif
#endif // CONFIG_USE_PREOPT_CACHES

.endmacro

CacheHit

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

// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
	TailCallCachedImp x17, x10, x1, x16	// authenticate and call imp
.elseif $0 == GETIMP
	mov	p0, p17
	cbz	p0, 9f			// don't ptrauth a nil imp
	AuthAndResignAsIMP x0, x10, 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, x10, x1, x16	// authenticate imp and re-sign as IMP
	cmp	x16, x15
	cinc	x16, x16, ne			// x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
	ret				// return imp via x17
.else
.abort oops
.endif
.endmacro