在上一篇文章中,我们结合
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
=imp
x9
=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