iOS底層(六)-objc_msgSend分析(一)

1、Runtime簡介

咱們都知道OC語言具備一個運行時的能力, 而這個能力全是得益於Runtime.html

Runtime: 結合了一套匯編、C、C++混合寫成的, 爲咱們當前OC提供運行時功能. 是一套api.api

Runtime官方文檔緩存

1.1 編譯時與運行時

編譯時: 主要是將語言的語法等翻譯成機器能夠識別的語言, 可以進行中間連接bash

運行時: 整個代碼運行起來的時候, 把代碼裝載在內存中, 此時內存處於活躍狀態.主要是爲了檢查和前面編譯時的一些錯誤.app

1.2 Runtime應用的方式

在Runtime應用裏面, 主要是有三種方式ide

  1. 經過Objective-C code的方式來調用一些方法, 例如: @selector()
  2. 經過NSObject的方法 例如: NSSelectorFromString()
  3. 經過底層api來調用 例如: sel_registerName 函數api

2、方法的本質

經過clang來生成一下cpp函數

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
#import "MyTest.h"
void run() {
    NSLog(@"%s", __func__);
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        MyTest *test = [MyTest alloc];
        [test sayHello];
        run();
        NSLog(@"%@----", test);
    
    return 0;
}
複製代碼

編譯後:oop

#pragma clang assume_nonnull end
void run() {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders__l_vdk2zw41289f08gcw7n2499c0000gn_T_main_22ff9d_mi_0, __func__);
}
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        MyTest *test = ((MyTest *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MyTest"), sel_registerName("alloc"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)test, sel_registerName("sayHello"));
        run();
        NSLog((NSString *)&__NSConstantStringImpl__var_folders__l_vdk2zw41289f08gcw7n2499c0000gn_T_main_22ff9d_mi_1, test);
    }
    return 0;
}
複製代碼

從中能夠得知: OC方法的本質就是經過objc_msgSend進行發送消息優化

參數 做用
id 消息接收者(test)
sel 方法編號(sayHello)

經過id接收者去cache中去尋找key, 經過與上mask獲得index, 再查找到imp方法實現ui

發送消息 實現
對象方法 objc_msgSend() (經過id對象查找sel)
類方法 objc_msgSend() (經過類查找sel)
向父類發送實例方法 objc_msgSendSuper()方法
向父類發送類方法 objc_msgSendSuper()方法

3、消息查找機制

3.一、快速流程

經過彙編斷點調試進入 msg_msgSend

在750源碼中, 搜索 objc_msgSend, 找到 objc-msg-arm64.s 文件中. 找到 ENTRY _objc_msgSend

ENTRY _objc_msgSend //進入_objc_msgSend
	UNWIND _objc_msgSend, NoFrame//沒有窗口

	cmp	p0, #0 // nil check and tagged pointer check 對比當前的0號寄存器是否爲空, 判斷當前是否有接收者
#if SUPPORT_TAGGED_POINTERS
	b.le	LNilOrTagged  		//  (MSB tagged pointer looks negative)  若是爲空, 跳轉 LNilOrTagged
#else
	b.eq	LReturnZero
#endif
	ldr	p13, [x0]		// p13 = isa
	GetClassFromIsa_p16 p13		// p16 = class
LGetIsaDone:
	CacheLookup NORMAL		// calls imp or objc_msgSend_uncached   開啓緩存查找
複製代碼

進入objc_msgSend方法, 進入到正常流程, 拿取到isa(p13)

對isa進行一次 GetClassFromIsa_p16 操做:

.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA
//此處用於watch os 使用
	// Indexed isa
	mov	p16, $0			// optimistically set dst = src
	tbz	p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa //判斷是否爲nopointer 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
複製代碼

拿到isa判斷是否爲nopint isa, 而後對其作位移操做. (從alloc中能夠得知經過位移獲取shiftcls. 能夠經過蒙板獲取) 由此能夠得知此isa是一個優化過的isa

接下來

LGetIsaDone:
	CacheLookup NORMAL		// calls imp or objc_msgSend_uncached   開啓緩存查找
複製代碼

開啓緩存中查找方法:

.macro CacheLookup
	// p1 = SEL, p16 = isa
	ldp	p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask 平移16個字節獲得真正的cache_t
#if !__LP64__
	and	w11, w11, 0xffff	// p11 = mask  //32位系統
#endif
	and	w12, w1, w11		// x12 = _cmd & mask   //key與上mask獲得cache_hash(oc750源碼中 cache_hash(cache_key_t key, mask_t mak) 中)
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
		             // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))  拿到buckets

	ldp	p17, p9, [x12]		// {imp, sel} = *bucket    拿到bucket中的imp sel
1:	cmp	p9, p1			// if (bucket->sel != _cmd)    判斷是否匹配到緩存
    b.ne	2f			//     scan more    沒有命中緩存走2:
	CacheHit $0			// call or return imp    命中方法緩存返回方法實現到$0
	
2:	// not hit: p12 = not-hit bucket
	CheckMiss $0			// miss if bucket->sel == 0   沒有找到緩存
	cmp	p12, p10		// wrap if bucket == buckets。對比bucket
    b.eq	3f          // 對比同樣走3:
	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   從新存儲一份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
複製代碼

沒有命中緩存的處理:

.macro CheckMiss
	// miss if bucket->sel == 0
.if $0 == GETIMP //imp 是否爲 NORMAL LOOKUP
	cbz	p9, LGetImpMiss
.elseif $0 == NORMAL
	cbz	p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
	cbz	p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
複製代碼
.endmacro
	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
	
	...
	...
複製代碼

在cache_t 分析中知道方法是存儲在類的bit中的rw 與 ro中

.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	__class_lookupMethodAndLoadCache3//最終目標

	// IMP in x0
	mov	x17, x0
	
	// restore registers and return
	ldp	q0, q1, [sp, #(0*16)]
	ldp	q2, q3, [sp, #(2*16)]
	ldp	q4, q5, [sp, #(4*16)]
	ldp	q6, q7, [sp, #(6*16)]
	ldp	x0, x1, [sp, #(8*16+0*8)]
	ldp	x2, x3, [sp, #(8*16+2*8)]
	ldp	x4, x5, [sp, #(8*16+4*8)]
	ldp	x6, x7, [sp, #(8*16+6*8)]
	ldr	x8,     [sp, #(8*16+8*8)]

	mov	sp, fp
	ldp	fp, lr, [sp], #16
	AuthenticateLR

.endmacro
複製代碼

能夠看到最終目標是 __class_lookupMethodAndLoadCache3. 全局搜索 class_lookupMethodAndLoadCache3(彙編默認在方法前加上 ‘’)

直接跳轉到objc-msg-new.mm

/*********************************************************************** * _class_lookupMethodAndLoadCache. * Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp(). * This lookup avoids optimistic cache scan because the dispatcher * already tried that. **********************************************************************/
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
複製代碼

3、總結:

在C語言中不可能經過寫一個函數來保留未知的參數而且跳轉到一個任意的函數指針, C語言沒有知足作這件事的必要特性. 另外一個緣由就是objc_msgSend必須足夠快, 彙編是最接近機器語言的.

相關文章
相關標籤/搜索