iOS 經過彙編探索 objc_msgSend

1、前言

衆所周知,OC 是一門動態語言,由於 runtime 的存在而變得強大,而在代碼中調用方法就是給對象發送消息也是由於 runtime 的存在,調用方法就是調用 objc_msgSend 這個函數,那在底層又是怎麼樣的呢?彙編又是怎麼一步步調用的呢?這篇文章會將經過彙編來分析 objc_msgSend 都作了啥。緩存

2、OC 方法底層是什麼樣的

一、將方法進行 clang 編譯

咱們在 main 中寫兩個方法,而後對其進行 clang 一下,在 .cpp 文件最後能發現以下代碼。bash

從編譯狀況下能看的出方法的底層就是經過調用 objc_msgSend 函數,第一個參數是消息接受者,第二個參數是方法名稱(第二個參數能夠替換成咱們很熟悉的 @selector)。

簡單來講給 OC 對象發送消息就是找函數實現的過程,OC 方法底層就是經過 sel 去找 imp 的過程,而 C 函數名就是函數指針,經過函數指針就能夠直接找到函數實現。函數

3、經過彙編分析 objc_msgSend

一、如何去尋找 objc_msgSend 源碼

main 函數中給方法打斷點進行彙編分析,而後跳到 objc_msgSend 函數裏面,如右圖,就能得知 objc_msgSend 源碼須要去 libobjc.A.dylib 庫中找,操做以下圖。oop

疑問點:爲何 objc_msgSend 是一段彙編,而不是 C 或者是 C++ 更加直接呢?post

我的觀點:ui

1.彙編更加容易被機器識別 2.參數未知,對於靜態的 C 或者是 C++ 來講是很難接受的spa

二、 objc_msgSend 彙編分析

咱們來到 objc.750版本 源碼中,經過全局搜索 objc_msgSend,找到在 objc-msg-arm64.s 的彙編代碼。3d

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 裏面應該有咱們想找的代碼,接下來就去分析一下 CheckMissrest

.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

三、 CacheLookup 小結

根據上述分析得出 CacheLookup 包含讀取方法緩存的核心邏輯,主要產生兩種結果:若緩存命中,返回 IMP 或調用 IMP;若緩存未命中,調用 __objc_msgSend_uncached (找到IMP會調用) 或 __objc_msgLookup_uncached (找到IMP不會調用) 方法。

4、 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*/);
}
複製代碼

5、總結

  • <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 方法查找的流程開始
相關文章
相關標籤/搜索