OC源碼分析之方法的緩存原理

前言

想要成爲一名iOS開發高手,免不了閱讀源碼。如下是筆者在OC源碼探索中梳理的一個小系列——類與對象篇,歡迎你們閱讀指正,同時也但願對你們有所幫助。html

  1. OC源碼分析之對象的建立
  2. OC源碼分析之isa
  3. OC源碼分析之類的結構解讀
  4. OC源碼分析之方法的緩存原理
  5. 未完待續...

本文是針對 方法緩存——cache_t 的分析(且源碼版本是 objc4-756.2),下面進入正文。git

1. cache_t源碼分析

當你的OC項目編譯完成後,類的實例方法(方法編號SEL 和 函數地址IMP)就保存在類的方法列表中。咱們知道 OC 爲了實現其動態性,將 方法的調用包裝成了 SEL 尋找 IMP 的過程。試想一下,若是每次調用方法,都要去類的方法列表(甚至父類、根類的方法列表)中查詢其函數地址,勢必會對性能形成極大的損耗。爲了解決這一問題,OC 採用了方法緩存的機制來提升調用效率,也就是cache_t,其做用就是緩存已調用的方法。當調用方法時,objc_msgSend會先去緩存中查找,若是找到就執行該方法;若是不在緩存中,則去類的方法列表(包括父類、根類的方法列表)查找,找到後會將方法的SELIMP緩存到cache_t中,以便下次調用時可以快速執行。github

1.1 cache_t結構

首先看一下cache_t的結構數組

struct cache_t {
    struct bucket_t *_buckets;  // 緩存數組,即哈希桶
    mask_t _mask;               // 緩存數組的容量臨界值
    mask_t _occupied;           // 緩存數組中已緩存方法數量

    ... // 一些函數
};

#if __LP64__
typedef uint32_t mask_t;
#else
typedef uint16_t mask_t;
#endif

struct bucket_t {
private:
#if __arm64__
    uintptr_t _imp;
    SEL _sel;
#else
    SEL _sel;
    uintptr_t _imp;
#endif
    ... // 一些方法
};
複製代碼

從上面源碼不難看出,在64位CPU架構下,cache_t長度是16字節。單從結構來看,方法是緩存在bucket_t(又稱哈希桶)中,接下來用個例子驗證一下cache_t是否緩存了已調用的方法。緩存

1.2 方法緩存的驗證

  1. 建立一個簡單的Person類,代碼以下
@interface Person : NSObject

- (void)methodFirst;
- (void)methodSecond;
- (void)methodThird;

@end

@implementation Person

- (void)methodFirst {
    NSLog(@"%s", __FUNCTION__);
}

- (void)methodSecond {
    NSLog(@"%s", __FUNCTION__);
}

- (void)methodThird {
    NSLog(@"%s", __FUNCTION__);
}

@end
複製代碼
  1. 方法調用前的cache_t

在方法調用前打個斷點,看看cache_t的緩存狀況安全

說明:bash

  • objc_class結構很容易推導得出,0x1000011d8cache_t首地址。(對類的結構感興趣的同窗請戳 OC源碼分析之類的結構解讀
  • 因爲尚未任何方法調用,因此_mask_occupied都是0
  1. 方法調用後的cache_t

執行allocinit這兩個方法後,cache_t變化以下多線程

從上圖可知,調用init後,_mask的值是3,_occupied則是1。_buckets指針的值(數組首地址)發生了變化(從0x1003db250變成0x101700090),同時緩存了init方法的SELIMP架構

思考: 1. alloc 方法調用後,緩存在哪裏? 2. 爲何 init 方法不在 _buckets 第一個位置? app

繼續執行methodFirst,再看cache_t

此時,_mask的值是3(沒發生變化),_occupied則變成了2,_buckets指針地址沒變,增長緩存了methodFirst方法的SELIMP

接着是執行methodSecond,且看

顯然,_occupied變成了3,而_buckets指針地址不改變,同時新增methodSecond的方法緩存。

最後執行methodThird後,再看cache_t變化

此次的結果就徹底不一樣了。_mask的值變成7,_occupied則從新變成了1,而_buckets不只首地址變了,以前緩存的initmethodFirstmethodSecond方法也沒了,僅存在的只有新增的methodThird方法。看來,cache_t並不是是如咱們所願的那樣——調用一個方法就緩存一個方法。

思考:以前緩存的方法(init、methodFirst 和 methodSecond)哪去了?

1.3 cache_t小結

讓咱們梳理一下上面的例子。在依次執行Person的實例方法initmethodFirstmethodSecondmethodThird後,cache_t變化以下

調用的方法 _buckets _mask _occupied
未調用方法 0 0
init init 3 1
init、methodFirst init、methodFirst 3 2
init、methodFirst、methodSecond init、methodFirst、methodSecond 3 3
init、methodFirst、methodSecond、methodThird methodThird 7 1

可見,cache_t的確能實時緩存已調用的方法

上面的驗證過程也能夠幫助咱們理解cache_t三個成員變量的意義。直接從單詞含義上解析,bucket可譯爲桶(即哈希桶),用於裝方法;occupied可譯爲已佔有,表示已緩存的方法數量;mask可譯爲面具、掩飾物,乍看無頭緒,可是注意到cache_t中有獲取容量的函數(capacity),其源碼以下

struct cache_t {
    ...
    mask_t mask();
    mask_t capacity();
    ...
}

mask_t cache_t::mask() 
{
    return _mask; 
}

mask_t cache_t::capacity() 
{
    return mask() ? mask()+1 : 0; 
}
複製代碼

由此能夠得出,若是_mask是0,說明未調用實例方法,即桶的容量爲0;當_mask不等於0的時候,意味着已經調用過實例方法,此時桶的容量爲_mask + 1。故,_mask從側面反映了桶的容量。

2. cache_t的方法緩存原理

接下來,筆者將從方法的調用過程開始分析cache_t的方法緩存原理。

2.1 cache_fill

OC方法的本質是 消息發送(即objc_msgSend),底層是經過方法的 SEL 查找 IMP。調用方法時,objc_msgSend會去cache_t中查詢方法的函數實現(這部分是由彙編代碼實現的,很是高效),在緩存中找的過程暫且不表;當緩存中沒有的時候,則去類的方法列表中查找,直至找到後,再調用cache_fill,目的是爲了將方法緩存到cache_t中,其源碼以下

void cache_fill(Class cls, SEL sel, IMP imp, id receiver) {
#if !DEBUG_TASK_THREADS
    mutex_locker_t lock(cacheUpdateLock);
    cache_fill_nolock(cls, sel, imp, receiver);
#else
    _collecting_in_critical();
    return;
#endif
}
複製代碼

objc_msgSend的具體流程筆者將另起一文分析,這裏不做贅述。

2.2 cache_fill_nolock

cache_fill又會來到cache_fill_nolock,這個函數的做用是將方法的SELIMP寫入_buckets,同時更新_mask_occupied

其源碼以及詳細分析以下:

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver) {
    cacheUpdateLock.assertLocked();

    // 若是類未初始化
    if (!cls->isInitialized()) return;

    // 在獲取cacheUpdateLock以前,確保其餘線程沒有將該方法寫入緩存
    if (cache_getImp(cls, sel)) return;

    // 獲取 cls 的 cache_t指針
    cache_t *cache = getCache(cls);

    // newOccupied爲新的方法緩存數,等於 當前方法緩存數+1
    mask_t newOccupied = cache->occupied() + 1;
    // 獲取當前cache_t的總容量,即 mask+1
    mask_t capacity = cache->capacity();
    if (cache->isConstantEmptyCache()) {
        // 當第一次調用類的實例方法時(如本文的【1.2】例中的`init`)
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // 新的方法緩存數 不大於 總容量的3/4,按原樣使用,無需擴容
    }
    else {
        // 新的方法緩存數 大於 總容量的3/4,須要擴容
        cache->expand();
    }

    // 根據sel獲取bucket,此bucket的sel通常爲0(說明這個位置還沒緩存方法),
    // 也可能與實參sel相等(hash衝突,可能性很低)
    bucket_t *bucket = cache->find(sel, receiver);
    // 當且僅當bucket的sel爲0時,執行_occupied++
    if (bucket->sel() == 0) cache->incrementOccupied();
    // 更新bucket的sel和imp
    bucket->set<Atomic>(sel, imp);
}

// INIT_CACHE_SIZE 即爲4
enum {
    INIT_CACHE_SIZE_LOG2 = 2,
    INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2)
};
複製代碼

從上面的源碼不難看出,cache_fill_nolock主要是cache_t緩存方法的調度中心,在這裏會

  1. 決定執行_buckets的哪種緩存策略(初始化後緩存、直接緩存、擴容後緩存,三者取一);
  2. 而後經過方法的sel找到一個bucket,並更新這個bucketselimp。(若是這個bucketsel爲0,說明是個空桶,正好能夠緩存方法,因而執行_occupied++)。

思考:爲何擴容臨界點是 3/4?

2.3 reallocate

在下面這兩種狀況下會執行reallocate

  • 一是第一次初始化_buckets的時候
  • 另外一種則是_buckets擴容的時候

咱們來看一下reallocate作了哪些事情

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
    // 當且僅當`_buckets`中有緩存方法時,feeOld爲true
    bool freeOld = canBeFreed();

    // 獲取當前buckets指針,即_buckets
    bucket_t *oldBuckets = buckets();
    // 開闢新的buckets指針
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    // Cache's old contents are not propagated. 
    // This is thought to save cache memory at the cost of extra cache fills.
    // fixme re-measure this

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

    // 將新buckets、新mask(newCapacity-1)分別賦值跟當前的 _buckets 和 _mask
    setBucketsAndMask(newBuckets, newCapacity - 1);
    
    if (freeOld) {
        // 釋放舊的buckets內存空間
        cache_collect_free(oldBuckets, oldCapacity);
        cache_collect(false);
    }
}
複製代碼

reallocate完美解釋了在例【1.2】中的幾個狀況:

  • init執行完後,_buckets指針地址變了,_mask變成了3;
  • methodThird執行完後,_buckets不只指針地址變了,同時以前緩存的initmethodFirstmethodSecond方法也都不在了

注意,_occupied的變化是在回到cache_fill_nolock後發生的。

思考:擴容後,爲何不直接把以前緩存的方法加入新的buckets中?

2.4 expand

cache_fill_nolock源碼來看,當新的方法緩存數(_occupied+1)大於總容量(_mask+1)時,會對_buckets進行擴容,也就是執行expand函數,其源碼以下

void cache_t::expand()
{
    cacheUpdateLock.assertLocked();
    
    // 獲取當前總容量,即_mask+1
    uint32_t oldCapacity = capacity();
    // 新的容量 = 舊容量 * 2
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;

    if ((uint32_t)(mask_t)newCapacity != newCapacity) {
        // mask overflow - can't grow further
        // fixme this wastes one bit of mask
        newCapacity = oldCapacity;
    }
    
    reallocate(oldCapacity, newCapacity);
}
複製代碼

這個函數很是簡單,僅僅是計算好新的容量後,就去調用reallocate函數。須要注意的是:

  • 在不超過uint32_t大小(4字節)時,每次擴容爲原來的2倍
  • 若是超過了uint32_t,則從新申請跟原來同樣大小的buckets

2.5 find

在執行完相應的buckets策略後,接下來就須要找到合適的位置(bucket),以存儲 方法的SELIMPfind具體作的事情就是根據方法的SEL,返回一個符合要求的bucket,一樣上源碼

bucket_t * cache_t::find(SEL s, id receiver)
{
    assert(s != 0);
    // 獲取當前buckets,即_buckets
    bucket_t *b = buckets();
    // 獲取當前mask,即_mask
    mask_t m = mask();
    // 由 sel & mask 得出起始索引值
    mask_t begin = cache_hash(s, m);
    mask_t i = begin;
    do {
        // sel爲0:說明 i 這個位置還沒有緩存方法;
        // sel等於s:命中緩存,說明 i 這個位置已緩存方法,多是hash衝突
        if (b[i].sel() == 0  ||  b[i].sel() == s) {
            return &b[i];
        }
    } while ((i = cache_next(i, m)) != begin);

    // hack
    // 找不到多餘的哈希桶(出錯的處理,打印問題)。通常不會走到這裏!
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)s, cls);
}

static inline mask_t cache_hash(SEL sel, mask_t mask) {
    return (mask_t)(uintptr_t)sel & mask;
}

#if __arm__ || __x86_64__ || __i386__
// objc_msgSend has few registers available.
// Cache scan increments and wraps at special end-marking bucket.
#define CACHE_END_MARKER 1
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}

#elif __arm64__
// objc_msgSend has lots of registers available.
// Cache scan decrements. No end marker needed.
#define CACHE_END_MARKER 0
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}

#else
#error unknown architecture
#endif
複製代碼

從源碼能夠發現,findbucket的方式用到了hash的思想:以_buckets做爲哈希桶,以cache_hash做爲哈希函數,進行哈希運算後得出索引值index(本質是xx & mask,因此index最大值就是_mask的值)。因爲索引值是經過哈希運算得出的,其結果天然是無序的,這也是爲何上例中init方法不在_buckets第一個位置的緣由。

3. 多線程對方法緩存的影響

既然哈希桶的數量是在運行時動態增長的,那麼在多線程環境下調用方法時,對方法的緩存有沒有什麼影響呢?且看下面的分析。

3.1 多線程同時讀取緩存

在整個objc_msgSend函數中,爲了達到最佳的性能,對方法緩存的讀取操做是沒有添加任何鎖的。而多個線程同時調用已緩存的方法,並不會引起_buckets_mask的變化,所以多個線程同時讀取方法緩存的操做是不會有安全隱患的

3.2 多線程同時寫緩存

從源碼咱們知道在桶數量擴容和寫桶數據以前,系統使用了一個全局的互斥鎖(cacheUpdateLock.assertLocked())來保證寫入的同步處理,而且在鎖住的範圍內部還作了一次查緩存的操做(if (cache_getImp(cls, sel)) return;),這樣就 保證了哪怕多個線程同時寫同一個方法的緩存也只會產生寫一次的效果,即多線程同時寫緩存的操做也不會有安全隱患

3.3 多線程同時讀寫緩存

這個狀況就比較複雜了,咱們先看一下objc_msgSend讀緩存的代碼(以 arm64架構彙編 爲例)

.macro CacheLookup
	// x1 = SEL, x16 = isa
	ldp	x10, x11, [x16, #CACHE] // x10 = buckets, x11 = occupied|mask
	and	w12, w1, w11		// x12 = _cmd & mask
	add	x12, x10, x12, LSL #4 // x12 = buckets + ((_cmd & mask)<<4)

	ldp	x9, x17, [x12]		// {x9, x17} = *bucket
1:	cmp	x9, x1			// if (bucket->sel != _cmd)
	b.ne	2f			//     scan more
	CacheHit $0			// call or return imp
	
2:	// not hit: x12 = not-hit bucket
	CheckMiss $0			// miss if bucket->sel == 0
	cmp	x12, x10		// wrap if bucket == buckets
	b.eq	3f
	ldp	x9, x17, [x12, #-16]! // {x9, x17} = *--bucket
	b	1b			// loop

3:	// wrap: x12 = first bucket, w11 = mask
	add	x12, x12, w11, UXTW #4 // x12 = buckets+(mask<<4)

	// Clone scanning loop to miss instead of hang when cache is corrupt.
	// The slow path may detect any corruption and halt later.

	ldp	x9, x17, [x12]		// {x9, x17} = *bucket
1:	cmp	x9, x1			// if (bucket->sel != _cmd)
	b.ne	2f			//     scan more
	CacheHit $0			// call or return imp
	
2:	// not hit: x12 = not-hit bucket
	CheckMiss $0			// miss if bucket->sel == 0
	cmp	x12, x10		// wrap if bucket == buckets
	b.eq	3f
	ldp	x9, x17, [x12, #-16]! // {x9, x17} = *--bucket
	b	1b			// loop

3:	// double wrap
	JumpMiss $0
	
.endmacro
複製代碼

其中,ldp指令的做用是將數據從內存讀取出來存到寄存器,第一個ldp代碼會 cache_t中的_buckets_occupied | _mask整個結構體成員分別讀取到x10x11兩個寄存器中,而且CacheLookup的後續代碼沒有再次讀取cache_t的成員數據,而是一直使用x10x11中的值進行哈希查找。因爲CPU能保證單條指令執行的原子性,因此 只要保證ldp x10, x11, [x16, #CACHE]這段代碼讀取到的_buckets_mask是互相匹配的(即要麼同時是擴容前的數據,要麼同時是擴容後的數據),那麼多個線程同時讀寫方法緩存也是沒有安全隱患的

3.3.1 編譯內存屏障

這裏有個疑問,即系統是如何確保_buckets_mask的這種一致性的呢?讓咱們看一下這兩個變量的寫入源碼

void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
    // objc_msgSend uses mask and buckets with no locks.
    // It is safe for objc_msgSend to see new buckets but old mask.
    // (It will get a cache miss but not overrun the buckets' bounds).
    // It is unsafe for objc_msgSend to see old buckets and new mask.
    // Therefore we write new buckets, wait a lot, then write new mask.
    // objc_msgSend reads mask first, then buckets.

    // ensure other threads see buckets contents before buckets pointer
    mega_barrier();

    _buckets = newBuckets;
    
    // ensure other threads see new buckets before new mask
    mega_barrier();
    
    _mask = newMask;
    _occupied = 0;
}
複製代碼

這段C++代碼先修改_buckets,而後再更新_mask的值,爲了確保這個順序不被編譯器優化,這裏使用了mega_baerrier()來實現 編譯內存屏障(Compiler Memory Barrier)

若是不設置 編譯內存屏障 的話,編譯器有可能會優化代碼先賦值_mask,而後纔是賦值_buckets,二者的賦值之間,若是另外一個線程執行ldp x10, x11, [x16, #0x10]指令,獲得的就是舊_buckets新_mask,進而出現內存數組越界引起程序崩潰。

而加入了編譯內存屏障後,就算獲得的是新_buckets舊_mask,也不會致使程序崩潰。

編譯內存屏障僅僅是確保_buckets的賦值會優先於_mask的賦值,也就是說,在任何場景下當指令ldp x10, x11, [x16, #CACHE]執行後,獲得的_buckets數組的長度必定是大於或等於_mask+1的,如此就保證了不會出現內存數組越界致使的程序崩潰。可見,藉助編譯內存屏障的技巧在必定的程度上能夠實現無鎖讀寫技術。

內存屏障感興趣的同窗可戳 理解 Memory barrier(內存屏障)

3.3.2 內存垃圾回收

咱們知道,在多線程讀寫方法緩存時,寫線程可能會擴容_buckets(開闢新的_buckets內存,同時銷燬舊的_buckets),此時,若是其餘線程讀取到的_buckets是舊的內存,就有可能會發生讀內存異常而系統崩潰。爲了解決這個問題,OC使用了兩個全局數組objc_entryPointsobjc_exitPoints,分別保存全部會訪問到cache的函數的起始地址、結束地址

extern "C" uintptr_t objc_entryPoints[];
extern "C" uintptr_t objc_exitPoints[];
複製代碼

下面列出這些函數(一樣以 arm64架構彙編 爲例)

.private_extern _objc_entryPoints
_objc_entryPoints:
	.quad   _cache_getImp
	.quad   _objc_msgSend
	.quad   _objc_msgSendSuper
	.quad   _objc_msgSendSuper2
	.quad   _objc_msgLookup
	.quad   _objc_msgLookupSuper2
	.quad   0

.private_extern _objc_exitPoints
_objc_exitPoints:
	.quad   LExit_cache_getImp
	.quad   LExit_objc_msgSend
	.quad   LExit_objc_msgSendSuper
	.quad   LExit_objc_msgSendSuper2
	.quad   LExit_objc_msgLookup
	.quad   LExit_objc_msgLookupSuper2
	.quad   0
複製代碼

當線程擴容哈希桶時,會先把舊的桶內存保存在一個全局的垃圾回收數組變量garbage_refs中,而後再遍歷當前進程(在iOS中,一個進程就是一個應用程序)中的全部線程,查看是否有線程正在執行objc_entryPoints列表中的函數(原理是PC寄存器中的值是否在objc_entryPointsobjc_exitPoints這個範圍內),若是沒有則說明沒有任何線程訪問cache,能夠放心地對garbage_refs中的全部待銷燬的哈希桶內存塊執行真正的銷燬操做;若是有則說明有線程訪問cache,此次就不作處理,下次再檢查並在適當的時候進行銷燬。

以上,OC 2.0runtime巧妙的利用了ldp彙編指令、編譯內存屏障技術、內存垃圾回收技術等多種手段來解決多線程讀寫的無鎖處理方案,既保證了安全,又提高了系統的性能。

在這裏,特別感謝 歐陽大哥!他的 深刻解構objc_msgSend函數的實現 這篇博文會幫助你進一步瞭解Runtime的實現,其在多線程讀寫方法緩存方面也讓筆者受益不淺,強烈推薦你們一讀!

4. 問題討論

來到這裏,相信你們對cache_t緩存方法的原理已經有了必定的理解。如今請看下面的幾個問題:

4.1 類方法的緩存位置

QPerson類調用alloc方法後,緩存在哪裏?

A:緩存在 Person元類 的 cache_t 中。證實以下圖

4.2 _mask的做用

Q:請說明cache_t_mask的做用

A_mask從側面反映了cache_t中哈希桶的數量(哈希桶的數量 = _mask + 1),保證了查找哈希桶時不會出現越界的狀況。

題解:從上面的源碼分析,咱們知道cache_t在任何一次緩存方法的時候,哈希桶的數量必定是 >=4且能被 4整除的_mask則等於哈希桶的數量-1,也就是說,緩存方法的時候,_mask的二進制位上全都是1。當循環查詢哈希桶的時候,索引值是由xx & _mask運算得出的,所以索引值是小於哈希桶的數量的(index <= _mask,故index < capacity),也就不會出現越界的狀況。

4.3 關於擴容臨界點3/4的討論

Q:爲何擴容臨界點是3/4?

A:通常設定臨界點就不得不權衡 空間利用率時間利用率 。在 3/4 這個臨界點的時候,空間利用率比較高,同時又避免了至關多的哈希衝突,時間利用率也比較高。

題解:擴容臨界點直接影響循環查找哈希桶的效率。設想兩個極端狀況:

當臨界點是1的時候,也就是說當所有的哈希桶都緩存有方法時,纔會擴容。這雖然讓開闢出來的內存空間的利用率達到100%,可是會形成大量的哈希衝突,加重了查找索引的時間成本,致使時間利用率低下,這與高速緩存的目的相悖;

當臨界點是0.5的時候,意味着哈希桶的佔用量達到總數一半的時候,就會擴容。這雖然極大避免了哈希衝突,時間利用率很是高,卻浪費了一半的空間,使得空間利用率低下。這種以空間換取時間的作法一樣不可取;

兩相權衡下,當擴容臨界點是3/4的時候,空間利用率 和 時間利用率 都相對比較高

4.4 緩存循環查找的死循環狀況

Q:緩存循環查找哈希桶是否會出現死循環的狀況?

A:不會出現。

題解:當哈希桶的利用率達到3/4的時候,下次緩存的時候就會進行擴容,即空桶的數量最少也會有總數的1/4,所以循環查詢索引的時候,必定會出現命中緩存或者空桶的狀況,從而結束循環。

5. 總結

經過以上例子的驗證、源碼的分析以及問題的討論,如今總結一下cache_t的幾個結論:

  1. cache_t能緩存調用過的方法。
  2. cache_t的三個成員變量中,
    • _buckets的類型是struct bucket_t *,也就是指針數組,它表示一系列的哈希桶(已調用的方法的SELIMP就緩存在哈希桶中),一個桶能夠緩存一個方法。
    • _mask的類型是mask_tmask_t64位架構下就是uint32_t,長度爲4個字節),它的值等於哈希桶的總數-1(capacity - 1),側面反映了哈希桶的總數。
    • _occupied的類型也是mask_t,它表明的是當前_buckets已緩存的方法數。
  3. 當緩存的方法數到達臨界點(桶總數的3/4)時,下次再緩存新的方法時,首先會丟棄舊的桶,同時開闢新的內存,也就是擴容(擴容後都是全新的桶,之後每一個方法都要從新緩存的),而後再把新的方法緩存下來,此時_occupied爲1。
  4. 當多個線程同時調用一個方法時,可分如下幾種狀況:
    • 多線程讀緩存:讀緩存由彙編實現,無鎖且高效,因爲並無改變_buckets_mask,因此並沒有安全隱患。
    • 多線程寫緩存:OC用了個全局的互斥鎖(cacheUpdateLock.assertLocked())來保證不會出現寫兩次緩存的狀況。
    • 多線程讀寫緩存:OC使用了ldp彙編指令、編譯內存屏障技術、內存垃圾回收技術等多種手段來解決多線程讀寫的無鎖處理方案,既保證了安全,又提高了系統的性能。

6. 參考資料

PS

相關文章
相關標籤/搜索