主頁 > 移動端開發 > iOS底層原理(五)Runtime(上)

iOS底層原理(五)Runtime(上)

2021-04-08 07:57:17 移動端開發

什么是Runtime

Objective-C是一門動態性比較強的編程語言,跟C、C++等語言有著很大的不同;Objective-C的動態性是由Runtime API來支撐的

Runtime API提供的介面基本都是C語言的,原始碼由C\C++\匯編語言撰寫

方法型別的底層結構

Class物件的底層結構objc_class中,我們知道通過bits & FAST_DATA_MASK就可以得到class_rw_t型別的表結構

class_rw_t里面的methods、properties、protocols都是二維陣列,是可讀可寫的,包含了類的初始內容、分類的內容

method_array_t舉例,里面的元素都是method_list_t型別的二維陣列,每一個二維陣列又是method_t型別的元素,表示每一個方法型別

class method_array_t : 
    public list_array_tt<method_t, method_list_t, method_list_t_authed_ptr>
{
    typedef list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> Super;

 public:
    method_array_t() : Super() { }
    method_array_t(method_list_t *l) : Super(l) { }

    ....
};

class_ro_t里面的baseMethodList、baseProtocols、ivars、baseProperties是一維陣列,是只讀的,包含了類的初始內容

objc原始碼里的頭檔案objc-runtime-new.mm,通過查看函式realizeClassWithoutSwift的實作,程式運行時會將class_ro_t里面的資料和分類里面的資料資訊全部合并到一起放到class_rw_t里面來

static Class realizeClassWithoutSwift(Class cls, Class previously)
{
    ....
    
    auto ro = (const class_ro_t *)cls->data();
    auto isMeta = ro->flags & RO_META;
    if (ro->flags & RO_FUTURE) {
        // This was a future class. rw data is already allocated.
        rw = cls->data();
        ro = cls->data()->ro();
        ASSERT(!isMeta);
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        // 分配記憶體空間,將ro的資料放到rw
        rw = objc::zalloc<class_rw_t>();
        rw->set_ro(ro);
        rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
        cls->setData(rw);
    }

    cls->cache.initializeToEmptyOrPreoptimizedInDisguise();

	....	
}

method_t

method_t是對方法\函式的封裝,里面包含了函式名,編碼資訊以及函式地址

struct method_t {
    static const uint32_t smallMethodListFlag = 0x80000000;

    method_t(const method_t &other) = delete;

    struct big {
        SEL name; // 函式名
        const char *types; // 編碼(回傳值型別、引數型別)
        MethodListIMP imp; // 指向函式的指標
    };
}

IMP代表函式的具體實作,指向著該函式的地址

typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 

SEL代表方法\函式名,一般叫做選擇器,底層結構跟char *類似,可以通過@selector()sel_registerName()獲得

typedef struct objc_selector *SEL;

不同類中相同名字的方法,所對應的方法選擇器是相同的,可以通過sel_getName()NSStringFromSelector()轉成字串

types包含了函式回傳值、引數編碼的字串,排列順序如下

iOS中提供了一個叫做@encode的指令,可以將具體的型別表示成字串編碼

一個函式默認會帶有兩個引數id _NonnullSEL _Nonnull,之后才是寫入的引數

下面舉例說明,函式的types是多少

- (int)test:(int)age height:(float)height
{
    NSLog(@"%s", __func__);
    return 0;
}

// 該函式types為i24@0:8i16f20
// i 回傳值int型別
// 24 幾個回傳值型別占據的大小總和(8 + 8 + 4 + 4)
// @ id型別
// 0 表示從第0位開始
// : SEL型別
// 8 從第8位開始
// i 引數int型別
// 16 從第16位開始
// f 引數float型別
// 20 從第20位開始

方法快取

Class內部結構中有個方法快取cache_t,用散串列(哈希表)來快取曾經呼叫過的方法,可以提高方法的查找速度

struct cache_t {
private:
	explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
	union {
	   struct {
	       explicit_atomic<mask_t>    _maybeMask; // 散串列的長度 - 1
	#if __LP64__
	       uint16_t                   _flags;
	#endif
	       uint16_t                   _occupied; // 已經快取的方法數量
	   };
	   explicit_atomic<preopt_cache_t *> _originalPreoptCache;
	};
	
	....
	
	mask_t mask() const; 
	
public:
    unsigned capacity() const;
    struct bucket_t *buckets() const; // 散串列
    Class cls() const;
    
    ....
}

散串列bucket_t內部有SEL作為key函式地址IMP一一對應

struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
    explicit_atomic<uintptr_t> _imp; // 函式的記憶體地址
    explicit_atomic<SEL> _sel; // sel為key
#else
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif

	....
}

objc-cache.mm檔案里可以查看 cache_t::insert函式,是通過一套哈希演算法計算出索引,然后根據索引在散串列陣列里直接插入資料進行快取

void cache_t::insert(SEL sel, IMP imp, id receiver) {

    ....
    
    mask_t newOccupied = occupied() + 1;
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    if (slowpath(isConstantEmptyCache())) {
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE;
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }
    else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
        // Cache is less than 3/4 or 7/8 full. Use it as-is.
    }
#if CACHE_ALLOW_FULL_UTILIZATION
    else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
        // Allow 100% cache utilization for small buckets. Use it as-is.
    }
#endif
    else {
        // 如果空間已滿,那么就進行擴容,乘以2倍
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        
        // 將舊的快取釋放,清空快取,然后設定最新的mask值
        reallocate(oldCapacity, capacity, true);
    }

    bucket_t *b = buckets();
    mask_t m = capacity - 1;
    
    // 通過 sel&mask 計算出索引(哈希演算法)
    mask_t begin = cache_hash(sel, m);
    mask_t i = begin;

    do {
        // 通過索引找到的該SEL為空,那么就插入bucket_t
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();
            b[i].set<Atomic, Encoded>(b, sel, imp, cls());
            return;
        }
        
        // 用索引從bucket里面取sel和傳進來的sel做比較,如果一樣證明已經存有,直接回傳
        if (b[i].sel() == sel) {
            return;
        }
        
        // 從散串列里查找,如果上述條件不成立(索引沖突),那么通過cache_next計算出新的索引再查找插入
    } while (fastpath((i = cache_next(i, m)) != begin));

    bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}

下面是cache_t::insert的一些詳細呼叫決議

當存盤空間已滿時,會進行擴容,并且將舊的快取全部釋放清空,然后設定最新的mask值mask值是散串列的存盤容量-1,也正好對應散串列的索引值

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld) {
    bucket_t *oldBuckets = buckets();
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    ASSERT(newCapacity > 0);
    ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);

    setBucketsAndMask(newBuckets, newCapacity - 1);
   
    // 將舊的快取和mask釋放
    if (freeOld) {
        collect_free(oldBuckets, oldCapacity);
    }
}

cache_hash的哈希演算法就是將mask進行一次位運算,所得的索引值只會小于等于mask值

static inline mask_t cache_hash(SEL sel, mask_t mask) {
    uintptr_t value = https://www.cnblogs.com/funkyRay/p/(uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
    value ^= value >> 7;
#endif
    return (mask_t)(value & mask);
}

如果計算出的索引在散串列中已經有了快取資料,那么就通過cache_next更新下索引值,再去對應的位置插入快取資料

通過原始碼可以看到計算方式如下:

有沖突的索引如果不為0就直接索引值減1,然后再根據新的索引值去散串列中對應插入

如果沖突的索引為0,那么直接就將mask賦值給新的索引值,再去對應查找插入

#if CACHE_END_MARKER
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}
#elif __arm64__
// arm64架構下如果索引非0,就是i-1,索引為0回傳mask
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}

快取的資料在散串列中都對應著一定的空間,所以這套查找演算法就是利用了空間換時間,來增加效率

方法呼叫的底層結構

我們先將下面的代碼通過Clang的命令生成C++代碼,如下所示

@interface Person : NSObject

- (void)test;
+ (void)test;
@end

@implementation Person

- (void)test
{
    NSLog(@"%s", __func__);
}

+ (void)test
{
    NSLog(@"%s", __func__);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    	Person *person = [[Person alloc] init];

     	[person test];
		[Person test];
    }
    return 0;
}

// 轉換成C++檔案后的兩個呼叫方法為:
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("test"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("test"));

發現函式呼叫最后都是轉化為objc_msgSend,尤其我們可以推斷出方法的呼叫本質就是objc_msgSend訊息發送

由于sel_registerName@selector回傳值都是SEL,我們通過列印兩個方法的地址是一樣的,可以確定兩個方法是可以等同的

NSLog(@"%p, %p", @selector(test), sel_registerName("test"));
// 列印結果都是0x100003f66

我們對兩個C++函式簡化之后,就得到以下兩個方法

objc_msgSend(person, @selector(test));
// 訊息接收者(receiver):person
// 訊息名稱:test

objc_msgSend([Person class], @selector(test));
// 訊息接收者(receiver):[Person class]
// 訊息名稱:test

而且第一個引數分別可以寫為person[Person class],也可以稱為訊息的接收者

第二個引數都是testSEL就是訊息的名稱

方法呼叫的執行流程

objc_msgSend的執行流程可以分為3大階段

  • 訊息發送
  • 動態方法決議
  • 訊息轉發

訊息發送

我們在objc原始碼里全域搜索關鍵字objc_msgSend可以發現,訊息發送的入口一開始是在objc-msg-arm64.s中通過匯編來實作第一步的

下面我們開始分析原始碼

【第一步】 這里主要就是判斷receiver是否有值,然后對應的跳轉,isa指標ISA_MASK進行位運算獲取到Class資料,并且要先去查找是否有快取方法,如果沒有則要去更深的Class資料中查找了

// MARK: _objc_msgSend的實作:匯編入口(ENTRY是入口的意思)
	ENTRY _objc_msgSend
    // 無視窗
	UNWIND _objc_msgSend, NoFrame
    
    // p0暫存器里存盤的值(也就是引數receiver)和0做對比(cmp是比較的意思)
	cmp	p0, #0               // nil check and tagged pointer check
// 支持TAGGED_POINTERS的流程
#if SUPPORT_TAGGED_POINTERS
    // 如果小于等于0,則跳轉LNilOrTagged
	b.le	LNilOrTagged     //  (MSB tagged pointer looks negative)
#else
    // 如果等于0,則跳轉LReturnZero
	b.eq	LReturnZero
#endif
    // 根據物件拿出isa ,即從x0暫存器指向的地址 取出 isa,存入 p13暫存器
	ldr	p13, [x0]		// p13 = isa
    // 在64位架構下通過 p16 = isa(p13) & ISA_MASK,拿出shiftcls資訊,得到class資訊
	GetClassFromIsa_p16 p13, 1, x0	// p16 = class
LGetIsaDone:
	// calls imp or objc_msgSend_uncached
    // 如果從快取中找不到,則跳轉__objc_msgSend_uncached
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    // 如果等于0,則跳轉LReturnZero
	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
	UNWIND _objc_msgLookup, NoFrame
	cmp	p0, #0			// nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
	b.le	LLookup_NilOrTagged	//  (MSB tagged pointer looks negative)
#else
	b.eq	LLookup_Nil
#endif
	ldr	p13, [x0]		// p13 = isa
	GetClassFromIsa_p16 p13, 1, x0	// p16 = class
LLookup_GetIsaDone:
	// returns imp
	CacheLookup LOOKUP, _objc_msgLookup, __objc_msgLookup_uncached

#if SUPPORT_TAGGED_POINTERS
LLookup_NilOrTagged:
	b.eq	LLookup_Nil	// nil check
	GetTaggedClass
	b	LLookup_GetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

LLookup_Nil:
	adr	x17, __objc_msgNil
	SignAsImp x17
	ret

	END_ENTRY _objc_msgLookup

	
	STATIC_ENTRY __objc_msgNil

	// x0 is already zero
	mov	x1, #0
	movi	d0, #0
	movi	d1, #0
	movi	d2, #0
	movi	d3, #0
	ret
	
	END_ENTRY __objc_msgNil


	ENTRY _objc_msgSendSuper
	UNWIND _objc_msgSendSuper, NoFrame

	ldp	p0, p16, [x0]		// p0 = real receiver, p16 = class
	b L_objc_msgSendSuper2_body

	END_ENTRY _objc_msgSendSuper

下面是_objc_msgSend的一些詳細呼叫決議

GetClassFromIsa_p16里是isa指標進行&ISA_MASK的運算程序

// MARK: GetClassFromIsa_p16
// 宏
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */

#if SUPPORT_INDEXED_ISA
	// 將isa的值存入p16暫存器
	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
    // 將_objc_indexed_classes所在的頁的基址 讀入x10暫存器
	adrp	x10, _objc_indexed_classes@PAGE
    // x10 = x10 + _objc_indexed_classes(page中的偏移量)(x10基址 根據 偏移量 進行 記憶體偏移)
	add	x10, x10, _objc_indexed_classes@PAGEOFF
    // 從p16的第ISA_INDEX_SHIFT位開始,提取 ISA_INDEX_BITS 位 到 p16暫存器,剩余的高位用0補充
	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
.endif
#else
	// 32-bit raw isa
	mov	p16, \src

#endif

.endmacro

CacheLookup里進行快取查找的程序,主要就是SEL & MASK得出一個索引,根據索引去buckets散串列中取對應的方法資料

// MARK: CacheLookup
.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
    // mac os或者模擬器
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
	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

// 64位真機
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    // 從x16(即isa)中取出cache 存入p11暫存器
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES

// 下面幾個是通過位運算& mask得出索引存入p12
#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
#endif
	eor	p12, p1, p1, LSR #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

// 非64位真機
#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

    // #define PTRSHIFT 3
    // LSL #(1+PTRSHIFT)-- 實際含義就是得到一個bucket占用的記憶體大小
	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

						// do {
// 從x13(即p13)中取出 bucket 分別將imp和sel 存入 p17(存盤imp) 和 p9(存盤sel)
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--

    // 比較 sel 與 p1(傳入的引數cmd)
	cmp	p9, p1				//     if (sel != _cmd) {
    // ne == not equal,請跳轉至 3f
	b.ne	3f				//         scan more
						//     } else {
    // 如果相等 即CacheHit 快取命中,直接回傳imp
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, _MagicSelRef@PAGE
	ldr	p9, [x9, _MagicSelRef@PAGEOFF]
	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

【第二步】 上述一系列操作如果沒有取到方法快取,那么就會進到__objc_msgSend_uncached

// MARK: __objc_msgSend_uncached
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p15 is the class to search
    
// 跳轉到MethodTableLookup
MethodTableLookup
TailCallFunctionPointer x17

END_ENTRY __objc_msgSend_uncached


STATIC_ENTRY __objc_msgLookup_uncached
UNWIND __objc_msgLookup_uncached, FrameWithNoSaves

// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p15 is the class to search
	
MethodTableLookup
ret

END_ENTRY __objc_msgLookup_uncached


STATIC_ENTRY _cache_getImp

GetClassFromIsa_p16 p0, 0
CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant

LGetImpMissDynamic:
mov	p0, #0
ret

LGetImpMissConstant:
mov	p0, p2
ret

END_ENTRY _cache_getImp

再進一步跳轉到MethodTableLookup,發現最侄訓呼叫到C語言函式lookUpImpOrForward

// MARK: MethodTableLookup
.macro MethodTableLookup
	
	SAVE_REGS MSGSEND

	// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
	// receiver and selector already in x0 and x1
	mov	x2, x16
	mov	x3, #3
    // 跳轉到C語言函式 lookUpImpOrForward
	bl	_lookUpImpOrForward

	// IMP in x0
	mov	x17, x0

	RESTORE_REGS MSGSEND

.endmacro

    // MARK: __objc_msgSend_uncached
	STATIC_ENTRY __objc_msgSend_uncached
	UNWIND __objc_msgSend_uncached, FrameWithNoSaves

	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band p15 is the class to search
    
    // 跳轉到MethodTableLookup
	MethodTableLookup
	TailCallFunctionPointer x17

	END_ENTRY __objc_msgSend_uncached

知識點: C的函式名稱對應到匯編中都會在其函式名之前再加上一個_,作為函式名稱

【第三步】 跳轉到objc-rumtime-new.mmlookUpImpOrForward函式來看,會到當前類的方法串列里查找,如果沒有再去父類的方法快取以及方法串列中查找,直到找到呼叫為止;如果都沒有找到,那么就會進入到方法決議的階段

NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    // 定義的訊息轉發
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();
    
    // 判斷類是否初始化,如果沒有,需要先初始化
    if (slowpath(!cls->isInitialized())) {
       
        behavior |= LOOKUP_NOCACHE;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.

    runtimeLock.lock();

    // We don't want people to be able to craft a binary blob that looks like
    // a class but really isn't one and do a CFI attack.
    //
    // To make these harder we want to make sure this is a class that was
    // either built into the binary or legitimately registered through
    // objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
    checkIsKnownClass(cls);

    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    // runtimeLock may have been dropped but is now locked again
    runtimeLock.assertLocked();
    curClass = cls;

    // The code used to lookup the class's cache again right after
    // we take the lock but for the vast majority of the cases
    // evidence shows this is a miss most of the time, hence a time loss.
    //
    // The only codepath calling into this without having performed some
    // kind of cache lookup is class_getInstanceMethod().

    // 查找類的快取
    
    // unreasonableClassCount -- 表示類的迭代的上限
    for (unsigned attempts = unreasonableClassCount();;) {
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
            // 又會在快取里找一次,如果找的就回傳
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
            // 當前類方法串列(采用二分法查找)
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) { // 如果存在,取出imp,存到快取中
                imp = meth->imp(false);
                goto done;
            }

            // 當前類 = 當前類的父類,并判斷父類是否為nil
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                
                // 未找到方法實作,方法決議器也不行,使用轉發
                imp = forward_imp;
                break;
            }
        }

        // 如果父類鏈中存在回圈,則停止
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // 父類快取
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // 如果在父類中找到了forward,則停止查找,且不快取,首先呼叫此類的方法決議器
            break;
        }
        if (fastpath(imp)) {
            // 如果在父類中,找到了此方法,將其存盤到cache中
            goto done;
        }
    }

    // 沒有找到方法實作,嘗試一次方法決議
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        // 將方法填充到快取中
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
 done_unlock:
    // 解鎖
    runtimeLock.unlock();
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

下面是lookUpImpOrForward的一些詳細呼叫決議

1.上述函式會根據傳進來的類遍歷查找,而且每次都要先去_cache_getImp中查找是否有方法快取,_cache_getImp里又會呼叫回CacheLookup進一步查找

STATIC_ENTRY _cache_getImp

	GetClassFromIsa_p16 p0, 0
	CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant

LGetImpMissDynamic:
	mov	p0, #0
	ret

LGetImpMissConstant:
	mov	p0, p2
	ret

	END_ENTRY _cache_getImp

2.在getMethodNoSuper_nolock里會找到class_rw_tmethods方法串列里進行遍歷查找

static method_t 
*getMethodNoSuper_nolock(Class cls, SEL sel) {

    runtimeLock.assertLocked();

    ASSERT(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?

    // 從類物件里拿到class_rw_t的methods
    auto const methods = cls->data()->methods();
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         mlists != end;
         ++mlists)
    {
        // <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest
        // caller of search_method_list, inlining it turns
        // getMethodNoSuper_nolock into a frame-less function and eliminates
        // any store from this codepath.
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

search_method_list_inline里會根據排序來選擇是采用二分查找還是線性查找

ALWAYS_INLINE static 
method_t *search_method_list_inline(const method_list_t *mlist, SEL sel) {

    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->isExpectedSize();
    
    // 如果排好序的就用二分查找
    if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
        return findMethodInSortedMethodList(sel, mlist);
    } else { // 線性查找,就是一個個找
        // Linear search of unsorted method list
        if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
            return m;
    }

#if DEBUG
    // sanity-check negative results
    if (mlist->isFixedUp()) {
        for (auto& meth : *mlist) {
            if (meth.name() == sel) {
                _objc_fatal("linear search worked when binary search did not");
            }
        }
    }
#endif

    return nil;
}

findMethodInSortedMethodList中進行二分查找

template<class getNameFunc>
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName) {

    ASSERT(list);

    auto first = list->begin();
    auto base = first;
    decltype(first) probe;

    uintptr_t keyValue = https://www.cnblogs.com/funkyRay/p/(uintptr_t)key;
    uint32_t count;
    
    for (count = list->count; count != 0; count >>= 1) {
        probe = base + (count >> 1);
        
        uintptr_t probeValue = (uintptr_t)getName(probe);
        
        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
                probe--;
            }
            return &*probe;
        }
        
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

findMethodInUnsortedMethodList中進行線性查找,也就是一個個往下遍歷查找

template<class getNameFunc>
ALWAYS_INLINE static method_t *
findMethodInUnsortedMethodList(SEL sel, const method_list_t *list, const getNameFunc &getName)
{
    for (auto& meth : *list) {
        if (getName(meth) == sel) return &meth;
    }
    return nil;
}

ALWAYS_INLINE static method_t *
findMethodInUnsortedMethodList(SEL key, const method_list_t *list)
{
    if (list->isSmallList()) {
        if (CONFIG_SHARED_CACHE_RELATIVE_DIRECT_SELECTORS && objc::inSharedCache((uintptr_t)list)) {
            return findMethodInUnsortedMethodList(key, list, [](method_t &m) { return m.getSmallNameAsSEL(); });
        } else {
            return findMethodInUnsortedMethodList(key, list, [](method_t &m) { return m.getSmallNameAsSELRef(); });
        }
    } else {
        return findMethodInUnsortedMethodList(key, list, [](method_t &m) { return m.big().name; });
    }
}

3.在log_and_fill_cache里將查找到的方法插入到快取中,最后呼叫到objc-cache.mm中的cache_t::insert函式,該函式的詳細決議可以查看文章一開始的cache_t部分內容

static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer) {

#if SUPPORT_MESSAGE_LOGGING
    if (slowpath(objcMsgLogEnabled && implementer)) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cls->cache.insert(sel, imp, receiver);
}
總結

整個訊息發送的流程可以用下圖來概述

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/273578.html

標籤:iOS

上一篇:iOS底層原理(五)Runtime(下)

下一篇:iOS-宮格拼圖

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more