iOS底层原理09:消息慢速查找

404 阅读4分钟

在上一篇文章中,我们结合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类的对象方法func3objc_msgSend流程:

x16=isa

x11=cache

判断w110号位置是否不为0就是判断cache_t内是否有preopt_cache_t

x10=buckets

x12=第一个检索的位置

x13=bucket_t

x17=imp x9=sel

x9x1不相等,跳转到指令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的文件中:

其主要执行了两个方法MethodTableLookupTailCallFunctionPointer

TailCallFunctionPointer

直接跳转$0,也就是x17,那么说明重要的操作在MehtodTableLookup

MehtodTableLookup

跳转了_lookUpImpOrForward之后,将x0imp赋值给x17x0作为第一个寄存器,常用来接收返回值,那么就说明在_lookUpImpOrForward中,将imp赋值给了x0寄存器

lookUpImpOrForward

最后发现,是跳转到了C++lookUpImpOrForward方法中:

那么为什么之前要用汇编查找缓存呢?那是因为汇编语言更接近机器语言,执行效率非常高并且汇编语言相对安全;汇编是一个快速查找的流程,而lookUpImpOrForward是一个慢速查找流程,其实是一个不断遍历methodlist的过程;

慢速查找流程

lookUpImpOrForward

由于lookUpImpOrForward最终需要返回一个imp,那么纵观方法实现,可以抓住其核心内容:

除此之外,我们需要留意两个方法checkIsKnownClassrealizeAndInitializeIfNeeded_locked

checkIsKnownClass

此方法会判断当前的class是否已经注册到当前的缓存表allocatedClasses里边(注册类);

realizeAndInitializeIfNeeded_locked

最终,再次方法中初始化父类元类,并针对rwro的数据进行操作,因为在rorw中有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 = 8base = 0count >> 1 = 4, 那么probe = 0 + (4) = 4,因为keyValue > proValue6 > 4,也就说明我们需要找的数据在5到8之间,那么base = 4 + 1 = 5count-- = 7

第一次循环结果:base = 5count = 7

第二次循环:

count = 7count >>= 1执行后count = 3base = 53 >> 1 = 1,那么probe = 5 + 1 = 6,刚好找到我们的正确数据;

这就是二分查找流程

慢速查找递归流程

二分查找结束之后找到meth,那么会执行goto done

根据meth找到imp之后,执行goto done

done中进行缓存填充log_and_fill_cache

之后调用insert进行缓存:

但是如果我们在之前的流程中没有找到imp呢?

如果当前没有找到meth,那么将curClass指向父类,在父类中继续查找imp

会通过汇编走父类的快速查找流程,那么就回到了与之前相似的流程:

父类快速查找流程没找到,将会走父类慢速查找流程,如果还没找到,将继续寻找父类,这是一个递归的查找流程,当curClassnil,意味着我们所有的父类都找完了,也没找到,那么将会把forward_imp赋值给imp