OC之消息发送(objc_msgSend)

360 阅读10分钟

之前的文章中有几次提到消息的发送,在编译的时候编译器就会把⽅法转换为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方法编译成了如下代码。 image.png 在这里,我们可以看到:

  • [p getIconInfo]被编译成了objc_msgSend函数。第一个参数是接受者,第二个参数是方法名
  • 当函数需要传递参数的时候则在objc_msgSend函数的第二个参数后便继续加上参数三,参数四...

如果我们把((void (*)(id, SEL))(void *)objc_msgSend)((id)p, NSSelectorFromString(@"getIconInfo"));直接放到main函数中运行(需要导入#import <objc/message.h>头文件),可以发现其可以调用getIconInfo函数。说明函数的底层就是调用的objc_msgSend

image.png

继承

如果类存在继承关系,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

编译文件后,我们可以看到

image.png 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);
    

image.png

那么这里的objc_super又起到什么作用呢?在这里

  • receiver 其意为消息的接收者,在源码注释中尤其说明消息接受者为类的实例
  • super_class 为从哪个类开始找 因此上方的那个问题[super class]其运行结果为FMAdminInfo,因为objc_msgSendSuper函数中消息传入的为类的实例也就是FMAdminInfo的实例。

image.png 关于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"));

image.png

源码分析

接下来我们就来看下objc_msgSend的汇编实现。

  1. 首先准备一个类FMUserInfo
  2. 并实例化一个类对象p
  3. 让类对象p调用实例方法saySomething,并加断点调试,
  4. 然后进入Debug->Debug Workflow ->Always Show Disassembly 进入汇编调试模式下。 image.png 刚进入,可以看到此时在18行,继续单步运行至20行后会发现跳转至objc_msgSend

image.png 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函数来进行函数查找; 在汇编调试下大概如下如所示: iShot_2022-05-13_22.54.28.png 此时通过lldb调试也能读取到相关信息:

image.png

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结构体
  • 获取bucketscmd & mask
  • 根据buckets找相应的sel
  • 如果有相应的selCacheHit函数
  • 如果没有相应的sel会调用_objc_msgSend_uncached函数 通过源码可以看到_objc_msgSend_uncached又调用了MethodTableLookup,然后又调用_lookUpImpOrForward

image.png

_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函数进入二分查找流程。 image.png image.png

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]