之前的文章中有几次提到消息的发送,在编译的时候编译器就会把⽅法转换为objc_msgSend这个函数,今天就主要来探索下objc_msgSend这个函数。
编译文件
首先我们准备一个FMUserInfo的类,并定义两个实例方法与一个类方法。
@interface FMUserInfo : NSObject
- (void)getIconInfo;
- (void)getBankCard:(NSString *)number;
+ (void)commit;
@end
-------
//在main方法中调用
int main(int argc, const char * argv[]) {
FMUserInfo *p = [FMUserInfo alloc];
[p getIconInfo];
[p getBankCard:@"1234"];
}
然后我们使用clang -rewrite-objc main.m命令来编译下该文件。然后打开编译后的文件,我们就看到整个main方法编译成了如下代码。
在这里,我们可以看到:
[p getIconInfo]被编译成了objc_msgSend函数。第一个参数是接受者,第二个参数是方法名。- 当函数需要传递参数的时候则在
objc_msgSend函数的第二个参数后便继续加上参数三,参数四...
如果我们把((void (*)(id, SEL))(void *)objc_msgSend)((id)p, NSSelectorFromString(@"getIconInfo"));直接放到main函数中运行(需要导入#import <objc/message.h>头文件),可以发现其可以调用getIconInfo函数。说明函数的底层就是调用的objc_msgSend。
继承
如果类存在继承关系,objc_msgSend又是如何调用的呢,我们让FMAdminInfo继承自FMUserInfo,然后在FMAdminInfo中实现如下方法,并思考[super class]打印结果为啥呢?
@interface FMAdminInfo : FMUserInfo
-(void)getIconInfo;
@end
@implementation FMAdminInfo
- (instancetype)init
{
self = [super init];
if (self) {
NSLog(@"===>%@",[self class]);
NSLog(@"===>%@",[super class]);
}
return self;
}
-(void)getIconInfo{
[super getIconInfo];
NSLog(@"%s",__func__);
}
@end
编译文件后,我们可以看到
super关键字调用的函数在被编译后实际是调用objc_msgSendSuper函数进行消息转发,我们通过查看源码可以看到:objc_msgSend的第一个参数接受者为self,而objc_msgSendSuper的第一个参数接受者为objc_super。
OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
那么这里的objc_super又起到什么作用呢?在这里
receiver其意为消息的接收者,在源码注释中尤其说明消息接受者为类的实例super_class为从哪个类开始找 因此上方的那个问题[super class]其运行结果为FMAdminInfo,因为objc_msgSendSuper函数中消息传入的为类的实例也就是FMAdminInfo的实例。
关于
super_class我们也可以通过如下代码来验证下:
void* (*objc_msgSendSuperTyped)(struct objc_super *self,SEL _cmd) = (void *)objc_msgSendSuper;
FMUserInfo * info = [[FMUserInfo alloc]init];
FMAdminInfo * admin = [[FMAdminInfo alloc]init];
struct objc_super fm_super;
fm_super.receiver = admin; //接收者:子类
fm_super.super_class = info.class; //从父类开始找
objc_msgSendSuperTyped(&fm_super,sel_registerName("getIconInfo"));
fm_super.receiver = info; //接收者:父类
fm_super.super_class = info.class; //从父类开始找
objc_msgSendSuperTyped(&fm_super,sel_registerName("getIconInfo"));
fm_super.receiver = info; //接收者:父类
fm_super.super_class = admin.class; //从子类开始找
objc_msgSendSuperTyped(&fm_super,sel_registerName("getIconInfo"));
源码分析
接下来我们就来看下objc_msgSend的汇编实现。
- 首先准备一个类
FMUserInfo, - 并实例化一个类对象
p, - 让类对象
p调用实例方法saySomething,并加断点调试, - 然后进入
Debug->Debug Workflow ->Always Show Disassembly进入汇编调试模式下。刚进入,可以看到此时在18行,继续单步运行至20行后会发现跳转至
objc_msgSend
ldr从内存读取数据至寄存器,然后br进行跳转至objc_msgSend函数内部
objc_msgSend
这里我们先看下objc_msgSend的汇编源码
//进入objc_msgSend流程
ENTRY _objc_msgSend
//流程开始,无需frame
UNWIND _objc_msgSend, NoFrame
//判断p0(消息接受者)是否存在,不存在则重新开始执行objc_msgSend
cmp p0, #0 // nil check and tagged pointer check
//如果支持小对象类型。返回小对象或空
#if SUPPORT_TAGGED_POINTERS
//b是进行跳转,b.le是小于判断,也就是小于的时候LNilOrTagged
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
//等于,如果不支持小对象,就LReturnZero
b.eq LReturnZero
#endif
//通过p13取isa
ldr p13, [x0] // p13 = isa
//通过isa取class并保存到p16寄存器中
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
//LGetIsaDone是一个入口
LGetIsaDone:
// calls imp or objc_msgSend_uncached
//进入到缓存查找或者没有缓存查找方法的流程
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
// nil check判空处理,直接退出
b.eq LReturnZero // nil check
GetTaggedClass
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可以看到大体做了以下几件事:
- 参数合法性校验;
- 获取
isa,进而获取class; - 最终调用
CacheLookup函数来进行函数查找; 在汇编调试下大概如下如所示:此时通过lldb调试也能读取到相关信息:
CacheLookup 方法的快查找
在objc_msgSend获取到class类对象之后,就进入了整个方法查找的核心。
而CacheLookup函数就是在cache中通过sel查找imp的核心函数。其源码为:
//从x16中取出class移到x15中
mov x15, x16 // stash the original isa
//开始查找
LLookupStart\Function:
// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
//ldr表示将一个值存入到p10寄存器中
//x16表示p16寄存器存储的值,当前是Class
//#数值表示一个值,这里的CACHE经过全局搜索发现是2倍的指针地址,也就是16个字节
//#define CACHE (2 * __SIZEOF_POINTER__)
//经计算,p10就是cache
ldr p10, [x16, #CACHE] // p10 = mask|buckets
lsr p11, p10, #48 // p11 = mask
and p10, p10, #0xffffffffffff // p10 = buckets
and w12, w1, w11 // x12 = _cmd & mask
//真机64位看这个
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//CACHE 16字节,也就是通过isa内存平移获取cache,然后cache的首地址就是 (bucket_t *)
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
//获取buckets
#if __has_feature(ptrauth_calls)
tbnz p11, #0, LLookupPreopt\Function
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
//and表示与运算,将与上mask后的buckets值保存到p10寄存器
and p10, p11, #0x0000fffffffffffe // p10 = buckets
//p11与#0比较,如果p11不存在,就走Function,如果存在走LLookupPreopt
tbnz p11, #0, LLookupPreopt\Function
#endif
//按位右移7个单位,存到p12里面,p0是对象,p1是_cmd
eor p12, p1, p1, LSR #7
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
and p10, p11, #0x0000ffffffffffff // p10 = buckets
//LSR表示逻辑向右偏移
//p11, LSR #48表示cache偏移48位,拿到前16位,也就是得到mask
//这个是哈希算法,p12存储的就是搜索下标(哈希地址)
//整句表示_cmd & mask并保存到p12
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
ldr p11, [x16, #CACHE] // p11 = mask|buckets
and p10, p11, #~0xf // p10 = buckets
and p11, p11, #0xf // p11 = maskShift
mov p12, #0xffff
lsr p11, p12, p11 // p11 = mask = 0xffff >> p11
and p12, p1, p11 // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif
//去除掩码后bucket的内存平移
//PTRSHIFT经全局搜索发现是3
//LSL #(1+PTRSHIFT)表示逻辑左移4位,也就是*16
//通过bucket的首地址进行左平移下标的16倍数并与p12相与得到bucket,并存入到p13中
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
未完,下方继续
这里我们看到有茫茫多的代码,但这里做的事情并不复杂
- 首先是类对象平移16字节后获取到
cache结构体; - 根据不同的架构,获取到
buckets、cmd & mask,为下一步在哈希表中查找做准备。
//去除掩码后bucket的内存平移
//PTRSHIFT经全局搜索发现是3
//LSL #(1+PTRSHIFT)表示逻辑左移4位,也就是*16
//通过bucket的首地址进行左平移下标的16倍数并与p12相与得到bucket,并存入到p13中
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// do {
//ldp表示出栈,取出bucket中的imp和sel分别存放到p17和p9
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
//cmp表示比较,对比p9和p1,如果相同就找到了对应的方法,返回对应imp,走CacheHit
cmp p9, p1 // if (sel != _cmd) {
//b.ne表示如果不相同则跳转到2f
b.ne 3f // scan more
// } else {
2: CacheHit \Mode // hit: call or return imp
// }
//向前查找下一个bucket,一直循环直到找到对应的方法,循环完都没有找到就调用_objc_msgSend_uncached
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
//通过p13和p10来判断是否是第一个bucket
cmp p13, p10 // } while (bucket >= buckets)
b.hs 1b
总结来看:CacheLookup又大体分为如下流程:
- 从
x16寄存器中取出class移到x15寄存器中后,进入查找流程; - 获取
cache结构体 - 获取
buckets、cmd & mask - 根据
buckets找相应的sel - 如果有相应的
sel走CacheHit函数 - 如果没有相应的
sel会调用_objc_msgSend_uncached函数 通过源码可以看到_objc_msgSend_uncached又调用了MethodTableLookup,然后又调用_lookUpImpOrForward
_lookUpImpOrForward 方法的慢查找
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
//
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
runtimeLock.assertUnlocked();
//判断类是否初始化
if (slowpath(!cls->isInitialized())) {
behavior |= LOOKUP_NOCACHE;
}
runtimeLock.lock();
//判断是否是一个已知的类,这个类是否已经被加载了。
checkIsKnownClass(cls);
//用来确定类的一个继承链关系,方便后便进行方法查找
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
// runtimeLock may have been dropped but is now locked again
runtimeLock.assertLocked();
curClass = cls;
//以上代码主要是在做准备工作
//死循环,获取循环的上限
for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
//第一次从共享缓存的cache里边去找imp
//目的:防止多线程操作时,刚好调用函数,此时缓存进来了。
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
//从当前类的方法列表中查找,采用二分查找的方式。
// curClass method list.
method_t *meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
//找到方法,跳转至done
goto done;
}
//如果在本类中没有找到,就往父类中找,要注意的是,如果父类为空了,也就是在继承链中都没有找到该方法,就要进入到动态方法决议的过程
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = forward_imp;
break;
}
}
// Halt if there is a cycle in the superclass chain.
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
//从当前类中没有找到方法的时候,会从父类的cache中再去寻找,在调用父类的lookUpImpOrForward
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
if (fastpath(imp)) {
// Found the method in a superclass. Cache it in this class.
goto done;
}
}
// No implementation found. Try method resolver once.
//这里进行方法的动态决议,调用resolveMethod_locked
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
//调用cache的insert方法,将查找到的imp方法缓存到cache中,方便下次快速调用。
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
done_unlock:
runtimeLock.unlock();
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
首先我们可以看到这个函数的返回值也是IMP,说明这个方法也是用来查找方法的。代码的功能含义我们可以看下上方代码中注释,这里我们做一下流程总结:
- 首先对传入参数做合法性校验,包括类是否已初始化、是否已加载、类的继承链关系等;
- 采用
二分查找的方式,从已排序的类的方法列表中进行目标函数的查找,也就是调用getMethodNoSuper_nolock函数; - 如果在本类中没有找到目标函数,就按照继承链在父类中查找;
- 如果在继承链中也没有找到目标函数,就转入动态方法决议的过程,调用
resolveMethod_locked函数。 - 如果在继承链中找到了目标函数,跳转至
done,也就是调用cache的insert方法的过程,将查找到的imp方法缓存到cache中,方便下次快速调用。
- 如果在继承链中也没有找到目标函数,就转入动态方法决议的过程,调用
- 如果在本类中找到目标函数,跳转至
done,调用cache的insert方法,将查找到的imp方法缓存到cache中。
接下来我们就看下二分查找的流程
二分查找
首先getMethodNoSuper_nolock函数先获取了类中的函数数组,然后调用search_method_list_inline函数,然后该函数又调用findMethodInSortedMethodList函数进入二分查找流程。
static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
ASSERT(list);
//获取第一个元素
auto first = list->begin();
auto base = first;
//获取表达式类型
decltype(first) probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
//count >>= 1,数据长度减半
//base左边界
//probe左右边界的中间
//count数据长度以及划定右边界
for (count = list->count; count != 0; count >>= 1) {
probe = base + (count >> 1);
uintptr_t probeValue = (uintptr_t)getName(probe);
if (keyValue == probeValue) {
//找到了目标函数
// 再继续查找目标函数在函数列表中第一次出现的位置
// 因为分类中的函数是放在methodlist前边的,分类方法优先
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
probe--;
}
return &*probe;
}
//未找到目标函数 目标值要比折半位置大
if (keyValue > probeValue) {
//将左边界右移
base = probe + 1;
count--;
}
}
return nil;
}
总结:
- 这里通过二分查找获取目标函数,基本原理就是目标函数比有序数组指定范围(第一次进入指定范围为数组长度)的中间位置大,取右边(
base = probe + 1;count--;probe更新);目标函数比有序数组指定范围的中间位置小,取左边(count >>= 1;probe更新)。 - 二分查找到函数后,获取该函数最前边的位置,分类的方法优先调用;
整体流程总结
根据上述流程,我们可以把整个objc_msgSend的流程以流程图的方式做个呈现:
graph TB
A[_objc_msgSend receiver sel...] --> |判断p0消息接受者是否存在| B[判断是否支持小对象类型]
B -->D[ReturnZero]
B -->|进入_objc_msgSend主流程|Getisa[通过p13取isa]
Getisa --> F[通过isa取class并保存到p16寄存器中]
F --> |进入到缓存查找流程| CacheLookup[CacheLookup]
CacheLookup --> H[首先获取cache结构体]
H --> |通过内存平移mask buckets |I[然后获取到buckets]
I --> |进入到哈希查找流程|J[buckets中对比查找sel]
J --> |有sel|K[cacheHit 缓存命中] --> andend[调用imp]
J --> |没有sel|M[没有缓存命中] --> uncache[进入 _objc_msgSend_uncached]
uncache --> tableLookUp[进入MethodTableLookup]
tableLookUp --> lookUpImpOrForward[进入lookUpImpOrForward]
lookUpImpOrForward --> |首先对类进行合法性校验|First[先去共享缓存中查找 防止多线程操作时 调用了函数] --> huancun[cache_getImp] --> andend[返回imp]
First --> |从当前类的方法列表中查找|getMethodNoSuper_nolock[getMethodNoSuper_nolock]
getMethodNoSuper_nolock -->|通过cls->data->methods|methods[获取到methods]-->method_list[调用search_method_list_inline方法]-->SortedMethod[然后调用findMethodInSortedMethodList方法]
SortedMethod --> |进入二分查找流程|erfen[二分查找]
erfen -->|找到方法|cateMethod[判断是否在方法列表前边 分类方法优先]-->method_t[返回method_t]-->fillcache[调用log_and_fill_cache方法]-->|调用cache的插入方法 将找到的imp插入缓存|cacheInsert[调用cls->cache.insert] --> andend[返回imp]
erfen -->|没找到方法|superclass[调用getSuperclass 获取父类]-->getImp[调用父类cache_getImp方法获取imp] -->|如果找到| fillcache
getImp -->|如果还没找到|resolveMethod[进入动态方法决议查找resolveMethod_locked] .-> andend[调用imp]