iOS 底层探索篇 —— Runimte 运行时&方法的本质
何时进行insert
上文讲了insert这个方法,那么到底什么时候进行插入呢。我们在源码
中搜索->insert
,看看insert方法什么时候被调用。
发现都不是要找的方法。那么现在我们就需要去cache_t
的insert方法的实现里面打断点
,然后运行一下,就可以得到insert是被谁调用的了。
这里可以看到insert的上一步是log_and_fill_cache
,那么insert大概率就是在这个里面被调用的,我们点进去看一下。
果然发现了 cls->cache.insert(sel, imp, receiver).
这里也可以在lldb输入bt进行输出查找
,但是不推荐
,因为太丑了太难寻找了。
那么又是那里调用的log_and_fill_cache
这个方法的呢。偷懒点击一下log_and_fill_cache下面那一行,就可以跳到调用log_and_fill_cache的地方了,也就是lookUpImpOrForward
方法。
在写入流程之前,还有一个cache
读取流程,即objc_msgSend
和 cache_getImp
在分析之前,首先了解什么是Runtime
Runtime 介绍
Runtime也就是运行时
是一个库,这个库使我们可以在程序运行时创建对象
、检查对象
,修改类
和对象的方法
。Runtime有两个版本 一个Legacy版本
(早期版本) ,一个Modern版本
(现行版本),它区别于编译时
-
运行时
是代码跑起来,被装载到内存中的过程
,如果此时出错,则程序会崩溃,是一个动态阶段
. -
编译时
顾名思义就是正在编译
的时候 . 那什么叫编译呢?就是编译器帮你把
源代码翻译成机器能识别的代码
. 是源代码翻译成机器能识别的代码的过程,主要是对语言进行最基本的检查报错
,即词法分析
、语法分析
等,是一个静态
的阶段
runtime的使用有以下三
种方式,其三种实现方法与编译层和底层的关系如图所示
-
通过
OC代码
,例如 [person sayNB] -
通过
NSObject接口
,例如isKindOfClass -
通过
Runtime API
,例如class_getInstanceSize
我们定义一个LGPerson类
,在类中添加两个方法
,实现
其中一个方法。
生成一个对象,并调用这两个方法,我们可以看到在编译时候是没有报错的
。
运行一下。
报错了
。这就是编译时和运行时的区别。
clang 一下 .m 文件去看一下 这三个方法底层是怎么实现的。
这里可以看出,方法的本质
就是objc_msgSend消息发送
,并且消息发送需要两个非常重要的参数。一个就是消息接收者
(receiver),第二个就是消息的主体
。
我们为方法添加参数
看看会有什么变化。
重新clang
一下。
发现方法名
多了个:
,后面多加了一个参数
。这就说明,方法的主体
等于 方法名加参数
。
既然调用方法等同于objc_msgSend
,那么直接调用objc_msgSend
的效果是不是一样的呢。
验证一下:
运行一下:
发现方法跑了两次,说明[person sayNB]
等价于objc_msgSend(person,sel_registerName("sayNB"))
这里也可以用@selector
来传入sel
结果输出两次,说明是有效的。
注:
1、直接调用objc_msgSend
,需要导入头文件#import <objc/message.h>
2、需要将target --> Build Setting -->搜索msg -- 将enable strict checking of obc_msgSend calls由YES 改为NO
,将严厉的检查机制关掉,否则objc_msgSend的参数会报错。
那么如果子类调用父类的方法
,会是什么情况呢?一起来看一下
先添加LGTeacher类,并添加sayNB的实现,让LGPerson继承
自LGTeacher。
clang编译一下:
我们看到开头是有objc_msgSendSuper
方法的,但是没有调用。
这里去查找一下objc_msgSendSuper是如何实现的
发现objc_msgSendSuper需要的参数有
struct objc_super * super
: 一个objc_super的结构体SEL op
: 一个sel...
: 参数(可能没有)
SEL和参数我们知道是什么,我们去找一下objc_super
是个什么东西组成的。
我们压根就不看__!OBJC2__
里面的东西,所以只需要看receiver
和 super_class.
我们尝试在main 中调用objc_msgSendSuper
这个方法。
运行一下:
发现两者都成功调用了父类LGTeacher中的sayHello方法
。所以这里,我们可以作一个猜测:方法调用,首先是在类中查找,如果类中没有找到,会到类的父类中查找。
接下来,我们来探索objc_msgSend的源码实现
objc_msgSend 汇编源码
图片来自链接: Style_月月.
我们在方法调用的地方打个端点,因为方法调用的地方会调用objc_msgSend方法,如何运行。
程序运行到这里,然后打开Xcode 工具栏的debug - debug workflow - always showdisassembly
。
在objc_msgSend
的那一行打下断点 ,继续运行,然后按住control点击step in
。
看到objc_msgSend在libobjc.A.dylib
里面,接着去源码
里面搜索objc_msgSend
。
因为我们要找汇编,所以我们看.s文件。我们看到有不同架构下的objc-msg文件
,这里我们只看真机情况下也就是objc-msg-arm64.s
文件。
看到这个ENTRY _objc_msgSend
,点进去,看到了这样一段代码:
//---- 消息发送 -- 汇编入口--objc_msgSend主要是拿到接收者的isa信息
ENTRY _objc_msgSend
//---- 无窗口
UNWIND _objc_msgSend, NoFrame
//---- p0 和空对比,即判断接收者是否存在,其中p0是receiver,也就是person
cmp p0, #0 // nil check and tagged pointer check
//---- le小于 --支持taggedpointer(小对象类型)的流程
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
//---- p0 等于 0 时,直接返回 空
b.eq LReturnZero
#endif
//---- p0即receiver存在的流程
//---- 根据对象拿出isa ,即从x0寄存器指向的地址 取出 isa,存入 p13寄存器,这里也注释了得知p13是isa。
ldr p13, [x0] // p13 = isa
//---- 在64位架构下通过 p16 = isa(p13) & ISA_MASK,拿出shiftcls信息,得到class信息
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
//---- 如果有isa,走到CacheLookup 即缓存查找流程,也就是所谓的sel-imp快速查找流程
CacheLookup NORMAL, _objc_msgSend
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
//---- 等于空,返回空
b.eq LReturnZero // nil check
// tagged
adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
ubfx x11, x0, #60, #4
ldr x16, [x10, x11, LSL #3]
adrp x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
cmp x10, x16
b.ne LGetIsaDone
// ext tagged
adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
ubfx x11, x0, #52, #8
ldr x16, [x10, x11, LSL #3]
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
参考自Style_月月.
我们看一下这个汇编:
cmp
是英文compare
的缩写,也就是对比,p0
的话就是p0的地址
,也就是objc_msgSend的第一个参数-消息接收者receiver person的地址,判断person的地址是否为0
。
cmp p0, #0
le小于,支持taggedpointer(小对象类型)的流程
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
p0 等于 0 时,直接返回 空。
#else
b.eq LReturnZero
接下来就是p0(receiver)存在的流程,来看第一行。
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
这里做的是把x0(receiver)的首地址也就是isa存入p13,然后进入GetClassFromIsa_p16,将p13,1,xo作为参数传入。
这里会判断是否 SUPPORT_INDEXED_ISA
。这里是不支持SUPPORT_INDEXED_ISA的。
所以走到下面__LP64__
。在__LP64__
流程中,因为传过来的needs_auth ==1 所以会走到 ExtractISA p16, \src, \auth_address
。
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
#if SUPPORT_INDEXED_ISA
// 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
// 64-bit packed isa
ExtractISA p16, \src, \auth_address
.endif
#else
// 32-bit raw isa
mov p16, \src
#endif
.endmacro
接下来搜索一下ExtractISA
,
这里就是将传过来的地址$1 & ISA_MASK(得到class)
后存到$0
里面,也就是p16
里面。
所以
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
这段代码的作用就是将xo的首地址也就是isa放入到p13的寄存器里面,然后根据p13去获取class
放入到p16。为什么要获取class呢?因为cache里面的insert在objc_msgSend之前,而cache在class里面 ,所以我们要获取class。