阅读 918

iOS 通过汇编探索 objc_msgSend

一、前言

众所周知,OC 是一门动态语言,因为 runtime 的存在而变得强大,而在代码中调用方法就是给对象发送消息也是因为 runtime 的存在,调用方法就是调用 objc_msgSend 这个函数,那在底层又是怎么样的呢?汇编又是怎么一步步调用的呢?这篇文章会将通过汇编来分析 objc_msgSend 都做了啥。

二、OC 方法底层是什么样的

1、将方法进行 clang 编译

我们在 main 中写两个方法,然后对其进行 clang 一下,在 .cpp 文件最后能发现如下代码。

从编译情况下能看的出方法的底层就是通过调用 objc_msgSend 函数,第一个参数是消息接受者,第二个参数是方法名称(第二个参数可以替换成我们很熟悉的 @selector)。

简单来说给 OC 对象发送消息就是找函数实现的过程,OC 方法底层就是通过 sel 去找 imp 的过程,而 C 函数名就是函数指针,通过函数指针就可以直接找到函数实现。

三、通过汇编分析 objc_msgSend

1、如何去寻找 objc_msgSend 源码

main 函数中给方法打断点进行汇编分析,然后跳到 objc_msgSend 函数里面,如右图,就能得知 objc_msgSend 源码需要去 libobjc.A.dylib 库中找,操作如下图。

疑问点:为什么 objc_msgSend 是一段汇编,而不是 C 或者是 C++ 更加直接呢?

个人观点:

1.汇编更加容易被机器识别 2.参数未知,对于静态的 C 或者是 C++ 来说是很难接受的

2、 objc_msgSend 汇编分析

我们来到 objc.750版本 源码中,通过全局搜索 objc_msgSend,找到在 objc-msg-arm64.s 的汇编代码。

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    cmp	p0, // 判断当前 p0 寄存器是否为空,当前 p0 存的是 objc_object 对象地址
    
    // 处理对象是 tagged pointer 或 nil 的情况
    #if SUPPORT_TAGGED_POINTERS  
	b.le	LNilOrTagged   
    #else
	b.eq	LReturnZero
    #endif
    
    // 为正常的消息发送流程,就会走如下代码
	ldr	p13, [x0]		// p13 = isa,把 x0 指向内存的前 64 位放到 p13(即是 objc_object 的 isa 成员变量)
	GetClassFromIsa_p16 p13		// p16 = class,是一个宏,取面具,isa & ISA_MASK,得到当前类
LGetIsaDone:
	CacheLookup NORMAL	// 查找缓存
复制代码

此时对 isa 处理已经完成,已经找到当前类,接下来就是去缓存里面找方法,如果有直接返回对应的 imp ,接下来我们通过 command + F 搜索 CacheLookup,发现 CacheLookup 的参数分为三种,NORMAL(正常的去查找) 、 GETIMP(直接返回 IMP) 和 LOOKUP(主动的慢速去查找)。

.macro CacheLookup
	// p1 = SEL, p16 = isa
	// x16代表 class,#CACHE 是一个宏定义 #define CACHE (2 * __SIZEOF_POINTER__),代表16个字节
	// class 平移 CACHE(也就是16个字节)得到 cache_t,然后将 cache_t里面的 buckets 和 occupied|mask 赋值给 p10和p11
	// 为什么 occupied|mask 两个值给了一个寄存器呢?因为 occupied|mask 都是只占4字节,而一个寄存器是8字节,这样赋值给一个寄存器节省内存
	ldp	p10, p11, [x16, #CACHE]	// p10 = buckets, p11 = occupied|mask
	
#if !__LP64__
	and	w11, w11, 0xffff	// p11 = mask,iOS 为小端模式,w11只取前面四个字节,为 mask
#endif
	and	w12, w1, w11		// x12 = _cmd & mask,得到当前方法 hash 表的下标
	add	p12, p10, p12, LSL #(1+PTRSHIFT) // LSL 左移
	// p10(buckets) 平移 p12 左移 #(1+PTRSHIFT) 之后的值
		             // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

	ldp	p17, p9, [x12]		// {imp, sel} = *bucket,通过 bucket 取出方法的 imp 和 sel
    // 判断 bucket 的 sel 和 _cmd 是否相同,p9为sel
1:	cmp	p9, p1			// if (bucket->sel != _cmd)
    // 如果不同,走第二步,也就是 CheckMiss
	b.ne	2f			//     scan more
    // 如果相同,就会命中缓存,直接返回 imp,当前的 imp 存在 $0 里面
	CacheHit $0			// call or return imp
	
2:	// not hit: p12 = not-hit bucket
	CheckMiss $0			// miss if bucket->sel == 0
	// 比较 bucket
	cmp	p12, p10		// wrap if bucket == buckets
        // 相同,则会走第三步,将上面流程再走一次,重新查找一次,如果还是查找不到就会 JumpMiss
	b.eq	3f
	// 不同,就会 *--bucket 循环
	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
复制代码

上述分析,感觉 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,所以如果没找到就会走 __objc_msgSend_uncached

3、 CacheLookup 小结

根据上述分析得出 CacheLookup 包含读取方法缓存的核心逻辑,主要产生两种结果:若缓存命中,返回 IMP 或调用 IMP;若缓存未命中,调用 __objc_msgSend_uncached (找到IMP会调用) 或 __objc_msgLookup_uncached (找到IMP不会调用) 方法。

四、 MethodTableLookup 分析

CheckMiss 来到 __objc_msgSend_uncached

   STATIC_ENTRY __objc_msgSend_uncached
   UNWIND __objc_msgSend_uncached, FrameWithNoSaves
   
   MethodTableLookup
   TailCallFunctionPointer x17
   
   END_ENTRY __objc_msgSend_uncached
复制代码

MethodTableLookup 后面是比较复杂的逻辑,下面会分析,TailCallFunctionPointer x17 若找到了 IMP 会放到 x17 寄存器中,然后把 x17 的值传递给 TailCallFunctionPointer 宏调用方法。

MethodTableLookup

.macro MethodTableLookup
   
   // push frame
   SignLR
   // 后面要跳转函数,意味着lr的变化,所以开辟栈空间后需要把之前的fp/lr值存储到栈上便于复位状态
   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 是跳转,跳转到 __class_lookupMethodAndLoadCache3 方法
   bl	__class_lookupMethodAndLoadCache3

   // IMP in x0
   mov	x17, x0
   
   // restore registers and return
   ...
   
   mov	sp, fp
   ldp	fp, lr, [sp], #16
   AuthenticateLR

.endmacro
复制代码

当我们进行全局搜索 __class_lookupMethodAndLoadCache3 方法的时候却怎么也搜索不到,__ 代表着汇编函数,而 __class_lookupMethodAndLoadCache3C 函数,我们去掉一个 _ 进行全局搜索,我们就来到了 lookUpImpOrForward 消息查找流程

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{        
   return lookUpImpOrForward(cls, sel, obj, 
                             YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
复制代码

五、总结

  • <1> ENTRY _objc_msgSend
  • <2> 对消息接受者 (id self,sel _cmd) 判断处理
  • <3> LNilOrTagged 判断处理
  • <4> GetClassFromIsa_p16isa 的指针处理,isa & ISA_MASK 得到当前的类
  • <5> CacheLookup 查找缓存
  • <6> cache_t 处理 bucket 以及内存哈希的处理
  • <7> __objc_msgSend_uncached 告诉找不到缓存的 imp
  • <8> MethodTableLookup 跳转到方法查找的流程
  • <9> bl __class_lookupMethodAndLoadCache3 方法查找的流程开始
文章分类
iOS
文章标签