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.
- 自定义方法调用:
[person sayHello]
- 系统动态库api:
isKindOfClass
Runtime
的api:class_getInstanceSize
我们探究Runtime
就从最熟悉的自定义方法调用开始入手。
cpp方式查看
自定义类LGPerson
,LGPerson
中自定义实例方法sayHello
,然后在main
函数中调用,并生成cpp
文件查看。
共调用了4个方法
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
定义
有2个参数,一个objc_super
类型的指针,一个SEL
,看一下objc_super
这里的成员super_class
是第一要查找的类。
我们自定义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
的缓存查找实现流程分析,如有疑问或错误之处,请评论区留言或私信我。