第七节课 消息流程分析之快速查找(上)
在上篇文章我们分析了cache
的insert
流程,但是在insert
之前呢?还有一个cache
读取流程,即objc_msgSend
和 cache_getImp
。在分析之前,首先了解什么是Runtime
Runtime的运行时理解
编译时:
是将源代码翻译成机器能识别的代码的过程
,简单的一些翻译工作,主要是对语言进行最基本的检查报错,即词法分析、语法分析等,是一个静态的阶段
运行时:
是代码跑起来,被装载到内存中的过程
,如果此时出错,则程序会崩溃,是一个动态阶段
runtime
的使用有以下三种方式:
OC代码
: [persion sayHi]
NSObject
: isKindOfClass
Runtime API
: class_getInstanceSize
其三种实现方法与编译层和底层的关系如上图所示
其中的compiler
就是我们了解的编译器,即LLVM
,例如OC
的alloc
对应底层的objc_alloc
, runtime system libarary
就是底层库
探索方法的本质
写两个方法,只实现一个,并在main
函数进行调用
HZMPerson *person = [HZMPerson alloc];
[person sayNB];
[person sayHello];
在编译
的时候是成功
的,但是运行
起来后却崩溃
了,这其实就是我们上面说到过的编译时与运行时的区别
。
添加上未实现的方法,编译之后,clang
编译的源码查看cpp文件
。可以看到,再cpp
文件中的main
代码翻译成如下代码
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
HZMPerson *person = ((HZMPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("HZMPerson"), sel_registerName("alloc"));
HZMTeacher *teach = ((HZMTeacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("HZMTeacher"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_nd_w4zbcj291zn3gc4kvv8vv7bc0000gn_T_main_23e09d_mi_2);
}
return 0;
}
可以发现,上层的代码都会翻译成一个解释代码,可以看到我们调用方法的过程翻译过来是
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
这样一段代码,我们简化一下就是
调用方法 = 消息的发送: objc_msgsend
(消息的接收者,消息的主体(sel
+参数
))
为了验证下,我们通过手动调用objc_msgsend
来模仿方法调用
注意:
1、直接调用objc_msgSend
,需要导入头文件#import <objc/message.h>
2、需要将target --> Build Setting -->搜索msg -- 将enable strict checking of obc_msgSend calls
改为NO
,将严厉的检查机制关掉,否则objc_msgSend的参数会报错
[person sayNB];
objc_msgSend(person, @selector(sayNB));
结论:方法的本质
就是objc_msgSend
消息发送
除了验证,我们还可以尝试让person
的调用执行父类中实现,通过objc_msgSendSuper
实现
查看objc_msgSendSuper
的源码,看一下需要的参数。
objc_msgSendSuper
方法中有两个参数(结构体,sel)
,其结构体类型是objc_super
定义的结构体对象,且需要指定receiver 和 super_class
两个属性
struct objc_super zm_objc_super;
zm_objc_super.receiver = person;
zm_objc_super.super_class = HZMPerson.class;
objc_msgSendSuper(&zm_objc_super,@selector(sayHello));
按照objc_msgSendSuper
的格式创建zm_objc_super
的结构体,再补全两个属性,发现可以直接使用了,与我们之前直接调用方法的打印一模模一样样。
发现不论是[person sayHello]
还是objc_msgSendSuper
都执行的是父类中sayHello
的实现,所以这里,我们可以作一个猜测:方法调用,首先是在类中查找,如果类中没有找到,会到类的父类中查找。
接下来我们就来看看objc_msgSend
的源码实现
objc_msgSend流程上
掏出我们的源码吧
搜索
objc_msgSend
,因为objc_msgSend
是用汇编来实现的,所以我们直接找.s
文件进入arm64.s
。
objc_msgSend 汇编源码
//---- 消息发送 -- 汇编入口--objc_msgSend主要是拿到接收者的isa信息
ENTRY _objc_msgSend
//---- 无窗口
UNWIND _objc_msgSend, NoFrame
//---- p0 和空对比,即判断接收者是否存在,其中p0是objc_msgSend的第一个参数-消息接收者receiver
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寄存器
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
我们梳理一下主要的流程:
【第一步】判断objc_msgSend
方法的第一个参数receiver
是否为空
如果支持tagged pointer
,跳转至LNilOrTagged
如果小对象为空,则直接返回空,即LReturnZero
如果小对象不为空,则处理小对象的isa
,走到【第二步】
如果即不是小对象,receiver
也不为空,有以下两步
从receiver
中取出isa
存入p13
寄存器
通过 GetClassFromIsa_p16
中,arm64
架构下通过 isa & ISA_MASK
获取shiftcls
位域的类信息,即class,GetClassFromIsa_p16
的汇编实现如下,然后走到【第二步】
.macro GetClassFromIsa_p16 /* src */
//---- 此处用于watchOS
#if SUPPORT_INDEXED_ISA
// Indexed isa
//---- 将isa的值存入p16寄存器
mov p16, $0 // optimistically set dst = src
tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa -- 判断是否是 nonapointer isa
// isa in p16 is indexed
//---- 将_objc_indexed_classes所在的页的基址 读入x10寄存器
adrp x10, _objc_indexed_classes@PAGE
//---- x10 = x10 + _objc_indexed_classes(page中的偏移量) --x10基址 根据 偏移量 进行 内存偏移
add x10, x10, _objc_indexed_classes@PAGEOFF
//---- 从p16的第ISA_INDEX_SHIFT位开始,提取 ISA_INDEX_BITS 位 到 p16寄存器,剩余的高位用0补充
ubfx p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS // extract index
ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:
//--用于64位系统
#elif __LP64__
// 64-bit packed isa
//---- p16 = class = isa & ISA_MASK(位运算 & 即获取isa中的shiftcls信息)
and p16, $0, #ISA_MASK
#else
// 32-bit raw isa ---- 用于32位系统
mov p16, $0
#endif
.endmacro
【第二步】获取isa
完毕,进入慢速查找流程CacheLookup NORMAL