一、运行时runtime
1.什么是runtime
runtime是一套由c、c++、汇编混合写成的,为oc提供运行时功能的api。
那为什么不直接用oc来实现运行时呢?oc是一门高级语言,我们知道越是高级的原因,被机器识别的能力就越弱,汇编是最接近底层的机器语言的。所以就需要借助c、c++和汇编来保证系统稳定性和提高执行效率。
2.runtime版本
Objective-C运行时系统有两个版本:早期版本(legacy 1.0)和现行版本(modern 2.0)。现行版本主要是Objective-C 2.0 及与其相关的新特性。在看底层源码的时候,有时可以看到一些宏定义,用来判断不同版。
3.运行平台
iPhone 程序和 Mac OS X v10.5 及以后的系统中的 64 位程序使用的都是 Objective-C 运行时系统的现行版 本。
其它情况(Mac OS X 系统中的 32 位程序)使用的是早期版本。
4.交互方式
Objective-C 程序有三种途径和运行时系统交互:
通过 Objective-C代码,比如@selector()
通过 Foundation框架中NSObject的方法,比如NSSelectorFromString()
通过直接调用runtime函数, sel_registerName()
二、方法的本质探索
将一段简单的方法调用代码,放在main方法中,然后使用clang命令编译。
ZPerson *person = [ZPerson alloc];
[person sayHello];
使用终端进入当前main.m文件所在领,然后使用clang -rewrite-objc main.m
命令编译。得到如下编译后的源码:
ZPerson *person = ((ZPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("ZPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
去掉强制类型转换后:
objc_msgSend(objc_getClass("ZPerson"), sel_registerName("alloc"));
objc_msgSend(person, sel_registerName("sayHello"));
通过上面这段代码,我们发现不管是alloc
方法调用还是sayHello
方法的调用,都是通过objc_msgSend
函数来实现的。因此我们可以说OC方法的本质就是通过objc_msgSend
来发送消息。objc_msgSend
包含有方法的调用的两个隐藏参数:self(消息接受者)和sel(方法编号)。sel_registerName
等同于oc中的@selector()
,可以根据传进的方法名得到一个sel
。
三、objc_msgSend分析
1.源码查找
在objc_msgSend
上按住command+点击,我们只能进到类似的方法定义的声明,而不能再向下进到方法的实现中去。
OBJC_EXPORT void
objc_msgSend(void /* id self, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
这是因为objc_msgSend
是由汇编的代码实现的。首先从性能方面考虑,方法调用需要被快速的处理和响应,而汇编更容易被机器识别。而是由于未知参数的原因(个数未知、类型未知,比如NSLog()
),c和c++作为静态语言,并不能满足这一特性。
所以只能通过全局查找objc_msgSend
,来寻找它的实现过程。
可以看到,根据不同的平台,obcj提供了不同版本的方法实现,我们选择arm64版本进行分析。
ENTRY
为方法的入口标志,我们将从开始入手分析。
2.流程分析
a.找到_objc_msgSend
ENTRY _objc_msgSend
...
END_ENTRY _objc_msgSend
b.对消息接收者的判断处理
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
cmp
是一个比较指令,p0
为方法的第一个参数,也就是方法的接收者。所以这段代码就是判断方法接收者是否为空,然后做相应的处理。
c.获取isa
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
ldr是数据读取指令,将x0中的数据读取到p13中。这里p13就是isa,根据isa拿到类。注意:我们知道对象方法存在类里,类方法存在元类里。那么我们这时候拿到的就是类或者是元类。那么是怎么拿到的呢?
.macro GetClassFromIsa_p16 /* src */
#if SUPPORT_INDEXED_ISA
// Indexed isa
mov p16, $0 // 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__
// 64-bit packed isa
and p16, $0, #ISA_MASK
#else
// 32-bit raw isa
mov p16, $0
#endif
.endmacro
macro
是宏定义。这里的if
判断我们进入的是#elif __LP64__
,将$0
和ISA_MASK
进行与运算,然后复制给p16
。还记得我们之前博客里如何从isa里取值吗,ISA_MASK
是不是似相识?将isa
的bits
和ISA_MASK
进行与运算,得到class
。(Class)(isa.bits & ISA_MASK)
。
d.CacheLookup查找缓存
再回到objc_msgSend
的流程分析中来。
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
取到isa
之后,进入到CacheLookup
中来。也就是从缓存中查找。
/********************************************************************
*
* CacheLookup NORMAL|GETIMP|LOOKUP
*
* Locate the implementation for a selector in a class method cache.
*
* Takes:
* x1 = selector
* x16 = class to be searched
*
* Kills:
* x9,x10,x11,x12, x17
*
* On exit: (found) calls or returns IMP
* with x16 = class, x17 = IMP
* (not found) jumps to LCacheMiss
*
********************************************************************/
.macro CacheLookup
// p1 = SEL, p16 = isa
//x16 平移16位找到cache
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
//w 低位
and w11, w11, 0xffff // p11 = mask
#endif
and w12, w1, w11 // x12 = _cmd & mask
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
//ne noequal
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
//eq equal
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
add p12, p12, w11, UXTW #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // double wrap
JumpMiss $0
.endmacro
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
x16为cacht_t的地址,#CACHE为两个指针的大小(16字节),从 x16 + 0x16 指向的地址里面取出 2个 8位的数,分别存入p10, p11。根据上篇的cache_t分析可知,取出的p10是buckets,p11存的是occupied和mask。
and w12, w1, w11 // x12 = _cmd & mask
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
w是低位的意思。w1也就是p1,也就是第二个参数sel。w11也就是刚取出的p11的低位,也就是mask。w1与上w11也就等同于上篇中根据key和mask取下标cache_hash()。w12为取出的下标。
e.从cache_t中取出buckects递归遍历
ldp p17, p9, [x12] // {imp, sel} = *bucket
//1和2形成一个递归
//比较p9和p1
1: cmp p9, p1 // if (bucket->sel != _cmd)
//如果不相等 就去执行2方法 不再向下执行
b.ne 2f // scan more
//既然执行CacheHit 就说明p1和p9相等, 也就是从缓存里找到我们要找到方法 返回imp 不再向下执行 递归调用结束
CacheHit $0 // call or return imp
//递归的流程之一
2: // not hit: p12 = not-hit bucket
//检查p9是否为空, 如果p9为空,说明缓存数组为空 如果为空就去跳转执行 __objc_msgSend_uncached 递归也结束了
CheckMiss $0 // miss if bucket->sel == 0
//如果p9不为空, 就比较p12和p10 也就是比较取出的bucket和buckets首个元素
cmp p12, p10 // wrap if bucket == buckets
//如果相等 说明我们已经遍历完了buckets 去跳转执行3方法 递归也结束了
b.eq 3f
//如果还没遍历到第一个元素, 就继续位移 取出新的imp和sel
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
//b为直接跳转, 直接跳转1方法 用新取出的bucket去比较
//执行到这里可以看出这是一个递归的流程
b 1b // loop
结合上面的代码注释,我们可以总结:CacheLookup
的查找过程,就是一个遍历已有缓存的过程,如果遍历过程中缓存命中,就去执行CacheHit
。如果没有命中,就去执行CheckMiss
。
f.缓存命中
.macro CacheHit
.if $0 == NORMAL
TailCallCachedImp x17, x12 // authenticate and call imp
.elseif $0 == GETIMP
mov p0, p17
AuthAndResignAsIMP x0, x12 // authenticate imp and re-sign as IMP
ret // return IMP
.elseif $0 == LOOKUP
AuthAndResignAsIMP x17, x12 // authenticate imp and re-sign as IMP
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
我们传递的参数$0
的值是NORMAL
,所以这是开始验证并调用方法。
g.CheckMiss检查
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
我们传递的参数$0
的值是NORMAL
。cbz一个判断指令,如果p9
为0
,也就是缓存数组为空,可肯定是找不到缓存了,就去执行__objc_msgSend_uncached
。
h.方法缓存未找到
看一下__objc_msgSend_uncached
:
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
这里会执行一个MethodTableLookup
方法继续查看MethodTableLookup
:
.macro MethodTableLookup
// push frame
SignLR
stp fp, lr, [sp, #-16]!
mov fp, sp
//保存参数寄存器,为调用c函数做准备
// save parameter registers: x0..x8, q0..q7
sub sp, sp, #(10*8 + 8*16)
stp q0, q1, [sp, #(0*16)]
stp q2, q3, [sp, #(2*16)]
stp q4, q5, [sp, #(4*16)]
stp q6, q7, [sp, #(6*16)]
stp x0, x1, [sp, #(8*16+0*8)]
stp x2, x3, [sp, #(8*16+2*8)]
stp x4, x5, [sp, #(8*16+4*8)]
stp x6, x7, [sp, #(8*16+6*8)]
str x8, [sp, #(8*16+8*8)]
// receiver and selector already in x0 and x1
mov x2, x16
bl __class_lookupMethodAndLoadCache3
// IMP in x0
mov x17, x0
// restore registers and return
ldp q0, q1, [sp, #(0*16)]
ldp q2, q3, [sp, #(2*16)]
ldp q4, q5, [sp, #(4*16)]
ldp q6, q7, [sp, #(6*16)]
ldp x0, x1, [sp, #(8*16+0*8)]
ldp x2, x3, [sp, #(8*16+2*8)]
ldp x4, x5, [sp, #(8*16+4*8)]
ldp x6, x7, [sp, #(8*16+6*8)]
ldr x8, [sp, #(8*16+8*8)]
mov sp, fp
ldp fp, lr, [sp], #16
AuthenticateLR
.endmacro
MethodTableLookup
中将寄存器中数据都做了保存,并调用了一个__class_lookupMethodAndLoadCache3
方法。我们查找__class_lookupMethodAndLoadCache3
,发现并不能找到。我们知道汇编调用c方法的时候会在前面加一个下划线,而此时恰好有两条下划线,我们试试去掉一个在去查找呢?果然我们找了_class_lookupMethodAndLoadCache3
方法。
_class_lookupMethodAndLoadCache3
,又调用了了lookUpImpOrForward
去查找,我们将在后面分析。
i.再查找一遍缓存
我们回到CacheLookup
继续向下分析代码。
3: // wrap: p12 = first bucket, w11 = mask
add p12, p12, w11, UXTW #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // double wrap
JumpMiss $0
上面的3``````2``````1
步骤是重新的又遍历了一遍缓存。由于多线程的原因,可能当前调用的方法这时可能已经在别的线程调用结束了,也就是说现在可能缓存中已经有了方法的缓存了,这时我们再遍历一遍,也算是再给缓存查找一次机会吧。如果执行到JumpMiss
,说明是怎的的在缓存里找不到调用方法了,这将直接调用__objc_msgSend_uncached
。
.macro JumpMiss
.if $0 == GETIMP
b LGetImpMiss
.elseif $0 == NORMAL
b __objc_msgSend_uncached
.elseif $0 == LOOKUP
b __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
方法调用时查找缓存的流程就到这了,结合前篇的cache_t分析效果更好哟!如果有不对的地方,欢迎评论区批评指正。