iOS底层学习-objc_msgSend

632 阅读3分钟

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

一、runtime运行时

1、编译时和运行时

  • 编译时:顾名思义就是正在编译的时候,那啥叫编译呢?就是编译器帮你把源代码翻译成机器能识别的代码。编译时会检查代码、词法分析,语法分析。这个检查的过程通常叫做静态类型检查
  • 运行时:代码跑起来,被装装载到内存中。运行时检查错误和编译时检查错误不一样,不是简单的代码扫描,而是在内存中做操作和判断

2、runtime版本

Runtime有两个版本 ⼀个Legacy版本(早期版本) ,⼀个Modern版本(现⾏版本)

  • 早期版本对应的编程接口:Objective-C 1.0
    早期版本用于Objective-C 1.0,32位的Mac OS X的平台
  • 现行版本对应的编程接口:Objective-C 2.0,源码中经常看到的OBJC2
    现行版本用于Objective-C 2.0,iPhone程序和Mac OS X v10.5及以后的系统中的64位程序

3、runtime的发起方式

runtime发起方式有三种:

  • 通过OC方法[obj method]
  • 通过NSObject的方法,isKindofClass performSelector
  • 通过objc底层提供的apiclass_getInstanceSize objc_msgSend

4、底层分析方法的调用

观察下面代码,创建一个对象,调用其两个方法

LRPerson *person = [LRPerson alloc];
[person saySomething];
[person sayHello:@"Hello"];

通过clang命令讲代码编译成C++代码

LRPerson *person = ((LRPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LRPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("saySomething"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello:"), (NSString *)&__NSConstantStringImpl__var_folders_tc_3wwljjy118gc8b_00djjc3sh0000gn_T_main_4057b5_mi_2);

可以发现在底层中,方法的调用,都是通过objc_msgSend函数

所以方法调用的本质即消息发送: objc_msgSend(消息的接受者,消息的主体(sel + 参数))

5、objc_msgSend

既然方法调用是通过objc_msgSend发送消息。那么直接在代码中通过objc_msgSend调用方法是否可行呢?

调用之前先在Build Setting中将Enable Strict Checking of objc_msgSend Calls设置为NO,关掉编译器检查

#import <objc/message.h>

LRPerson *person = [LRPerson alloc];
[person saySomething];
objc_msgSend(person, @selector(saySomething));
[person sayHello:@"Hello"];

经尝试发现打印结果一致,可直接用objc_msgSend调用方法

二、objc_msgSend底层汇编分析

1、查找汇编源码

在调用方法打断点进入汇编,找到objc_msgSend就跟进去可以发现objc_msgSend来自objc底层源码

在底层源码中查找objc_msgSend,直接看arm64真机架构,在objc_msg_arm64.s中查看底层源码,找到ENTRY入口 首先找下源码中用到的宏定义,看下图:

2、源码分析

    ENTRY _objc_msgSend // 入口_objc_msgSend(id receiver, SEL _cmd)
    UNWIND _objc_msgSend, NoFrame
    
    // cmp: p0和0作比较,p0为第一个参数receiver(消息接收者)
    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS  // 64位系统,判断支持TaggedPointers类型
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)  <=0跳转LNilOrTagged
#else
    b.eq    LReturnZero  // ==0  消息为空 跳转LReturnZero
#endif
    ldr p13, [x0]       // p13 = isa    将x0寄存器里的地址给到p13,receiver首地址即isa地址,所以p13 = isa
    GetClassFromIsa_p16 p13, 1, x0  // p16 = class  GetClassFromIsa_p16(isa, 1, receiver) 通过isa获取class
LGetIsaDone: // 获取到isa后继续走下面流程
    // calls imp or objc_msgSend_uncached   查找cache CacheLookup(NORMAL,_objc_msgSend,__objc_msgSend_uncached)
    CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    b.eq    LReturnZero     // nil check
    GetTaggedClass          // p16 = class
    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第一个参数receiver是否为空,并判断支持TaggedPointers类型
  • 若支持且receiver <= 0则跳转至LNilOrTagged判断receiver是否为空,为空跳转LReturnZero,不为空通过GetTaggedClass得到p16 = class,再跳转至LGetIsaDone
  • 若不支持receiver为空则跳转LReturnZeroreceiver不为空则往下走,先通过receiver获取isa并赋值到p13,再通过GetClassFromIsa_p16的到p16 = class
  • 最后都会走到LGetIsaDone,走CacheLookup缓存查找流程

查看CacheLookup源码:

.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
        ...
        
	mov	x15, x16			// stash the original isa   x15 = class
LLookupStart\Function:
	// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS  // arm64模拟器
	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
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16  // arm64真机
        // CACHE : (2 * __SIZEOF_POINTER__)
        // [x16, #CACHE] = isa + 0x10 = cache地址 = _bucketsAndMaybeMask地址
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets  p11 = _bucketsAndMaybeMask
#if CONFIG_USE_PREOPT_CACHES  // arm64真机
#if __has_feature(ptrauth_calls)  //  A12芯片以上  iPhone X
	tbnz	p11, #0, LLookupPreopt\Function // tbnz 测试位不为0则跳转。与tbz对应
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
	tbnz	p11, #0, LLookupPreopt\Function // 若_bucketsAndMaybeMask第0位!=0,则跳转至LLookupPreopt\Function
#endif
	eor	p12, p1, p1, LSR #7             // eor异或(^) p12 = p1 ^(p1 >> 7)
        // p12 = p12 & (_bucketsAndMaybeMask >> 48) = index & mask值 = buckets下标
	and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
	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

	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
                                                // 遍历查找
						// do {
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--     内存平移 向前查找
	cmp	p9, p1				//     if (sel != _cmd) {         没找到
	b.ne	3f				//         scan more              跳转到第3步
						//     } else {
2:	CacheHit \Mode				// hit:    call or return imp     缓存命中,返回imp
						//     }
3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;   没找到走__objc_msgSend_uncached
	cmp	p13, p10			// } while (bucket >= buckets)
	b.hs	1b

	// wrap-around:
	//   p10 = first bucket
	//   p11 = mask (and maybe other bits on LP64)
	//   p12 = _cmd & mask
	//
	// A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
	// So stop when we circle back to the first probed bucket
	// rather than when hitting the first bucket again.
	//
	// Note that we might probe the initial bucket twice
	// when the first probed slot is the last entry.


#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
	add	p13, p10, w11, UXTW #(1+PTRSHIFT)
						// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
						// p13 = buckets + (mask << 1+PTRSHIFT)
						// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	add	p13, p10, p11, LSL #(1+PTRSHIFT)
						// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
						// p12 = first probed bucket

						// do {
4:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
	cmp	p9, p1				//     if (sel == _cmd)
	b.eq	2b				//         goto hit
	cmp	p9, #0				// } while (sel != 0 &&
	ccmp	p13, p12, #0, ne		//     bucket > first_probed)
	b.hi	4b

...

.endmacro

大体流程如下:

  • class内存平移找到cachecache中有bucketmask
  • 通过异或运算和移位运算得到bucket的下标index
  • 取出bucket中的sel和传进来的_cmd进行对比,如果相等则缓存命中返回imp
  • 不相等则bucket平移进行循环遍历查找
  • 如果找不到则走__objc_msgSend_uncached