在上一篇文章中,我们结合
objc源码分析了objc_msgSend的流程,今天我们使用真机,通过汇编流程来分析objc_msgSend
objc_msgSend的真机汇编流程
缓存未命中
新建一个工程,在工程中新建类Person:
@interface Person : NSObject
- (void)func1;
- (void)func2;
- (void)func3;
- (void)func4;
@end
@implementation Person
- (void)func1 {
NSLog(@"%s", __func__);
}
- (void)func2 {
NSLog(@"%s", __func__);
}
- (void)func3 {
NSLog(@"%s", __func__);
}
- (void)func4 {
NSLog(@"%s", __func__);
}
@end
在main函数中,断点如下:
进入汇编窗口:
Debug->Debug Workflow->Always Show Disassembly
进入objc_msgSend:
继续执行:
通过lldb打印,确认当前为Person类的对象方法func3的objc_msgSend流程:
x16=isa
x11=cache
判断
w11的0号位置是否不为0就是判断cache_t内是否有preopt_cache_t
x10=buckets
x12=第一个检索的位置
x13=bucket_t
x17=impx9=sel
x9和x1不相等,跳转到指令0x1b01e3210
指令跳转,判断
x9是否为0,是则继续跳转到_objc_msgSend_uncached
缓存未命中,跳转_objc_msgSend_uncached,接下来我们演示一下缓存命中的情形
缓存命中
main方法代码修改如下:
断点执行,进入汇编窗口,最终结果:
此时,
x10=buckets,x16=isa
buckets=buckets^_cmd,bucket=bucket^isa
汇编缓存
在上文中,缓存未命中是跳转到了__objc_msgSend_uncached,那么__objc_msgSend_uncached是什么呢?
__objc_msgSend_uncached
在objc源码中objc-msg-arm64.s的文件中:
其主要执行了两个方法MethodTableLookup和TailCallFunctionPointer
TailCallFunctionPointer
直接跳转$0,也就是x17,那么说明重要的操作在MehtodTableLookup中
MehtodTableLookup
跳转了_lookUpImpOrForward之后,将x0即imp赋值给x17,x0作为第一个寄存器,常用来接收返回值,那么就说明在_lookUpImpOrForward中,将imp赋值给了x0寄存器
lookUpImpOrForward
最后发现,是跳转到了C++的lookUpImpOrForward方法中:
那么为什么之前要用汇编查找缓存呢?那是因为汇编语言更接近机器语言,执行效率非常高并且汇编语言相对安全;汇编是一个快速查找的流程,而lookUpImpOrForward是一个慢速查找流程,其实是一个不断遍历methodlist的过程;
慢速查找流程
lookUpImpOrForward
由于lookUpImpOrForward最终需要返回一个imp,那么纵观方法实现,可以抓住其核心内容:
除此之外,我们需要留意两个方法checkIsKnownClass和realizeAndInitializeIfNeeded_locked
checkIsKnownClass
此方法会判断当前的class是否已经注册到当前的缓存表allocatedClasses里边(注册类);
realizeAndInitializeIfNeeded_locked
最终,再次方法中初始化父类和元类,并针对rw和ro的数据进行操作,因为在ro和rw中有methodList;
二分查找流程
接下来,我们重点分析在lookUpImpOrForward中究竟是如何查找方法的,分析发现查找imp的循环,没有结束条件,那么也就因为着,在for循环内部一定存在跳出for循环的条件即找到了imp;那么这个地方就是需要重点研究分析的核心所在;我们自己在开始分析之间简单梳理一下大致的查找流程:
- 1、先查找自己的
methodList,通过sel找到imp - 2、如果自己的
methodList找不到,那么根据以往经验,就需要查找父类或者NSObject,因为NSObject的父类是nil,那么如果我们还找不到,就需要跳出当前查找流程;
接下来我们具体分析源码:
接下来就进入了我们的核心查找流程:
接下来我们以例子来分析一下这个循环流程:
先理解一下count >>= 1的结果:
假如count = 8,那么8的二进制表达为1000,那么8 >> 1也就是1000右移1位,变成0100,是十进制的4,那么可以知道count >> 1其实可以理解为count减半(除以2);那么count >>= 1的结果就是count = count >> 1;
那么如果此时,列表中有8条数据,而第6个是我们要找的数据,这个流程分析如下:
第一次循环:
count = 8,base = 0,count >> 1 = 4, 那么probe = 0 + (4) = 4,因为keyValue > proValue即6 > 4,也就说明我们需要找的数据在5到8之间,那么base = 4 + 1 = 5,count-- = 7,
第一次循环结果:base = 5,count = 7
第二次循环:
count = 7,count >>= 1执行后count = 3,base = 5,3 >> 1 = 1,那么probe = 5 + 1 = 6,刚好找到我们的正确数据;
这就是二分查找流程
慢速查找递归流程
二分查找结束之后找到meth,那么会执行goto done
根据meth找到imp之后,执行goto done
在done中进行缓存填充log_and_fill_cache
之后调用insert进行缓存:
但是如果我们在之前的流程中没有找到imp呢?
如果当前没有找到meth,那么将curClass指向父类,在父类中继续查找imp
会通过汇编走父类的快速查找流程,那么就回到了与之前相似的流程:
父类的快速查找流程没找到,将会走父类的慢速查找流程,如果还没找到,将继续寻找父类,这是一个递归的查找流程,当curClass为nil,意味着我们所有的父类都找完了,也没找到,那么将会把forward_imp赋值给imp