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

相关文章
相关标签/搜索