iOS底层之Runtime探索(一)

8,215 阅读5分钟

iOS 全网最新objc4 可调式/编译源码
编译好的源码的下载地址

Runtime简介

Runtime 简称运行时Objective-C语言将尽可能多的决策从编译时链接时推迟到运行时。只要可能,它都会动态地进行操作。这意味着该语言不仅需要编译器,还需要运行时系统来执行编译的代码。运行时系统充当Objective-C语言的一种操作系统(官方翻译)。
了解Objective-C运行时系统的工作原理以及如何利用它。但是,通常情况下,编写Cocoa应用程序时,您不需要了解和理解这些材料(官方翻译🐶)。

  • 编译时:顾名思义就是正在编译的时候。就是编译器帮你把源代码翻译成机器能识别的代码。编译器进行代码的语法分析,发现其中的编译错误和警告等,叫做静态类型检查
  • 运行时:代码跑起来被装载到内存中,运行时类型检查和编译时类型检查不一样,不是简单的代码扫描分析,而是在内存中做些操作。

Runtime官方介绍:Objective-C Runtime Programming Guide

Runtim探究

按照官方文档:

Objective-C programs interact with the runtime system at three distinct levels: through Objective-C source code; through methods defined in the NSObject class of the Foundation framework; and through direct calls to runtime functions.

三种和Runtime的交互方式:

  • 自定义方法调用: [person sayHello]
  • 系统动态库api:isKindOfClass
  • Runtime的api:class_getInstanceSize

我们探究Runtime就从最熟悉的自定义方法调用开始入手。

cpp方式查看

自定义类LGPersonLGPerson中自定义实例方法sayHello,然后在main函数中调用,并生成cpp文件查看。

image.png 共调用了4个方法

  • LGPersonalloc类方法;
  • 实例方法sayPerson
  • NSObject的方法isKindOfClass:
  • NSObjectclass类方法;

我们来看cpp中的代码实现

image.png 可以看到,不论实例方法还是类方法都是调用的函数objc_msgSend,我们对objc_msgSend进行梳理发现它的结构是objc_msgSend(id receiver, sel),那我们是不是也可以直接调用objc_msgSend呢?

objc_msgSend调用实现

image.png 调用成功,这里也就验证了方法的调用其实就是消息发送。在查看objc_msgSend时我还发现了一个方法objc_msgSendSuper

objc_msgSendSuper调用实现

image.png 查看objc_msgSendSuper定义 image.png 有2个参数,一个objc_super类型的指针,一个SEL,看一下objc_super

image.png 这里的成员super_class是第一要查找的类。

我们自定义LGTeacher类继承自LGPerson,调用父类的方法sayHelloobjc_msgSendobjc_msgSendSuper

image.png 可以看到,三种方式都能实现,那么objc_msgSend是怎么实现消息发送的呢?

objc_msgSend探究

通过汇编调试方法,发现objc_msgSend的定义是在libobjc库中 image.png 那我们就去源码找objc_msgSend,通过全局搜索锁定汇编文件objc-msg-arm64,接下来我们就来看objc_msgSend的汇编流程,加了一些注释

objc_msgSend汇编源码

// _objc_msgSend调用时有两个参数, id receiver(isa), SEL
ENTRY _objc_msgSend         //  _objc_msgSend 入口
UNWIND _objc_msgSend, NoFrame

cmp p0, #0       // 第一个参数receiver和0比较
#if SUPPORT_TAGGED_POINTERS  // 是否支持Taggedpointer类型
b.le LNilOrTagged //  (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif                      //cmp比较 receiver有值就走 endif
ldr p13, [x0] // p13 = isa (取出x0=isa赋值给p13)
GetClassFromIsa_p16 p13, 1, x0 // p16 = class (调用GetClassFromIsa_p16方法,p13, 1, x0作为参数传入)
LGetIsaDone: // 一个标记符号,拿到isa后操作完后,继续后面流程
// calls imp or objc_msgSend_uncached(调用CacheLookup,NORMAL, _objc_msgSend, __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

END_ENTRY _objc_msgSend

ENTRY _objc_msgLookup
UNWIND _objc_msgLookup, NoFrame
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LLookup_NilOrTagged //  (MSB tagged pointer looks negative)
#else
b.eq LLookup_Nil
#endif
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
LLookup_GetIsaDone:
// returns imp
CacheLookup LOOKUP, _objc_msgLookup, __objc_msgLookup_uncached

伪代码复现一下代码逻辑

  1. 判断参数receider也就是isa是否为nil
  2. 是nil再判断是否支持Taggedpointer类型,如果支持则走LNilOrTagged流程,否则就走LReturnZero流程;
  3. receider不为nil,取出isa赋值给p13
  4. 调用GetClassFromIsa_p16,并传参数p13, 1, x0也就是isa,1,x0,回去class地址赋值给p16;
  5. 调用方法CacheLookup,并传参数NORMAL, _objc_msgSend, __objc_msgSend_uncached

GetClassFromIsa_p16方法解析

同样看一下GetClassFromIsa_p16源码,其核心功能是获取isa指向的class地址,这里也加了注释

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

#if SUPPORT_INDEXED_ISA  // armv7k || (arm64 && !LP64)
// 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   // 这里穿的needs_auth = 1,所以走else流程
// 64-bit packed isa
/**
 解析:
 src = p13(isa), needs_auth = 1, auth_address = x0
 .macro ExtractISA and    $0, $1, #ISA_MASK
 等于:
 (isa & #ISA_MASK) 赋值给 p16 --> 这里就是去出isa指向的class地址
 */
ExtractISA p16, \src, \auth_address
.endif
#else
// 32-bit raw isa
mov p16, \src
#endif
.endmacro
  • SUPPORT_INDEXED_ISAarmv7karm64切非LP64;
  • needs_auth参数为1;

根据上面两个条件GetClassFromIsa_p16的核心代码就是ExtractISA p16, \src, \auth_addressExtractISA也是宏定义源码为

.macro ExtractISA
    and    $0, $1, #ISA_MASK
.endmacro

结合GetClassFromIsa_p16ExtractISA解析

  • p16ExtractISA里面的 $0
  • srcp13也就是isaExtractISA里面的 $1
  • and $0, $1, #ISA_MASKisa & ISA_MASK = cls类的地址,即为从对象的isa获取class的过程。

这里得到$0也就是p16cls,继续走流程看CacheLookup

缓存查找

CacheLookup汇编源码解析

根据CacheLookup名称,我们也能猜出大概即从缓存中查找,从前面《类的缓存cache_分析》我们知道方法调用后是缓存在cache_t关联的bucket_t中,前面得到了p16也就是class,下面就是找类的bucket_t

CacheLookup源码

// NORMAL, _objc_msgSend, __objc_msgSend_uncached
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant

// 
    mov x15, x16 //x16 (p16 = isa) 取值 --> x15 (stash the original isa)
LLookupStart\Function:
#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 = cache
#if CONFIG_USE_PREOPT_CACHES // arm64 下为1
#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

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

补充几个定义

  • 真机的CACHE_MASK_STORAGECACHE_MASK_STORAGE_HIGH_16,我们看真机环境;
  • #CACHE(2 * __SIZEOF_POINTER__)2倍指针大小2 * 8 = 16
  • arm64环境下CONFIG_USE_PREOPT_CACHES值为1;
  • __has_feature(ptrauth_calls): 是否为A12及更高处理器,我们看通用版本,默认这里为0;
  • PTRSHIFT值为3

按照上面定义复现一下代码逻辑

  1. mov x15, x16: 取x16也是p16(cls)x15
  2. ldr p11, [x16, #CACHE]: p16(cls)平移16字节得到cache,存在p11就是cache的地址;
  3. and p10, p11, #0x0000fffffffffffe: 0x0000fffffffffffebucketsMask掩码值,所以这里用cachebucketMask掩码值取得buckets地址存在p10
  4. eor p12, p1, p1, LSR #7p1SEL,这里对应源码cache_hash方法中的sel ^= sel >> 7p1右移7位得到的值再异或p1,存到p12
  5. and p12, p12, p11, LSR #48p11右移48位得到mask值,再和p12,即sel & mask得到sel的哈希下标值存在p12
  6. add p13, p10, p12, LSL #(1+PTRSHIFT)bucket_t的成员是selimp,内存大小为16字节,p12, LSL #(1+PTRSHIFT)相当于哈希下标值index左移4位,得到index对应与buckets首地址的偏移量, 通过p10也就是buckets首地址向下移动p12, LSL #(1+PTRSHIFT),取到bucket_t地址存在p13;

image.png

  1. 1:中的ldp p17, p9, [x13], #-BUCKET_SIZE相当于取出p13 bucket_t中的selp9,取impp17#-BUCKET_SIZE*buckets--先取值后--
  2. cmp p9, p1:比较缓存里的selp1是否一致,如果一致则走2:中的CacheHit缓存命中,\Mode为第一个参数值NORMAL,否则可能为哈希冲突也可能没有缓存该sel,进入3:语句;
  3. 3:中先判断p9是否有值,没有则说明没有缓存selMissLabelDynamic,也就是传入的第三个参数__objc_msgSend_uncached
  4. 如果p9有值,则比较p10p13是否同一个地址,不是则继续1:流程循环,如果是同一个地址,因为1:中是*buckets--遍历查找,也就意味着找到了buckets的首地址位置,那就跳转到buckets的最后位置继续循环。

image.png

  1. add p13, p10, p11, LSR #(48 - (1+PTRSHIFT)): 取buckets中的最后一个bucket_t地址存在p13
  2. add p12, p10, p12, LSL #(1+PTRSHIFT):用p12记录第一次查找的位置;
  3. 4:中的逻辑是遍历最后一个位置到第一次查找的位置中的所有bucket_t,找到了就CacheHit,否则就MissLabelDynamic

CacheLookup中如果能够找到缓存方法,则走CacheHit中的NORMAL逻辑,找不到就走__objc_msgSend_uncached

CacheHit源码解析

image.pngCacheHit$0为传进来的NORMAL,所以这里的代码逻辑就是用TailCallCachedImp对缓存里找到的imp先解码再调用。

image.png

缓存查找流程图

类结构之cache_t-导出.png

总结

  • 汇编源码真恶心,慢慢跟流程还算能啃下来。
  • 通过上面的流程分析到objc_msgSend的调用,其实就是通过SEL查找IMP的过程,这个过程越快越好;
  • 汇编是比较接近机器码的,所以OC的设计是用汇编实现方法的缓存查找会提高方法调用的效率;
  • objc_msgSend流程就是先去类的缓存中找有没有对应的sel,找到了则直接调用缓存中的imp
  • 找不到imp就是下一个流程了,objc_msgSend慢速查找流程

以上是对Runtime的一些分析,以及方法调用过程中objc_msgSend的缓存查找实现流程分析,如有疑问或错误之处,请评论区留言或私信我。