iOS缓存的总结
内存的分类
在iOS中缓存主要分为两种,一种是内存缓存一种是磁盘缓存。内存缓存提供容量小但是高速的存取功能,磁盘缓存提供容量大但是低速的存取功能。在使用的时候一般是将最近的数据(如一天)存储在内存缓存中;将超出最近时间而又在合适时间内的数据(如超过一天在一周内)从内存缓存中清除,将其存储在磁盘缓存中;将超出最大时间(如超过一周)的数据从磁盘中销毁。
内存缓存
NSCache
苹果提供了NSCache作为一个简单的内存缓存,它有着和NSDictionary相似的API,但是它是线程安全的,并不retain key。它的底层直接调用了 libcache.dylib,通过pthead_mutex(互斥锁)完成了线程安全。但是由于它的性能和key值的相似度有关,如果有大量相似的key的话,NSCache的存取性能下降地厉害。——ibireme
TMMemoryCache
TMMemoryCache 是TMMemory的内存缓存实现,它提供了很多NSCache没有的缓存功能,如数量限制,总容量限制,存活时间限制,内存警告以及应用退到后台清空缓存。它在设计的时候主要采用了线程安全,他将所有的读写操作放在了一个concurrent queue中,使用dispatch_barier_async来保证任务可以顺序执行,但是使用了大量的异步Block实现了存取功能,造成了很大的性能和死锁问题。
dispatch_barier_async : 它的异步体现在会把后面的任务也先添加在队列中,然后在执行的时候体现出barier的特性,必须先它加入队列中的任务执行完毕之后,它的任务才执行,必须它的任务执行完之后,后它加入队列中的任务才可以开始执行。
dispatch_barrier_sync: 它的同步体现在,它不会现将后面的任务加入队列,即不会执行后续的代码,直到先它加入队列的任务执行完毕,以及它执行完毕后才开始执行后续代码。
- (void)setObject:(id)object forKey:(NSString *)key withCost:(NSUInteger)cost block:(TMMemoryCacheObjectBlock)block
{
NSDate *now = [[NSDate alloc] init];
if (!key || !object)
return;
__weak TMMemoryCache *weakSelf = self;
// 1. 使用 dispatch_barrier_async实现线程安全
dispatch_barrier_async(_queue, ^{
TMMemoryCache *strongSelf = weakSelf;
if (!strongSelf)
return;
// 2. 执行willAddObjectBlock
if (strongSelf->_willAddObjectBlock)
strongSelf->_willAddObjectBlock(strongSelf, key, object);
// 3.存储key对应的时间,数据,大小
[strongSelf->_dictionary setObject:object forKey:key];
[strongSelf->_dates setObject:now forKey:key];
[strongSelf->_costs setObject:@(cost) forKey:key];
_totalCost += cost;
// 4.执行添加完成后的Block
if (strongSelf->_didAddObjectBlock)
strongSelf->_didAddObjectBlock(strongSelf, key, object);
// 5.根据时间排序来清空指定缓存大小的内存
if (strongSelf->_costLimit > 0)
[strongSelf trimToCostByDate:strongSelf->_costLimit block:nil];
//6.异步回调
if (block) {
__weak TMMemoryCache *weakSelf = strongSelf;
dispatch_async(strongSelf->_queue, ^{
TMMemoryCache *strongSelf = weakSelf;
if (strongSelf)
block(strongSelf, key, object);
});
}
});
}
以上通过GCD中的dispatch_barrier_async来实现了线程安全。如果需要实现同同步存储操作,那该怎么办呢?作者使用了dispatch_semaphore_t的方式来实现了同步存储操作:
- (void)setObject:(id)object forKey:(NSString *)key withCost:(NSUInteger)cost
{
if (!object || !key)
return;
// 1. 创建信号量(信号量为0)
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[self setObject:object forKey:key withCost:cost block:^(TMMemoryCache *cache, NSString *key, id object) {
// 3.发出信号(信号量-1)
dispatch_semaphore_signal(semaphore);
}];
// 2.设置等待时间(信号量+1)
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
注意使用的时候:调用 dispatch_semaphore_wait 方法会使信号量的值-1, 表示增加一个线程等待处理共用资源, 当 dispatch_semaphore_signal 时会使信号量的值+1, 表示该线程不再占用共用资源,不占据信号量了。信号量使用的目的是等待异步存储产生结果之后,才执行后续的操作。即dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER),后续的操作需要等待dispatch_semaphore_signal(semaphore)执行使信号量+1才执行。
PINMemoryCache
PINMemoryCache它的功能和接口和TMMemoryCache类似,但是它修复了性能和死锁的问题,采用的是pthread_mutex_lock来保证线程的安全,去掉了dispatch_barier_async,避免了线程切换产生的巨大的开销,避免了可能的死锁。
- (void)setObject:(id)object forKey:(NSString *)key withCost:(NSUInteger)cost ageLimit:(NSTimeInterval)ageLimit
{
······
// 1.加锁
[self lock];
// 2. 根据key存储数据,日期,大小
NSNumber* oldCost = _costs[key];
if (oldCost)
_totalCost -= [oldCost unsignedIntegerValue];
NSDate *now = [NSDate date];
_dictionary[key] = object;
_createdDates[key] = now;
_accessDates[key] = now;
_costs[key] = @(cost);
if (ageLimit > 0.0) {
_ageLimits[key] = @(ageLimit);
} else {
[_ageLimits removeObjectForKey:key];
}
_totalCost += cost;
// 3. 解锁
[self unlock];
······
}
关键就是接下来如何实现lock方法来满足线程安全呢?
- (void)lock
{
__unused int result = pthread_mutex_lock(&_mutex);
NSAssert(result == 0, @"Failed to lock PINMemoryCache %@. Code: %d", self, result);
}
- (void)unlock
{
__unused int result = pthread_mutex_unlock(&_mutex);
NSAssert(result == 0, @"Failed to unlock PINMemoryCache %@. Code: %d", self, result);
}
在这里它使用的是pthread_mutex来实现的线程安全,这个锁是互斥锁(锁的类型我们后面简要来做一下总结)。
####YYMemoryCache
相对于 PINMemoryCache 来说,去掉了异步访问的接口,尽量优化了同步访问的性能,用 pthread_mutex保证线程安全(注意 之前使用的是OSSpinLock但是会有bug,所以后来更换为了pthread_mutex)。而且在缓存内部还使用了双向链表和 NSDictionary 实现了 LRU 淘汰算法。这里代码就不贴了,大家有兴趣自己去研究研究吧。
不过这里可以提一提什么是LRU算法(least recently used 最近最少使用): 如果一个数据在最近一段时间内没有被访问,那么在以后他被访问的可能性也会比较小。 (也就是说在限定空间已满的情况下,应该把最久没有被访问到的数据淘汰!)
如何实现呢:
在需要插入新数据的时候,如果数据在链表中存在,那么就将该节点移动到链表的头部,如果该数据在链表中不存在,那么新建节点,将节点插入到链表的头部,如果缓存满了,就要将链表中最后一个节点删除;访问数据的时候,如果数据存在,那么就将节点移动到链表的头部,这样的话链表最尾部的数据就是最久未被访问的数据了。

###磁盘缓存 根据YY大神的解析,磁盘缓存的实现技术大致分为三类:基于文件读写;基于mmap文件内存映射;基于数据库。
TMDiskCache, PINDiskCache, SDWebImage 等缓存,都是基于文件系统的,即一个 Value 对应一个文件,通过文件读写来缓存数据。他们的实现都比较简单,性能也都相近,缺点也是同样的:不方便扩展、没有元数据、难以实现较好的淘汰算法、数据统计缓慢。
FastImageCache 采用的是 mmap 将文件映射到内存。用过 MongoDB 的人应该很熟悉 mmap 的缺陷:热数据的文件不要超过物理内存大小,不然 mmap 会导致内存交换严重降低性能;另外内存中的数据是定时 flush 到文件的,如果数据还未同步时程序挂掉,就会导致数据错误。抛开这些缺陷来说,mmap 性能非常高。
NSURLCache、FBDiskCache 都是基于 SQLite 数据库的。基于数据库的缓存可以很好的支持元数据、扩展方便、数据统计速度快,也很容易实现 LRU 或其他淘汰算法,唯一不确定的就是数据库读写的性能,当单条数据小于 20K 时,数据越小 SQLite 读取性能越高;单条数据大于 20K 时,直接写为文件速度会更快一些。
所以基于 SQLite 的这种表现,磁盘缓存最好是把 SQLite 和文件存储结合起来:key-value 元数据保存在 SQLite 中,而 value 数据则根据大小不同选择 SQLite 或文件存储。
// YYCache 中的磁盘缓存
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData {
if (key.length == 0 || value.length == 0) return NO;
if (_type == YYKVStorageTypeFile && filename.length == 0) {
return NO;
}
// 1.文件名不为空
if (filename.length) {
// 2.将数据存入文件中,存入则返回
if (![self _fileWriteWithName:filename data:value]) {
return NO;
}
// 3.将数据存入数据库中,存入则返回
if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
[self _fileDeleteWithName:filename];
return NO;
}
return YES;
} else {
if (_type != YYKVStorageTypeSQLite) {
NSString *filename = [self _dbGetFilenameWithKey:key];
if (filename) {
[self _fileDeleteWithName:filename];
}
}
// 4. 将数据存入到数据库中
return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
}
}
什么是基于mmap的文件内存映射
将一个文件或者其他对象映射到进程的地址空间,实现磁盘内存地址和进程虚拟地址空间中的一段虚拟的地址的对应关系。 进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。

锁的分类
首先来看一组图片,关于锁的速度比较,然后我再简单介绍介绍不同类型的锁:

信号量
什么是信号量?
在进入一段代码之前,要先获取一个信号量,在结束代码之前,释放该信号量,只要信号量满足一定的条件,那么其他想执行此代码线程就可以执行该代码了。
DispatchSemaphore
在swift中信号量的实现方式就是DispatchSemaphore,设置信号量的值为多少,就意味着最多有多少线程可以同时处理执行代码,所以如果将信号量初始化为1,那就意味着同时只有一个线程可以使用该资源.
注意:wait()首先检查信号量的大小,如果为1,那么执行下方的代码,并且执行信号量-1操作, 如果为0,那么不执行后续代码,等待信号量变为1才开始执行后续代码。singal()使信号量+1
let semaphore = DispatchSemaphore.init(value: 1)
DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
semaphore.wait()
print("线程一开始")
sleep(2)
print("线程一结束")
semaphore.signal()
}
DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
print("线程二开始")
sleep(1)
semaphore.wait()
print("线程二结束")
semaphore.signal()
}
互斥锁
什么是互斥锁?
当一个线程对某资源在进行访问时,锁定此资源,其他对该资源产生访问的线程会被挂起,直到该线程解锁了互斥量。iOS中的互斥锁:NSLock,pthread_mutex,@synchronized
NSLock
let lock = NSLock.init()
func 🐶() {
lock.lock()
for index in 0..<3 {
print("\(index) I am a dog")
}
lock.unlock()
}
DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
print("线程一开始")
🐶()
print("线程一结束")
}
DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
print("线程二开始")
🐶()
print("线程二结束")
}
输出:
线程二开始
线程一开始
0 I am a dog
1 I am a dog
2 I am a dog
线程二结束
0 I am a dog
1 I am a dog
2 I am a dog
线程一结束
pthread_mutex
**注意:**在Swift中pthread_mutex的初始化和正常的swift类型不一样
var mutex = pthread_mutex_t()
pthread_mutex_init(&mutex, nil)
var mutex = pthread_mutex_t()
pthread_mutex_init(&mutex, nil)
func 🐶() {
pthread_mutex_lock(&mutex)
for index in 0..<3 {
print("\(index) I am a dog")
}
pthread_mutex_unlock(&mutex)
}
DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
print("线程一开始")
🐶()
print("线程一结束")
}
DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
print("线程二开始")
🐶()
print("线程二结束")
}
输出:
线程一开始
线程二开始
0 I am a dog
1 I am a dog
2 I am a dog
线程一结束
0 I am a dog
1 I am a dog
2 I am a dog
线程二结束
@synchronize
在OC和Swift中的@synchronize是截然不同的,不过这个不同主要是语法上的不一致,锁的类型和性质都是一样的,使用如下:
obj_sync_enter(lock)
obj_sync_exit(lock)
func 🐶(_ lock: Any) {
objc_sync_enter(lock)
for index in 0..<3 {
print("\(index) I am a dog")
}
objc_sync_exit(lock)
}
DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
print("线程一开始")
🐶(PlaygroundPage.current)
print("线程一结束")
}
DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
print("线程二开始")
🐶(PlaygroundPage.current)
print("线程二结束")
}
输出:
线程一开始
线程二开始
0 I am a dog
1 I am a dog
2 I am a dog
线程一结束
0 I am a dog
1 I am a dog
2 I am a dog
线程二结束
递归锁
什么是递归锁?
顾名思议,可以当前线程多次获取的锁就称为递归锁。它记录了当前线程获得锁的次数,每一次加锁,都要对应一次解锁,这样才不会产生死锁,一旦锁全部被释放完毕,资源才可以被其它线程锁使用。上图中的NSRecursiveLock和pthread_mutex(recusiveLock),而NSRecursiveLock内部封装的就是pthread_mutex(recursiveLock)
var mutexattr = pthread_mutexattr_t()
pthread_mutexattr_init(&mutexattr)
pthread_mutexattr_settype(&mutexattr, PTHREAD_MUTEX_RECURSIVE)
var mutex: pthread_mutex_t = pthread_mutex_t()
let lock = pthread_mutex_init(&mutex, &mutexattr)
pthread_mutexattr_destroy(&mutexattr)
func dog(count: Int) {
for _ in 0..<count{
pthread_mutex_lock(&mutex)
dog(count: count - 1)
pthread_mutex_unlock(&mutex)
}
}
DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
print("start")
dog(count: 3)
print("end")
}
读写锁
什么是读写锁?
读写锁,在多线程编程的环境之下,写锁是具备排他性的,一旦多个线程同时对同一个文件进行写操作,那么带来的副作用是灾难性的,但是读操作:多个线程是可以同时对同一文件进行读操作的。那么写操作可以使用是锁来实现呢?条件锁,信号量,互斥锁其实理论上都是可以的。
var lock = pthread_rwlock_t()
// 读锁
pthread_rwlock_rdlock(&lock)
// 写锁
pthread_rwlock_wrlock(&lock)
// 解锁
pthread_rwlock_unlock(&lock)
条件锁
什么是条件锁?
条件锁是一种比较特殊的锁,一般使用在线程之间中的调度,一个线程阻塞另一个线程,直到其中一个线程中的条件满足时,发送信号给另一线程使得另一线程正常的执行。比如说你开启一个下载图片线程,还有一个线程处理图片,那么这两个线程就可以通过条件锁来实现线程之间的调度。
NSConditionLock
lock() 表示该线程希望获得该锁,如果没有其他线程获取该锁,那么它就可以执行后续代码了,如果有其他的线程已经获取该锁了,那么它需要等待
lock(WhenCondition: 条件a): 如果没有其他线程获得该锁,但是该锁内部的condition不等于A条件,它依然不能获得锁,仍然等待。如果内部的condition等于A条件,并且没有其他线程获得该锁,则进入代码区,同时设置它获得该锁,其他任何线程都将等待它代码的完成,直至它解锁。
unLock(WhenCondition: 条件b): 表示立即释放锁,而且把条件设置为b
自旋锁
什么是自旋锁呢?
自旋锁是一种"忙等待",如果一个线程获得该锁之后,其他需要该资源的线程不会挂起,而是做一个忙等循环,直到此线程获得的这个自旋锁被释放之后,其他线程根据优先级获取到这个自旋锁,未获得这个锁的线程继续进行忙等待。 YYKit 作者 @ibireme 的文章说到自旋锁存在优先级反转问题:不再安全的 OSSpinLock
锁的总结
好了,我们最后来总结一下锁吧:
- 如果在进行文件的读写是,还是应该使用读写锁:
pthread_rwlock - 当对性能的要求比较高的时候,应该使用
DispatchSemaphore或者pthread_mutex - 因为苹果的自旋锁
OSSpinLock有bug问题,所以在使用自旋锁的时候需要谨慎
我们学到了什么?
最后,看完这篇文章你应该学会什么,或者说我写完之后,应该记住什么?
- iOS中一共存在几种不同类型的锁,以及他们在Swift中的基本实现
- 具体的开发环境中应该如何使用这些锁
- 缓存的分类以及定义
- 内存缓存目前有几种实现方案,以及它们内部是如何做到线程安全的?
- 什么是LRU算法,基本的原理是什么?