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 theNSObjectclass 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文件检查。

iOS底层之Runtime探索(一)
共调用了4个办法

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

咱们来看cpp中的代码完成

iOS底层之Runtime探索(一)
可以看到,不管实例办法还是类办法都是调用的函数objc_msgSend,咱们对objc_msgSend进行整理发现它的结构是objc_msgSend(id receiver, sel),那咱们是不是也可以直接调用objc_msgSend呢?

objc_msgSend调用完成

iOS底层之Runtime探索(一)
调用成功,这儿也就验证了办法的调用其实便是音讯发送。在检查objc_msgSend时我还发现了一个办法objc_msgSendSuper

objc_msgSendSuper调用完成

iOS底层之Runtime探索(一)
检查objc_msgSendSuper界说
iOS底层之Runtime探索(一)
有2个参数,一个objc_super类型的指针,一个SEL,看一下objc_super

iOS底层之Runtime探索(一)
这儿的成员super_class是第一要查找的类。

咱们自界说LGTeacher类承继自LGPerson,调用父类的办法sayHelloobjc_msgSendobjc_msgSendSuper

iOS底层之Runtime探索(一)
可以看到,三种办法都能完成,那么objc_msgSend是怎么完成音讯发送的呢?

objc_msgSend探求

经过汇编调试办法,发现objc_msgSend的界说是在libobjc库中

iOS底层之Runtime探索(一)
那咱们就去源码找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;

iOS底层之Runtime探索(一)

  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的最终方位持续循环。

iOS底层之Runtime探索(一)

  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源码解析

iOS底层之Runtime探索(一)
CacheHit$0为传进来的NORMAL,所以这儿的代码逻辑便是用TailCallCachedImp对缓存里找到的imp先解码再调用。

iOS底层之Runtime探索(一)

缓存查找流程图

iOS底层之Runtime探索(一)

总结

  • 汇编源码真恶心,慢慢跟流程还算能啃下来。
  • 经过上面的流程剖析到objc_msgSend的调用,其实便是经过SEL查找IMP的进程,这个进程越快越好;
  • 汇编是比较接近机器码的,所以OC的规划是用汇编完成办法的缓存查找会进步办法调用的功率;
  • objc_msgSend流程便是先去类的缓存中找有没有对应的sel,找到了则直接调用缓存中的imp
  • 找不到imp便是下一个流程了,objc_msgSend的慢速查找流程。

以上是对Runtime的一些剖析,以及办法调用进程中objc_msgSend的缓存查找完成流程剖析,如有疑问或过错之处,请谈论区留言或私信我。