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
NSObjectclass of the Foundation framework; and through direct calls to runtime functions.
三种和Runtime的交互办法:
- 自界说办法调用:
[person sayHello]- 体系动态库api:
isKindOfClassRuntime的api:class_getInstanceSize
咱们探求Runtime就从最了解的自界说办法调用开端入手。
cpp办法检查
自界说类LGPerson,LGPerson中自界说实例办法sayHello,然后在main函数中调用,并生成cpp文件检查。
-
LGPerson的alloc类办法; - 实例办法
sayPerson; -
NSObject的办法isKindOfClass:; -
NSObject的class类办法;
咱们来看cpp中的代码完成

objc_msgSend,咱们对objc_msgSend进行整理发现它的结构是objc_msgSend(id receiver, sel),那咱们是不是也可以直接调用objc_msgSend呢?
objc_msgSend调用完成

办法的调用其实便是音讯发送。在检查objc_msgSend时我还发现了一个办法objc_msgSendSuper
objc_msgSendSuper调用完成

objc_msgSendSuper界说

objc_super类型的指针,一个SEL,看一下objc_super
咱们自界说LGTeacher类承继自LGPerson,调用父类的办法sayHello,objc_msgSend,objc_msgSendSuper

objc_msgSend是怎么完成音讯发送的呢?
objc_msgSend探求
经过汇编调试办法,发现objc_msgSend的界说是在libobjc库中

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
伪代码复现一下代码逻辑
- 判别参数
receider也便是isa是否为nil; - 是nil再判别是否支撑
Taggedpointer类型,假如支撑则走LNilOrTagged流程,不然就走LReturnZero流程; -
receider不为nil,取出isa赋值给p13; - 调用
GetClassFromIsa_p16,并传参数p13, 1, x0也便是isa,1,x0,回去class地址赋值给p16; - 调用办法
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_ISA为armv7k或arm64切非LP64; -
needs_auth参数为1;
依据上面两个条件GetClassFromIsa_p16的核心代码便是ExtractISA p16, \src, \auth_address,ExtractISA也是宏界说源码为
.macro ExtractISA
and $0, $1, #ISA_MASK
.endmacro
结合GetClassFromIsa_p16和ExtractISA解析
p16为ExtractISA里面的$0;src为p13也便是isa为ExtractISA里面的$1;and$0, $1, #ISA_MASK:isa & ISA_MASK=cls类的地址,即为从对象的isa获取class的进程。
这儿得到$0也便是p16为cls,持续走流程看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_STORAGE为CACHE_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
按照上面界说复现一下代码逻辑
-
mov x15, x16: 取x16也是p16(cls)给x15; -
ldr p11, [x16, #CACHE]:p16(cls)平移16字节得到cache,存在p11便是cache的地址; -
and p10, p11, #0x0000fffffffffffe:0x0000fffffffffffe为bucketsMask掩码值,所以这儿用cache与bucketMask掩码值取得buckets地址存在p10; -
eor p12, p1, p1, LSR #7:p1为SEL,这儿对应源码cache_hash办法中的sel ^= sel >> 7,p1右移7位得到的值再异或p1,存到p12; -
and p12, p12, p11, LSR #48:p11右移48位得到mask值,再和p12相与,即sel & mask得到sel的哈希下标值存在p12; -
add p13, p10, p12, LSL #(1+PTRSHIFT):bucket_t的成员是sel和imp,内存大小为16字节,p12, LSL #(1+PTRSHIFT)相当于哈希下标值index左移4位,得到index对应与buckets首地址的偏移量, 经过p10也便是buckets首地址向下移动p12, LSL #(1+PTRSHIFT),取到bucket_t地址存在p13;
-
1:中的ldp p17, p9, [x13], #-BUCKET_SIZE相当于取出p13 bucket_t中的sel给p9,取imp给p17,#-BUCKET_SIZE为*buckets--先取值后--; -
cmp p9, p1:比较缓存里的sel和p1是否共同,假如共同则走2:中的CacheHit缓存射中,\Mode为第一个参数值NORMAL,不然或许为哈希冲突也或许没有缓存该sel,进入3:句子; -
3:中先判别p9是否有值,没有则阐明没有缓存sel走MissLabelDynamic,也便是传入的第三个参数__objc_msgSend_uncached, - 假如p9有值,则比较
p10和p13是否同一个地址,不是则持续1:流程循环,假如是同一个地址,由于1:中是*buckets--遍历查找,也就意味着找到了buckets的首地址方位,那就跳转到buckets的最终方位持续循环。
-
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT)): 取buckets中的最终一个bucket_t地址存在p13; -
add p12, p10, p12, LSL #(1+PTRSHIFT):用p12记载第一次查找的方位; -
4:中的逻辑是遍历最终一个方位到第一次查找的方位中的所有bucket_t,找到了就CacheHit,不然就MissLabelDynamic
在CacheLookup中假如可以找到缓存办法,则走CacheHit中的NORMAL逻辑,找不到就走__objc_msgSend_uncached。
CacheHit源码解析

CacheHit中$0为传进来的NORMAL,所以这儿的代码逻辑便是用TailCallCachedImp对缓存里找到的imp先解码再调用。
缓存查找流程图
总结
- 汇编源码真恶心,慢慢跟流程还算能啃下来。
- 经过上面的流程剖析到
objc_msgSend的调用,其实便是经过SEL查找IMP的进程,这个进程越快越好;汇编是比较接近机器码的,所以OC的规划是用汇编完成办法的缓存查找会进步办法调用的功率;objc_msgSend流程便是先去类的缓存中找有没有对应的sel,找到了则直接调用缓存中的imp;- 找不到
imp便是下一个流程了,objc_msgSend的慢速查找流程。
以上是对Runtime的一些剖析,以及办法调用进程中objc_msgSend的缓存查找完成流程剖析,如有疑问或过错之处,请谈论区留言或私信我。






