消息查找流程(上)- 消息快速发送 之 objc_msgSend 分析

519 阅读7分钟

我们从类的结构分析过来知道了 cache_t ,在学习 cache_t 的时候,知道先进项方法查找,再进行方法缓存,那么 OC 调用方法的本质是什么呢?此时我们必然都回答 发消息,那么问题来了,OC 是如何发消息的呢?这就是我们接下来需要探索的。

如果有看到文章和源码不一样的话,是因为当前文章写的时候比较早,虽然在后期有修改,但是不能顾及所有。大致流程相同,自行比对即可。

1、runtime 的简单介绍

OC 中能进行以发消息的形式调用方法得益于 OC 强大的运行时,即 runtime

我们知道 OCC、C++、汇编 联合写成的语言,但是 CC++ 是静态语言,是编译时确定方法的类型和参数的个数,所以 runtime 是为这种集合写成的 OC 语言提供运行时能力的一套 API

关于runtime的官方介绍链接

2、 编译时和运行时的简单介绍

编译时: 当我们 commend+b 时进行的工作就是编译时,此时 LLVM 会将高级语言的语法、语义翻译成机器能够识别的语言,此时就是编译时。

运行时:当编译成功后会生成一个 machO 的可执行文件,当前将可执行文件中的代码装载在内存中运行就叫运行时。

3、runtime的使用方式

  • @selector()Objective-C Code
  • NSSelectorFormString()NSObject 的方法
  • sel_registerName 底层函数 API

4、使用 clang 查看编译后的源码

main.m 中些如下代码:

//main.m

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
       
        TestClass *object = [TestClass alloc];
        [object testClassInstanceMethod];
        
    }
    return 0;
}

在当前 main.m 的路径下使用 clang -rewrite-objc main.m -o main.cpp 翻阅到源码最后就能看到如下:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 


        TestClass *object = ((TestClass *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("TestClass"), sel_registerName("alloc"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)object, sel_registerName("testClassInstanceMethod"));

    }
    return 0;
}

此时就能看到方法的本质就是 objc_msgSendobjc_msgSend 有2个参数第一个为 id消息接受者,第二个是 SEL 方法编号,有这两个参数就可以进行方法具体实现的查找,比如去接收者的类 cache_t 中进行查找,在 cache_tbucket_t 就有2个成员 uintptr_t _imp;SEL _sel; 如果命中了就会返回方法的实现 imp

补充:如果在 OC 中直接写 run() 是不会使用 objc_msgSend 的,因为调用方法是一个查找函数实现的过程,而 run() 函数的名称就是函数的指针地址。

5、objc_msgSend 的使用介绍

main.m 中写如下代码

//main.m
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        TestClass *object = [TestClass alloc];
        
        //给对象发消息
        ((void(*)(id,SEL))objc_msgSend)(object,sel_registerName("testClassInstanceMethod"));
        
        //给类发消息
        ((void(*)(id,SEL))objc_msgSend)(object_getClass(object),sel_registerName("testClassClassMethod"));
        
        
        //给父类发消息(对象方法)1
        struct objc_super testSuper;
        testSuper.receiver = object;
        testSuper.super_class = object.superclass;
        ((void(*)(struct objc_super *,SEL))objc_msgSendSuper)(&testSuper,@selector(superClassInstanceMethod));
        
        
        //给父类发消息(对象方法)2
        struct objc_super testSuper_2;
        testSuper_2.receiver = object;
        testSuper_2.super_class = object.class;
        ((void(*)(struct objc_super *,SEL))objc_msgSendSuper)(&testSuper_2,@selector(superClassInstanceMethod));
        
        
        //给父类发消息(类方法)1
        struct objc_super testClassSuper_1;
        testClassSuper_1.receiver = [object class];
        testClassSuper_1.super_class = class_getSuperclass(object_getClass([object class]));
        ((void(*)(struct objc_super *,SEL))objc_msgSendSuper)(&testClassSuper_1,@selector(superClassClassMethod));
        
        
        //给父类发消息(类方法)2
        struct objc_super testClassSuper_2;
        testClassSuper_2.receiver = object;
        testClassSuper_2.super_class = objc_getMetaClass("TestClass");
        // object_getClass([object class]);
        ((void(*)(struct objc_super *,SEL))objc_msgSendSuper)(&testClassSuper_2,@selector(superClassClassMethod));
        
    }
    return 0;
}

给对象发消息和给对象的类发消息没有疑问,但是给父类发送消息时,发对象方法的消息和发类方法消息时,出了一点问题,按理来说都应该走上方代码 1 的流程才对,结果代码 2 也能发送,我们分析一下:

代码 1 的流程如下:

  • 给父类发送对象消息:receiver 是当前对象,super_class 是当前对象所属类的父类;
  • 给父类发送类消息:receiver 是当前对象的类,super_class 是当前对象所属类的元类的父类;

代码 2 的流程如下:

  • 给父类发送对象消息:receiver 是当前对象,super_class 是当前对象所属类;
  • 给父类发送类消息:receiver 是当前对象,super_class 是当前对象所属类的元类;

原因是在 struct objc_super 结构体中有一句这样的注释 super_class is the first class to search,也就是说 super_class 搜索父类中的一个类,OC 在搜索过程中,会以当前的类为起始点,然后遍历其父类,直到搜索到了要发送方法或者到 NSObject 的父类 nil 停止。

6、objc_msgSend 的源码分析

objc_msgSend 的源码其实是一段可怕的汇编,因为汇编是最接近机器语言的,一些未知的参数是 CC++ 难以处理的, 所以 OC 的开发者为了让效率更高,更加灵活,选用了汇编实现。

消息查找的流程分为2个流程:

  • 快速流程 就是当前需要探索的汇编
  • 慢速流程 就是类中方法的查找是 C++ 写的

当前我们只关注快速流程,慢速流程下一篇会探索。

搜索 objc_msgSend ,因为我们知道了 objc_msgSend 是汇编写的,只需要找文件后缀名为 .s 的,在 objc-msg-arm64.s 找到了 ENTRY _objc_msgSend 方法,这是 objc_msgSend 的开始。

1、 objc_msgSend 汇编的入口

先看下方汇编:

    //进入_objc_msgSend
    ENTRY _objc_msgSend

    UNWIND _objc_msgSend, NoFrame

    //p0: 是self 
    //cmp:比较指令
    //这里是判断接受者是不是空
    cmp	p0, #0			// nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
    //b.le : if 判断,如果成立就跳转 LNilOrTagged
    b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
    b.eq	LReturnZero
#endif
    //上方都不成立 
    //ldr : 将 x0 寄存器的值给 p13 ,p13 = isa
    //[x0]: 取 x0 寄存器的值
    ldr	p13, [x0]		// p13 = isa
    
    // 把 p13 传给 GetClassFromIsa_p16 进行运算,结果返回class
    GetClassFromIsa_p16 p13		// p16 = class
LGetIsaDone:
    //查找缓存
    CacheLookup NORMAL		// calls imp or objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    // b.eq : 如果相等就跳转
    // 结合上方的意思是:
    //如果 如果接受者是nil 就直接返回了
    b.eq	LReturnZero		// nil check
GetClassFromIsa_p16 获取 p16 = class

查看 GetClassFromIsa_p16 的汇编如下:

.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA
	// Indexed isa
	mov	p16, $0			// optimistically set dst = src
	tbz	p16, #ISA_INDEX_IS_NPI_BIT, 1f	// done if not non-pointer isa
	// isa in p16 is indexed
	adrp	x10, _objc_indexed_classes@PAGE
	add	x10, x10, _objc_indexed_classes@PAGEOFF
	ubfx	p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
	ldr	p16, [x10, p16, UXTP #PTRSHIFT]	// load class from array
1:

#elif __LP64__
	// 64-bit packed isa
	
	and	p16, $0, #ISA_MASK

#else
	// 32-bit raw isa
	mov	p16, $0

#endif

.endmacro

因为 SUPPORT_INDEXED_ISA 是适用于Apple Watch 开发的,所以对于我们当前是不满足条件的,SUPPORT_INDEXED_ISA 的结果就是 0,所以简化上方汇编,就是下方的了,其实就是 isa.bit & ISA_MASK,这不就是我们 getIsa() 的源码吗?返回必定是 Class

.macro GetClassFromIsa_p16 /* src */

    and p16, $0, #ISA_MASK
    // 32-bit raw isa
    mov p16, $0
	
.endmacro

2、汇编中缓存的查找

我们从上方的汇编能看到最后调用了 CacheLookup 去查找缓存,CacheLookup 汇编如下:

.macro CacheLookup
    // p1 = SEL, p16 = isa
    /* x16:是当前类
     * [x16, #CACHE]: 当期 x16寄存器的地址偏移 CACHE 个单位 就是 cache_t
     */
    ldp	p10, p11, [x16, #CACHE]	// p10 = buckets, p11 = occupied|mask
#if !__LP64__
    and	w11, w11, 0xffff	// p11 = mask
#endif
    //当前开始哈希查找了 取低32位 mask & sel
    and	w12, w1, w11		// x12 = _cmd & mask
    add	p12, p10, p12, LSL #(1+PTRSHIFT)
		             // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
    
    
    /**
     * bucket_t {
     *   int32 imp;
     *   SEL sel;
     * }
     */
    //取出 bucket 中的 imp 和 sel 给 p17 和 p9
    ldp	p17, p9, [x12]		// {imp, sel} = *bucket
    
    //将 p9 (sel) 和当前传入 SEL _cmd 比较
1:  cmp	p9, p1			// if (bucket->sel != _cmd)
    //如果不相等跳转 流程 2
    b.ne	2f			//     scan more
    //缓存命中返回 imp
    CacheHit $0			// call or return imp
	
2:  // not hit: p12 = not-hit bucket
    //如果没有找到就去找方法列表
    CheckMiss $0			// miss if bucket->sel == 0
    //比较 bucket == buckets
    cmp	p12, p10		// wrap if bucket == buckets
    //如果相等就跳转3
    b.eq	3f
    ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket
    b	1b			// loop

3:  // wrap: p12 = first bucket, w11 = mask
    // 存一份
    add	p12, p12, w11, UXTW #(1+PTRSHIFT)
		                        // p12 = buckets + (mask << 1+PTRSHIFT)
    //当缓存被破坏时,克隆扫描循环将会丢失而不是挂起。
    //慢速路径可能会检测到任何损坏,然后停止。
    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

    ldp	p17, p9, [x12]		// {imp, sel} = *bucket

    //此时又寻找了一次,原因是防止因为多线程的原因导致上一次写入没有完成 就去查找了缓存。所以在一次进行查找。
1:  cmp	p9, p1			// if (bucket->sel != _cmd)
    b.ne	2f			//     scan more
    CacheHit $0			// call or return imp
	
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0			// miss if bucket->sel == 0
    cmp	p12, p10		// wrap if bucket == buckets
    b.eq	3f
    ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket
    b	1b			// loop

3:  // double wrap
    JumpMiss $0
	
.endmacro
1、ldp p10, p11, [x16, #CACHE]

#CACHE 代表的是 CACHE 是一个宏定义,查找后发现 #define CACHE (2 * __SIZEOF_POINTER__),而 #define CLASS __SIZEOF_POINTER__ ,所以 #CACHE 就是 16 个字节。

[x16, #CACHE] 代表取 x16 寄存器中的地址平移16个字节,我们知道在 objc_classs 的结构中排列顺序为 : isa superclass cache_t .....,平移16个字节就是 cache_t

ldp p10, p11, [x16, #CACHE] 就是将 cache_t 的地址分别放入 p10p11 寄存器,p10 放入的是 bucketsp11 放入的是低32位 mask ,高32位是 occupied

2、CheckMiss $0

从上方的汇编知道,如果没有命中缓存就会走 CheckMiss,那 CheckMiss 到底都干了些什么呢? CheckMiss 的汇编如下:

.macro CheckMiss
	// miss if bucket->sel == 0
.if $0 == GETIMP
	cbz	p9, LGetImpMiss
.elseif $0 == NORMAL
	cbz	p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
	cbz	p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

因为传入的参数一直为 NORMAL ,所以走 cbz p9, __objc_msgSend_uncached ,搜索 __objc_msgSend_uncached,看到如下汇编:

STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search

//方法表的查找
MethodTableLookup
TailCallFunctionPointer x17

END_ENTRY __objc_msgSend_uncached

在上方汇编中看到 MethodTableLookup ,查找它的宏定义如下,能看到进行一系列参数入栈后,调用了 _lookUpImpOrForward 方法。

.macro MethodTableLookup
	
	// push frame
	SignLR
	stp	fp, lr, [sp, #-16]!
	mov	fp, sp

	// save parameter registers: x0..x8, q0..q7
	sub	sp, sp, #(10*8 + 8*16)
	stp	q0, q1, [sp, #(0*16)]
	stp	q2, q3, [sp, #(2*16)]
	stp	q4, q5, [sp, #(4*16)]
	stp	q6, q7, [sp, #(6*16)]
	stp	x0, x1, [sp, #(8*16+0*8)]
	stp	x2, x3, [sp, #(8*16+2*8)]
	stp	x4, x5, [sp, #(8*16+4*8)]
	stp	x6, x7, [sp, #(8*16+6*8)]
	str	x8,     [sp, #(8*16+8*8)]

	// receiver and selector already in x0 and x1
	mov	x2, x16
	
	//方法跳转
	bl  _lookUpImpOrForward

        //...

.endmacro

上述代码为什么要这么多参数入栈呢?我们上方说过因为未知参数的原因选择了汇编做消息发送,所以这里就是这个原因。

到此 objc_msgSend 的汇编分析完毕,这就是消息发送时的快递发送流程,下一篇探索 _lookUpImpOrForward慢速发送流程

以上就是 objc_msgSend 分析的内容了。

PS:可以运行的并且不断更新进行注释的objc_ 源码地址