天天看點

iOS底層原理之方法的本質

前言

在之前的OC類的探索(三) - cache_t分析中,我們分析了方法緩存的調用流程,然後經過向上的探索,發現了objc_msgSend,今天來探索一下這個。

一、知識準備

1.資料

objc源碼

Runtime

2.Runtime

2.1 Runtime簡介

Runtime通常叫它運作時,還有一個大家常說的編譯時,它們之間的差別是什麼
  • 編譯時:顧名思義正在編譯的時候,啥叫編譯呢?就是編譯器把源代碼翻譯成機器能夠識别的代碼。編譯時會進行詞法分析,文法分析主要是檢查代碼是否符合蘋果的規範,這個檢查的過程通常叫做靜态類型檢查
  • 運作時:代碼跑起來,被裝裝載到記憶體中。運作時檢查錯誤和編譯時檢查錯誤不一樣,不是簡單的代碼掃描,而是在記憶體中做操作和判斷

2.2 Runtime版本

Runtime有兩個版本,一個Legacy版本(早期版本),一個Modern版本(現行版本)
  • 早期版本對應的程式設計接口:Objective-C 1.0
  • 現行版本對應的程式設計接口:Objective-C 2.0,
  • 源碼中經常看到的OBJC2早期版本用于Objective-C 1.0,32位的Mac OS X的平台
  • 現行版本用于Objective-C 2.0,iPhone程式和Mac OS X v10.5及以後的系統中的64位程式

2.3 Runtime調用三種方式

  • Objective-C方式,[penson sayHello]
  • Framework & Serivce方式,isKindOfClass
  • Runtime API方式,class_getInstanceSize
  • 三者關系圖:
    iOS底層原理之方法的本質

3.上次知識點補充

3.1 為什麼擴容是在容量的 3/4 時進行?

  • 3/4作為負載因子是大多數資料結構算法的共識,負載因子在0.75時空間的使用率是相對較大的;
  • cache存入方法時是根據hash算法計算出來的值作為存儲下标,緩存空間的剩餘大小對下标是否沖突至關重要,當3/4作為負載因子發生hash沖突的幾率相對較低;

二、方法的本質objc_msgSend

1.代碼檢視objc_msgSend

老規矩 直接上代碼

MHTeacher *t = [MHTeacher alloc];
[t sayHello];
[t skill:@"1222"];
           

編譯一下

MHTeacher *t = ((MHTeacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MHTeacher"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)t, sel_registerName("sayHello"));
((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)t, sel_registerName("skill:"), (NSString *)&__NSConstantStringImpl__var_folders_jt_46dmknbx2pb23kf9n3jncn740000gn_T_main_5862f5_mi_0);
           

我們看到 無論是alloc 、sayHello還是skill: 都變成了 objc_msgSend(id,sel_registerName(方法名)) 然後就是參數

是以方法的本質是objc_msgSend 消息轉,那讓我們來實驗一下:

[t sayHello];
objc_msgSend((id)t,sel_registerName("sayHello"));
一樣的結果:
111
111
           

tips

  • 必須導入相應的頭檔案#import <objc/message.h>
  • 關閉objc_msgSend檢查機制:target --> Build Setting -->搜尋objc_msgSend – Enable strict checking of obc_msgSend calls設定為NO

搜一下源碼,發現了:

#if !OBJC_OLD_DISPATCH_PROTOTYPES
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wincompatible-library-redeclaration"
OBJC_EXPORT void
objc_msgSend(void /* id self, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
#pragma clang diagnostic pop
#else
           

蕪湖還有個objc_msgSendSuper,有一個這個objc_super 看一下

#ifndef OBJC_SUPER
#define OBJC_SUPER

/// Specifies the superclass of an instance. 
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};
#endif
           

再調用一下,成功了。

struct objc_super my_objc_super;
    my_objc_super.receiver = t;
    my_objc_super.super_class = MHPerson.class;
    objc_msgSendSuper(&my_objc_super, @selector(sayHello1));
[61947:5799740] 2222
           

2.彙編檢視objc_msgSend

搜的過程中發現了這個:

ENTRY _objc_msgSend  // /_ objc_ _msgSend入口,此時有兩個參數-個是(就是isa)id receiver 還有一個是SEL_cmd
	UNWIND _objc_msgSend, NoFrame

	cmp	p0, #0			// nil check and tagged pointer check  receiver和0比較
#if SUPPORT_TAGGED_POINTERS   //  __LP64__ 64位系統支援Taggedpointer類型
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative) 小于等于0支援Taggedpointer類型 走LNilOrTagged流程
#else
	b.eq	LReturnZero  // 等于0直接傳回nil就是給- -個空對象發消息
#endif                      // 對象有值或者說isa有值
	ldr	p13, [x0]		// p13 = isa  //把x0寄存器裡面的位址讀取到p13寄存器,對象的位址等于isa的位址
	GetClassFromIsa_p16 p13, 1, x0	// p16 = class //
LGetIsaDone:       // 這是一 個标記符号,拿到isa操作完以後繼續後面的操作
	// calls imp or objc_msgSend_uncached   傳遞三個參數 NORMAL_ objc_ msgSend__ objc, _msgSend_ uncached
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
	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
	ENTRY _objc_msgLookup
           

2.1大緻思路:

判斷receiver是否等于nil, 在判斷是否支援Taggedpointer小對象類型

  • 支援Taggedpointer小對象類型,小對象為空 ,傳回nil,不為nil處理isa擷取class跳轉CacheLookup流程
  • 不支援Taggedpointer小對象類型且receiver = nil,跳轉LReturnZero流程傳回nil
  • 不支援Taggedpointer小對象類型且receiver != nil,通過GetClassFromIsa_p16把擷取到class 存放在p16的寄存器中,然後走CacheLookup流程,

2.2 GetClassFromIsa_p16

GetClassFromIsa_p16 核心功能擷取class存放在p16寄存器

// p13 , 1 , x0
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */

#if SUPPORT_INDEXED_ISA
	// Indexed isa
	mov	p16, \src			// 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_index[email protected]
	add	x10, x10, [email protected]
	ubfx	p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
	ldr	p16, [x10, p16, UXTP #PTRSHIFT]	// load class from array
1:

#elif __LP64__
.if \needs_auth == 0 // _cache_getImp takes an authed class already
	mov	p16, \src
.else
	// 64-bit packed isa
	ExtractISA p16, \src, \auth_address // 把 \src \auth_address 傳進ExtractISA 得到的結果指派給p16寄存器
.endif
#else
	// 32-bit raw isa
	mov	p16, \src
#endif
.endmacro
           

2.3 ExtractISA

ExtractISA 主要功能 isa & ISA_MASK = class 存放到p16寄存器

// A12 以上 iPhone X 以上的
#if __has_feature(ptrauth_calls)
   ...
#else
   ...
.macro ExtractISA
	and    $0, $1, #ISA_MASK  // and 表示 & 操作, $0 = $1(isa) & ISA_MASK  = class
.endmacro
// not JOP
#endif
           

2.4 CacheLookup流程

// NORMAL, _objc_msgSend, __objc_msgSend_uncached
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
	//
	// Restart protocol:
	//
	//   As soon as we're past the LLookupStart\Function label we may have
	//   loaded an invalid cache pointer or mask.
	//
	//   When task_restartable_ranges_synchronize() is called,
	//   (or when a signal hits us) before we're past LLookupEnd\Function,
	//   then our PC will be reset to LLookupRecover\Function which forcefully
	//   jumps to the cache-miss codepath which have the following
	//   requirements:
	//
	//   GETIMP:
	//     The cache-miss is just returning NULL (setting x0 to 0)
	//
	//   NORMAL and LOOKUP:
	//   - x0 contains the receiver
	//   - x1 contains the selector
	//   - x16 contains the isa
	//   - other registers are set as per calling conventions
	//

	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, #CACHE]				// p10 = mask|buckets  //CACHE 2*8 (2 * __SIZEOF_POINTER__) cache_t
	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
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets   //CACHE 2*8 (2 * __SIZEOF_POINTER__) cache_t
    #if CONFIG_USE_PREOPT_CACHES
        #if __has_feature(ptrauth_calls)
    tbnz	p11, #0, LLookupPreopt\Function
    and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
        #else
    and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
    tbnz	p11, #0, LLookupPreopt\Function // 不為0就跳轉LLookupPreopt
        #endif
    eor	p12, p1, p1, LSR #7  // p1右移7位
    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) //  PTRSHIFT = 3
						// 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
						//     } else {
2:	CacheHit \Mode				// hit:    call or return imp
						//     }
3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
	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

LLookupEnd\Function:
LLookupRecover\Function:
	b	\MissLabelDynamic

#if CONFIG_USE_PREOPT_CACHES
#if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16
#error config unsupported
#endif
LLookupPreopt\Function:
#if __has_feature(ptrauth_calls)
	and	p10, p11, #0x007ffffffffffffe	// p10 = buckets
	autdb	x10, x16			// auth as early as possible
#endif

	// x12 = (_cmd - first_shared_cache_sel)
	adrp	x9, [email protected]
	ldr	p9, [x9, [email protected]]
	sub	p12, p1, p9

	// w9  = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
#if __has_feature(ptrauth_calls)
	// bits 63..60 of x11 are the number of bits in hash_mask
	// bits 59..55 of x11 is hash_shift

	lsr	x17, x11, #55			// w17 = (hash_shift, ...)
	lsr	w9, w12, w17			// >>= shift

	lsr	x17, x11, #60			// w17 = mask_bits
	mov	x11, #0x7fff
	lsr	x11, x11, x17			// p11 = mask (0x7fff >> mask_bits)
	and	x9, x9, x11			// &= mask
#else
	// bits 63..53 of x11 is hash_mask
	// bits 52..48 of x11 is hash_shift
	lsr	x17, x11, #48			// w17 = (hash_shift, hash_mask)
	lsr	w9, w12, w17			// >>= shift
	and	x9, x9, x11, LSR #53		// &=  mask
#endif

	ldr	x17, [x10, x9, LSL #3]		// x17 == sel_offs | (imp_offs << 32)
	cmp	x12, w17, uxtw

.if \Mode == GETIMP
	b.ne	\MissLabelConstant		// cache miss
	sub	x0, x16, x17, LSR #32		// imp = isa - imp_offs
	SignAsImp x0
	ret
.else
	b.ne	5f				// cache miss
	sub	x17, x16, x17, LSR #32		// imp = isa - imp_offs
.if \Mode == NORMAL
	br	x17
.elseif \Mode == LOOKUP
	orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
	SignAsImp x17
	ret
.else
.abort  unhandled mode \Mode
.endif

5:	ldursw	x9, [x10, #-8]			// offset -8 is the fallback offset
	add	x16, x16, x9			// compute the fallback isa
	b	LLookupStart\Function		// lookup again with a new isa
.endif
#endif // CONFIG_USE_PREOPT_CACHES

.endmacro
           

源碼分析:首先是根據不同的架構判斷,下面都是以真機為例。上面這段源碼主要做了三件事

  • 擷取_bucketsAndMaybeMask位址也就是cache的位址:p16 = isa(class),p16 + 0x10 = _bucketsAndMaybeMask = p11
  • 擷取buckets位址就是緩存記憶體的首位址:buckets = ((_bucketsAndMaybeMask >> 48 )- 1 )
  • 擷取hash下标: p12 =(cmd ^ ( _cmd >> 7))& msak 這一步的作用就是擷取hash下标index
  • 流程如下:isa --> _bucketsAndMaybeMask --> buckets -->hash下标
  • 根據下标index 找到index對應的bucket。p13 = buckets + ((_cmd ^ (_cmd >> 7)) & mask) << (1+PTRSHIFT))
  • 先擷取對應的bucket然後取出imp和sel存放到p17和p9,然後*bucket–向前移動
    • 1流程:p9= sel和 傳入的參數_cmd進行比較。如果相等走2流程,如果不相等走3流程
    • 2流程:緩存命中直接跳轉CacheHit流程
    • 3流程:判斷sel = 0條件是否成立。如果成立說明buckets裡面沒有傳入的參數_cmd的緩存,沒必要往下走直接跳轉__objc_msgSend_uncached流程。如果sel != 0說明這個bucket被别的方法占用了。你去找下一個位置看看是不是你需要的。然後在判斷下個位置的bucket和第一個bucket位址大小,如果大于第一個bucket的位址跳轉1流程循環查找,如果小于等于則接繼續後面的流程
  • 如果循環到第1個bucket裡都沒有找到符合的_cmd。那麼會接着往下走,因為下标index後面的可能還有bucket還沒有查詢
    • 4流程: 如果 bucket 已經走到了 0 位置,還不相等:
`buckets + (mask << 1+PTRSHIFT) => mask 向左移動了 4 位置 => 7  16 => p13 定位到最後一個的位置。

 add p12, p10, p12, LSL #(1+PTRSHIFT) => p12 = first probed bucket
 
 ccmp p13, p12, #0, ne // bucket > first_probed)
 
 bucket > first_probed原因:因為之前已經比較過一次,是以這裡就必須大于 first_probed 否則還要走一次進行比較`
           

2.5 CacheHit

.macro CacheHit
.if $0 == NORMAL
	TailCallCachedImp x17, x10, x1, x16	// authenticate and call imp
.elseif $0 == GETIMP
	mov	p0, p17
	cbz	p0, 9f			// don't ptrauth a nil imp
	AuthAndResignAsIMP x0, x10, x1, x16	// authenticate imp and re-sign as IMP
9:	ret				// return IMP
.elseif $0 == LOOKUP
	// No nil check for ptrauth: the caller would crash anyway when they
	// jump to a nil IMP. We don't care if that jump also fails ptrauth.
	AuthAndResignAsIMP x17, x10, x1, x16	// authenticate imp and re-sign as IMP
	cmp	x16, x15
	cinc	x16, x16, ne			// x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
	ret				// return imp via x17
.else
.abort oops
.endif
.endmacro
           
  • mode 為normal 是以執行TailCallCachedImp
// A12 以上 iPhone X 以上的
#if __has_feature(ptrauth_calls)
   ...
#else
.macro TailCallCachedImp
	// $0 = cached imp, $1 = buckets, $2 = SEL, $3 = class(也就是isa)
	eor	$0, $0, $3   // $0 = imp ^ class 這一步是對imp就行解碼,擷取運作時的imp位址
	br	$0           //調用 imp
.endmacro
...
#endif
           
  • 緩存查詢到以後直接對bucket的imp進行解碼操作。即imp = imp ^ class,然後調用解碼後的imp

三、總結:

語言總是蒼白的,是以讓我們借用一張圖來總結一下objc_msgSend的流程:

iOS底層原理之方法的本質

請多指教

繼續閱讀