iOS底层原理-Runtime运行时&方法的本质

683 阅读6分钟

前言

在上一篇iOS底层原理-cache_t分析cache的结构和方法的存储做了分析,通过LLDB示例调试进行了验证,这些是通过查看源码找到相应的insert()方法,但这样就有个疑问,cache是什么时候插入的?

cache是什么时候写入的?

LLDB调试

我们在对象调起方法时打个断点,当断住时在cache_t::insert方法再打个断点(确定为当前对象调用方法走的insert),进到insert方法里,输入bt打印当前堆栈信息。

001.png

可以看到在调起insert之前调用了log_and_fill_cache方法,这时我们再搜索一下,可以找到在lookUpImpOrForward方法里调用了,跟上面截图的调用结果一致。

Cache的定义

打开cache的源码可以看到下面的一段注释

/*
 * Cache readers (PC-checked by collecting_in_critical())
 * objc_msgSend*
 * cache_getImp
 *
 * Cache readers/writers (hold cacheUpdateLock during access; not PC-checked)
 * cache_t::copyCacheNolock    (caller must hold the lock)
 * cache_t::eraseNolock        (caller must hold the lock)
 * cache_t::collectNolock      (caller must hold the lock)
 * cache_t::insert             (acquires lock)
 * cache_t::destroy            (acquires lock)
 */

从注释可以看到在cache_t::insert之前有cache_getImpobjc_msgSend*,下面着重了解一下objc_msgSend

Runtime

在分析objc_msgSend之前,先来了解一下Runtime的概念

The Objective-C runtime is a runtime library that provides support for the dynamic properties of the Objective-C language, and as such is linked to by all Objective-C apps. Objective-C runtime library support functions are implemented in the shared library found at /usr/lib/libobjc.A.dylib.

Runtime的发起方式

  • ObjectIve-C层面调起相关方法
  • NSObject接口
  • objc底层API 002.png

Clang编译

我们通过Clang对main.m进行编译,看编译后的Objective-C是如何调用方法的?

main.m

LGPerson *p  = [LGPerson alloc];
[p saySomething];
// clang对main.m进行编译
clang -rewrite-objc main.m -o main.cpp

编译后得到下面的调用函数

LGPerson *p = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("saySomething"));

根据 clang 编译结果我们得知方法的本质是发送消息,底层实现是 objc_msgSend(消息接收者, 消息主体[sel+参数])

汇编调试

在[p saySomething]的调用方法上打个断点,然后以汇编的模式进行查看(Debug ~> Debug Workflow ~> Always show Disassembly),然后按住control键,点击step into,进入objce_msgSend

003.png

在对象调用方法时通过汇编断点来看一下发送消息的流程,可以找到 objc_msgSend 定位到 libobjc.dylib 文件,因此我们在源码全局搜索 objc_msgSend,汇编是以.s文件结尾,我们找到 objc-msg-arm64.s(以arm64架构),这就是Object-C中著名的objc_msgSend流程,下面我们通过汇编的方式了解一下消息是怎么发送的? 004.png

objc_msgSend分析

通过搜索我们可以找到objc_msgSend标识为ENTRY入口的地方继续分析。

常用汇编指令

  • cmp: 比较指令
  • ldr: 把后面的值存入前面的地址中
  • mov: 寄存器加载数据,既能用于寄存器间的传输,也能用于加载立即数
  • eor: 异或操作
  • lsr: 逻辑右移
  • lsl: 逻辑左移
  • adrp: 通过基地址 + 偏移 获得一个字符串(全局变量)
  • ldp x0,x1,[sp] x0、x1 = sp栈内存中的值

以上是本篇用到的基本汇编指令,详情请参考Cooci的这篇文章。

汇编源码

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    cmp	p0, #0 // p0 消息接收者 判断receiver是否为空
#if SUPPORT_TAGGED_POINTERS // 判断是否为tagged pointer类型
    b.le	LNilOrTagged	// tagged pointer类型
#else
    b.eq	LReturnZero     // 非tagged pointer类型 LReturnZero返回nil
#endif
    ldr	p13, [x0] // 把x0的内存地址存入p13, x0:消息接收者,地址就是它的首地址isa, 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

上面汇编代码解析:

  • 判断对象是否为空
  • 判断是否为tagged pointer类型,是走LNilOrTagged,不是走LReturnZero(返回nil)
  • x0的首地址存入p13p13 = isa 对于GetClassFromIsa_p16是什么,全局搜索找一下它的定义,如下:
// GetClassFromIsa_p16 p13, 1, x0
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */

#if SUPPORT_INDEXED_ISA // 判断是否支持indexed isa,根据真机运行这里为0
    // 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 // 根据上面的传值needs_auth = 1
    mov	p16, \src
.else
    // 64-bit packed isa
    // 
    ExtractISA p16, \src, \auth_address
.endif
#else
    // 32-bit raw isa
    mov	p16, \src

#endif

.endmacro

根据SUPPORT_INDEXED_ISA = 0, needs_auth = 1,所以定位到ExtractISA p16, \src, \auth_address这行代码,再查一下ExtractISA的定义

#if __has_feature(ptrauth_calls)
.macro ExtractISA
    and	$0, $1, #ISA_MASK
.endmacro
#else
// ExtractISA p16, \src = p13(isa), \auth_address(isa)
.macro ExtractISA
    and $0, $1, #ISA_MASK  // $0 = $1 & ISA_MASK
.endmacro

根据上面的定义,我们得出ExtractISA的操作是$0 = $1 & ISA_MASK,也就是p16 = isa & ISA_MASK,所以p16 = class。接下来就是LGetIsaDone,然后走下面的代码

LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

再搜索一下CacheLookup的定义

.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant

    mov	x15, x16			// 把p16的地址移动到x15
LLookupStart\Function:
    // 条件判断,真机是CACHE_MASK_STORAGE_HIGH_16,p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    ...
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    ldr	p11, [x16, #CACHE]  // x16平移CACHE个位置,也就是class平移16字节, p11 = cache_t
    #if CONFIG_USE_PREOPT_CACHES  // 在arm64架构CONFIG_USE_PREOPT_CACHES = 1
        #if __has_feature(ptrauth_calls) // 针对arm64e架构(A12芯片及以上)
    tbnz p11, #0, LLookupPreopt\Function
    and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
        #else
    and	p10, p11, #0x0000fffffffffffe	// p10 = p11 & 0x0000fffffffffffe 也就是p10 = buckets
    tbnz p11, #0, LLookupPreopt\Function // 判断p11是否为空,不为空走LLookupPreopt
        #endif
    eor	p12, p1, p1, LSR #7
    and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
    #else
    and	p10, p11, #0x0000ffffffffffff	// p10 = p11 & 0x0000ffffffffffff, 也就是 p10 = buckets
    and	p12, p1, p11, LSR #48		// p11右移48位得到mask,所以 x12 = _cmd & mask
    #endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    ...
#else
#error Unsupported cache mask storage for ARM64.
#endif

#CACHE的定义

#define CACHE            (2 * __SIZEOF_POINTER__) // 2倍的指针大小,也就是 2 * 8 = 16

上面的流程解析

  • p16 = class
  • p16平移16字节,p11 = cache_t
  • p11 & 0x0000fffffffffffe得到buckets,也就是p10 = buckets
  • x12 = _cmd & mask,经过hash算法得到bucketsindexx12 = index 继续往下看
    // #define PTRSHIFT 3 
    add	p13, p10, p12, LSL #(1+PTRSHIFT) // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT)),
    (_cmd & mask) << 4就是把地址转换成int,buckets+就是做平移,所以p13就是找到对应的bucket
						// 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

#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
  • 通过buckets平移找到对应下标的bucket
  • {imp, sel} = *bucket--p17 = impp9 = selbucket继续向前平移
  • 判断当前的sel和要查找的sel是否相等,如果相等,跳到2CacheHit,找到了,如果不相等,跳到3
  • 判断sel是否为空,如果为空,跳转MissLabelDynamic,不为空判断bucket >= buckets,如果满足跳转到1,继续循环查找 根据第2步缓存查找到后,CacheHit做了什么?再搜索一下CacheHit的定义:
.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
    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

根据之前的参数,当前的$0NORMAL,再看一下TailCallCachedImp的定义:

.macro TailCallCachedImp
    // $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
    eor	$0, $0, $3 // $0就是X17,也就是imp; $3就是x16,也就是class
    br	$0 // 返回编码后的imp
.endmacro
  • $0 = imp ^ clsimpcls经过异或得到的编码后的imp
  • 返回编码后的imp

总结

  • 通过LLDB以汇编的方式调试,得到cache是什么时候插入的
  • 通过Clang编译成底层源码分析,了解了方法的调用底层是发送消息,通过objc_msgSend函数发送
  • objc_msgSend找到了消息发送是以汇编语言编写的
  • 根据汇编的流程一步步解析cache的查找方式