经常听说 HTTP
缓存 , 磁盘缓存 , 内存缓存 , 等等 . 但却搞不太清楚具体内容 ? 没关系 , 这两篇文章我们一起来探索一下 .
1. NSCache
1.1 NSCache 定义与主要特点
NSCache
是苹果官方提供的缓存类,具体使用和NSMutableDictionary
类似,在AFN
和SDWebImage
框架中被使用来管理缓存- 官方解释
NSCache
在系统内存很低时,会自动释放对象 ( 但是注意 , 这里还有点文章 , 本文会讲 )NSCache
是线程安全的,在多线程操作中,不需要对NSCache
加锁NSCache
的Key
只是对对象进行Strong
引用,不是拷贝,在清理的时候计算的是实际大小而不是引用的大小 , 其key
不需要实现NSCoping
协议. ( 这一点不太了解的同学可以类比NSMapTable
去学习)
1.2 NSCache 中比较重要的属性 & 方法
NSCache
中有几个比较重要的属性和方法 , 是你必须要了解的 :
1.2.1 属性
-
totalCostLimit
-
总消耗大小 . 当超过这个大小时
NSCache
会做一个内存修剪操作 . 默认值为0,表示没有限制
-
-
countLimit
-
能够缓存的对象的最大数量。默认值为0,表示没有限制
-
-
evictsObjectsWithDiscardedContent
-
标识缓存是否回收废弃的内容
-
1.2.2 方法
//在缓存中设置指定键名对应的值,0成本
- (void)setObject:(ObjectType)obj forKey:(KeyType)key;
/*
· 在缓存中设置指定键名对应的值,并且指定该键值对的成本,
用于计算记录在缓存中的所有对象的总成本
· 当出现内存警告或者超出缓存总成本上限的时候,缓存会开启一个回收过程,释放部分内容
*/
- (void)setObject:(ObjectType)obj forKey:(KeyType)keycost:(NSUInteger)g;
//删除缓存中指定键名的对象
- (void)removeObjectForKey:(KeyType)key;
//删除缓存中所有的对象
- (void)removeAllObjects;
1.3 NSCache Demo
简单的了解了 NSCache
这个类 , 我们来写个 demo
, 以便研究它的释放机制和逻辑 .
LBNSCacheIOP
类 , 遵循了NSCacheDelegate
, 主要是监听NSCache
对象的释放代理回调通知.
// LBNSCacheIOP.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface LBNSCacheIOP : NSObject
@end
NS_ASSUME_NONNULL_END
//LBNSCacheIOP.m
#import "LBNSCacheIOP.h"
@interface LBNSCacheIOP () <NSCacheDelegate>
@end
@implementation LBNSCacheIOP
- (void)cache:(NSCache *)cache willEvictObject:(id)obj{
NSLog(@"obj:%@ 即将被:%@销毁",obj,cache);
}
@end
ViewController
// ViewController.h
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@end
// ViewController.m
#import "ViewController.h"
#import "LBNSCacheIOP.h"
@interface ViewController ()
@property(nonatomic , strong) NSCache * cache;
@property(nonatomic , strong) LBNSCacheIOP * cacheIOP;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_cacheIOP = [LBNSCacheIOP new];
_cache = [[NSCache alloc] init];
_cache.countLimit = 5;
_cache.delegate = _cacheIOP;
//往缓存中添加数据
[self lb_addCacheObject];
//内存警告通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(lb_didReceiveMemoryWaring:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
#pragma Mark - funcs
- (void)lb_addCacheObject{
for (int i = 0; i < 10; i++) {
[_cache setObject:[NSString stringWithFormat:@"lb_%d",i] forKey:[NSString stringWithFormat:@"lb__%d",i]];
}
}
- (void)lb_getCacheObject{
for (int i = 0; i < 10; i++) {
NSLog(@"Cache object:%@, at index :%d",[_cache objectForKey:[NSString stringWithFormat:@"lb__%d",i]],i);
}
}
#pragma Mark - MemoryWaringNotif
- (void)lb_didReceiveMemoryWaring:(NSNotification *)notification{
NSLog(@"notification----%@",notification);
}
//点击屏幕查看当前缓存对象存储内容
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self lb_getCacheObject];
}
@end
简单说一下代码逻辑就是:创建了一个
NSCache
类 , 注册了代理去监听内容释放 , 页面创建就执行添加十个字符串进去 , 点击屏幕就查看当前cache
存储的内容.
OK , 执行 , 打印如下 :
obj:lb_0 即将被:<NSCache: 0x600002f41cc0>销毁
obj:lb_1 即将被:<NSCache: 0x600002f41cc0>销毁
obj:lb_2 即将被:<NSCache: 0x600002f41cc0>销毁
obj:lb_3 即将被:<NSCache: 0x600002f41cc0>销毁
obj:lb_4 即将被:<NSCache: 0x600002f41cc0>销毁
- 点击屏幕 . 打印如下:
Cache object:(null), at index :0
Cache object:(null), at index :1
Cache object:(null), at index :2
Cache object:(null), at index :3
Cache object:(null), at index :4
Cache object:lb_5, at index :5
Cache object:lb_6, at index :6
Cache object:lb_7, at index :7
Cache object:lb_8, at index :8
Cache object:lb_9, at index :9
可以看到 , 我们 countLimit
缓存数量设置为 5
时 , 后续继续添加缓存时 , NSCache
对象会释放之前存储的内容 , 然后设置新的内容 .
( 注意 , 我并没有说会依次从前往后按存的顺序释放 , 虽然目前来看打印结果是这样 , 释放的到底是谁会根据其他一些处理来决定 . 下面会讲述. )
- 选择模拟器 ,
shift + cmd + h
将程序放入后台 ,然后我们就看到控制台上打印了:
obj:lb_5 即将被:<NSCache: 0x600002f41cc0>销毁
obj:lb_6 即将被:<NSCache: 0x600002f41cc0>销毁
obj:lb_7 即将被:<NSCache: 0x600002f41cc0>销毁
obj:lb_8 即将被:<NSCache: 0x600002f41cc0>销毁
obj:lb_9 即将被:<NSCache: 0x600002f41cc0>销毁
- 点击屏幕 . 打印如下:
Cache object:(null), at index :0
Cache object:(null), at index :1
Cache object:(null), at index :2
Cache object:(null), at index :3
Cache object:(null), at index :4
Cache object:(null), at index :5
Cache object:(null), at index :6
Cache object:(null), at index :7
Cache object:(null), at index :8
Cache object:(null), at index :9
也就是说 ,APP
进入后台之后 NSCache
会自动释放存储内容 ,并触发回调。
- 那么当我们收到内存警告的时候 ,会自动释放其中内容吗 ?我们来测试一下:
选择模拟器 ,发送通知。查看控制台 , 然后点击屏幕

打印如下 :
notification----NSConcreteNotification 0x6000010816b0 {name = UIApplicationDidReceiveMemoryWarningNotification; object = <UIApplication: 0x7fb0d1600a50>}
Cache object:(null), at index :0
Cache object:(null), at index :1
Cache object:(null), at index :2
Cache object:(null), at index :3
Cache object:(null), at index :4
Cache object:lb_5, at index :5
Cache object:lb_6, at index :6
Cache object:lb_7, at index :7
Cache object:lb_8, at index :8
Cache object:lb_9, at index :9
以上发现 , 当收到内存警告时 , NSCache
并不会自动释放存储的内容 .
还有一点需要提到的就是 鉴于 NSCache 官方文档中描述的所说. 苹果源生提供了一个 NSDiscardableContent
协议机制 , 以此来提高缓存的驱逐/释放行为.

什么意思呢 ? 这里就不讲述的很细了 因为我也只是了解个大概
也就是说 , 当我们同意了这个这个协议 , 其实就是给存储的内容打上了一个
purgeable
(可被清除) 的标识 , 具体逻辑机制我们等下来探究 , 为什么要做这个呢 ? 结合苹果硬件来说的话 , 默认情况时 , 当我们申请一块内存 , 当没有空闲内存时 , 系统会将一块可释放的内存中的数据置换到磁盘上而并非是直接删除 . 那么这块内存就可以被用来存储新的内容.那么内存置换内容和创建新内容产生的开销对比 , 前者会更大 , 因此这个协议标识之后 , 这块内存会被直接释放 , 不再进行置换 . 以此达到优化的策略 .
还是不太清楚 ? 没关系 . 我们写代码来验证它的具体机制.
同样是刚刚我们的这一份代码 . 不过增加一下几个步骤的处理.
- 1️⃣: 添加一个
NSPurgeableData
类型的属性testPurgeableData
.
@property (nonatomic, strong) NSPurgeableData *testPurgeableData;
- 2️⃣: 同样 , 还是在
viewdidload
中设置初始化东西 , 读取一张图片CGImageGetDataProvider
, 然后赋值到_testPurgeableData
中.
- (void)viewDidLoad {
[super viewDidLoad];
_cacheIOP = [LBNSCacheIOP new];
_cache = [[NSCache alloc] init];
_cache.countLimit = 5;
_cache.delegate = _cacheIOP;
//加载一张图片数据
UIImage *image = [UIImage imageNamed:@"timg.jpeg"];;
CFDataRef rawData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));
//读取数据赋值给 NSPurgeableData 属性对象
_testPurgeableData = [[NSPurgeableData alloc] initWithData:(__bridge NSData * _Nonnull)(rawData)];
//往缓存中添加数据
[self lb_addCacheObject];
//内存警告通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(lb_didReceiveMemoryWaring:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
- 3️⃣: 添加数据的方法作如下处理 :
- (void)lb_addCacheObject{
for (int i = 0; i < 4; i++) {
[_cache setObject:[NSString stringWithFormat:@"lb_%d",i] forKey:[NSString stringWithFormat:@"lb__%d",i]];
}
[_cache setObject:_testPurgeableData forKey:@"lb__4"];
}
- 4️⃣: 接收到内存警告处理
#pragma Mark - MemoryWaringNotif
- (void)lb_didReceiveMemoryWaring:(NSNotification *)notification{
NSLog(@"notification----%@",notification);
[_testPurgeableData endContentAccess];
}
简单说一下代码 , 其实就是我们使用了一个
NSPurgeableData
的对象 , 因为它是遵循了NSDiscardableContent
协议的.
- 在初始化
vc
时添加了 4 个字符串和一个NSPurgeableData
对象.- 在收到内存警告时 将这个对象计数器减一
endContentAccess
.
这里的计数器还是提一下吧 , 它和我们的引用计数不同 , 但是又很类似.
@protocol NSDiscardableContent
@required
- (BOOL)beginContentAccess; //计数器加一,
- (void)endContentAccess; // 计数器减一
@end
当计数器 >= 1
时 , 代表对象是可以使用的 , 否则代表对象是可被清除的.
好 . 那么我们 run
一下 code
. 运行成功后 大家可以先点击一下屏幕打印一下当前 NSCache
存储的情况 . 我就不列了 . 因为图片 data
很长 . 然后选择模拟器 shift + cmd + m
发出内存警告. 点击屏幕 . 打印结果 :
Cache object:lb_0, at index :0
Cache object:lb_1, at index :1
Cache object:lb_2, at index :2
Cache object:lb_3, at index :3
obj:<NSPurgeableData: 0x6000010fc580> 即将被:<NSCache: 0x6000010c2680>销毁
Cache object:(null), at index :4
Cache object:(null), at index :5
Cache object:(null), at index :6
Cache object:(null), at index :7
Cache object:(null), at index :8
Cache object:(null), at index :9
我们看到一个小细节 , 收到内存警告并没有释放, 但当我们再次访问时 , 第 5 个数据被释放了. 第五个数据实现了 NSDiscardableContent
协议 , 那么也就是 当访问 NSCache
对象时 , 会自动释放掉所有计数为 0 的对象 .
看到这里我们大体上对 NSCache
的机制大体上有了了解. 那么接下来 我们结合 GNUstep 以及 swift foundation 来查看下 NSCache
源码.
1.4 GNUstep - NSCache 源码
1.4.1 GNUstep - NSCache 类源码
直接搜索 NSCache
来到这个类中.
@interface GS_GENERIC_CLASS(NSCache, KeyT, ValT) : NSObject
{
#if GS_EXPOSE(NSCache)
@private
/** The maximum total cost of all cache objects. */
NSUInteger _costLimit;
/** Total cost of currently-stored objects. */
NSUInteger _totalCost;
/** The maximum number of objects in the cache. */
NSUInteger _countLimit;
/** The delegate object, notified when objects are about to be evicted. */
id _delegate;
/** Flag indicating whether discarded objects should be evicted */
BOOL _evictsObjectsWithDiscardedContent;
/** Name of this cache. */
NSString *_name;
/** The mapping from names to objects in this cache. */
NSMapTable *_objects;
/** LRU ordering of all potentially-evictable objects in this cache. */
GS_GENERIC_CLASS(NSMutableArray, ValT) *_accesses;
/** Total number of accesses to objects */
int64_t _totalAccesses;
#endif
#if GS_NONFRAGILE
#else
@private id _internal GS_UNUSED_IVAR;
#endif
}
这里基本跟我们的认知差不多 , 值得一提的是 _objects
的内容是用 NSMapTable
管理的 .
1.4.2 setObject : forKey : cost
同样这个类中找到 setObject : forKey : cost
方法实现
- (void) setObject: (id)obj forKey: (id)key cost: (NSUInteger)num
{
_GSCachedObject *oldObject = [_objects objectForKey: key];
_GSCachedObject *newObject;
if (nil != oldObject)
{
[self removeObjectForKey: oldObject->key];
}
[self _evictObjectsToMakeSpaceForObjectWithCost: num];
newObject = [_GSCachedObject new];
// Retained here, released when obj is dealloc'd
newObject->object = RETAIN(obj);
newObject->key = RETAIN(key);
newObject->cost = num;
if ([obj conformsToProtocol: @protocol(NSDiscardableContent)])
{
newObject->isEvictable = YES;
[_accesses addObject: newObject];
}
[_objects setObject: newObject forKey: key];
RELEASE(newObject);
_totalCost += num;
}
简单概述一下 :
1.4.3 GNUstep - NSCache 机制总结
-
1 : 先根据
key
查找有无旧值 , 有则先移除 , 后设置新值 -
2 : 根据传过来的
cost
进行缓存淘汰_evictObjectsToMakeSpaceForObjectWithCost
( 这个方法源码过长 , 我就不放了, 简单概述一下他的淘汰策略 , 大家结合源码方法来看 )- 2.1 : 先计算出需要驱逐的空间大小 : 总开销 + 本次
set
开销 - 限制的大小 - 2.2 : 计算出了一个平均访问次数
averageAccesses
=((_totalAccesses / (double)count) * 0.2) + 1;
取平均数的百分之二十 , 用了一个二八定律 . 其实它的淘汰策略的根本原理也就是我们经常说的LRU
. - 2.3 : 循环处理 , 发送通知 ( discardContentIfPossible ) , 驱逐访问次数小于计算结果并且对象是可移除的
value
. 直到达到上面计算出来的所需空间. 最后更新占用数等属性.
- 2.1 : 先计算出需要驱逐的空间大小 : 总开销 + 本次
-
3 : 创建一个新的
_GSCachedObject
, 将属性赋值存储进去. -
4 : 将这个新创建的对象
set
进_objects
( NSMapTable ) 当中. -
5 : 总占用数更新.
1.5 Swift Foundation - NSCache 源码
swift foundation 这个是 Apple 开源的 Swift Foundation
库的源码 . 我们来看看它里面 NSCache
的淘汰策略.
同样 , 我们直接来到 NSCache.swift
中. 类中基本和我们熟知的大致相同 , 有一点需要提的就是:
Swift
中NSCache
的_entries
是使用Dictionary
来实现的 , 只不过它的key
value
分别是NSCacheKey
和NSCacheEntry<KeyType, ObjectType>
. 类比GNUstep
, 数据结构上是一模一样, 只不过GNUstep
使用了NSMapTable
来存储values
.
1.5.1 key -- NSCacheKey
而这个作为 key
值的 NSCacheKey
, 重写了 hash
和 isEqual
两个方法 , 以此来定义 当前 key
的哈希值相等的条件 ( NSMapTable ).
override var hash: Int {
switch self.value {
case let nsObject as NSObject:
return nsObject.hashValue
case let hashable as AnyHashable:
return hashable.hashValue
default: return 0
}
}
override func isEqual(_ object: Any?) -> Bool {
guard let other = (object as? NSCacheKey) else { return false }
if self.value === other.value {
return true
} else {
guard let left = self.value as? NSObject,
let right = other.value as? NSObject else { return false }
return left.isEqual(right)
}
}
1.5.2 value -- NSCacheEntry
这个 NSCacheEntry
是一个双向链表的数据结构 , 另外存储了用户传进来的 key
和 value
以及所花费的空间大小.
private class NSCacheEntry<KeyType : AnyObject, ObjectType : AnyObject> {
var key: KeyType
var value: ObjectType
var cost: Int
var prevByCost: NSCacheEntry?
var nextByCost: NSCacheEntry?
init(key: KeyType, value: ObjectType, cost: Int) {
self.key = key
self.value = value
self.cost = cost
}
}
1.5.3 设置新值
那么接下来我们同样来到赋值的方法.
open func setObject(_ obj: ObjectType, forKey key: KeyType, cost g: Int) {
let g = max(g, 0)
let keyRef = NSCacheKey(key)
_lock.lock()
let costDiff: Int
if let entry = _entries[keyRef] {
costDiff = g - entry.cost
entry.cost = g
entry.value = obj
if costDiff != 0 {
remove(entry)
insert(entry)
}
} else {
let entry = NSCacheEntry(key: key, value: obj, cost: g)
_entries[keyRef] = entry
insert(entry)
costDiff = g
}
_totalCost += costDiff
var purgeAmount = (totalCostLimit > 0) ? (_totalCost - totalCostLimit) : 0
while purgeAmount > 0 {
if let entry = _head {
delegate?.cache(unsafeDowncast(self, to:NSCache<AnyObject, AnyObject>.self), willEvictObject: entry.value)
_totalCost -= entry.cost
purgeAmount -= entry.cost
remove(entry) // _head will be changed to next entry in remove(_:)
_entries[NSCacheKey(entry.key)] = nil
} else {
break
}
}
var purgeCount = (countLimit > 0) ? (_entries.count - countLimit) : 0
while purgeCount > 0 {
if let entry = _head {
delegate?.cache(unsafeDowncast(self, to:NSCache<AnyObject, AnyObject>.self), willEvictObject: entry.value)
_totalCost -= entry.cost
purgeCount -= 1
remove(entry) // _head will be changed to next entry in remove(_:)
_entries[NSCacheKey(entry.key)] = nil
} else {
break
}
}
_lock.unlock()
}
方法很长 , 我没有做省略 , 方便没有下载的同学分析查看.
这里面有几个点需要提的 :
- 1 . 首先和
GNUstep
中一样 , 先通过这个key
在_entries
中取值 , 取到就代表有旧值 , 先更新这个对象中存储的value
和内存消耗大小 , 然后先移除 . 再添加插入 ( 更新链表结构 , 另外插入的时候根据占用内存排了序entry.cost > currentElement.cost
).- 2 . 接下来与
GNUstep
同样 , 根据totalCostLimit
占用大小限制 计算出需要放逐的空间大小. ( var purgeAmount = (totalCostLimit > 0) ? (_totalCost - totalCostLimit) : 0- 3 . 通知代理回调 , 即将放逐对象
- 4 . 更新总花费大小
_totalCost
, 释放对象 , 更新链表结构.- 5 . 通过个数限制
countLimit
计算需要释放个数. ( var purgeCount = (countLimit > 0) ? (_entries.count - countLimit) : 0- 6 . 通知代理回调 , 即将放逐对象
- 7 . 更新总花费大小
_totalCost
, 释放对象 , 更新链表结构.
1.6 NSCache 总结
- 通过
GNUstep
提供的源码 , 我们得知其对于NSCache
的处理是计算出一个平均访问次数 , 然后释放的是访问次数较少的对象 , 直到满足需要释放大小 .LRU
的机制. - 通过
swift-corelibs-foundation
源码 , 我们得知其首先 , 存储链表结构中是按对象花费内存大小排序的 .- 然后首先通过用户有无指定
totalCostLimit
大小限制来依次释放 , ( 先释放占用较小的对象 ) , 直到满足需要释放大小 . - 然后再通过个数限制来释放 , 直到满足需要释放大小 ( 依旧是先释放较小的对象 ) .
- 然后首先通过用户有无指定
至此 , NSCache
的淘汰策略和结构原理我们已经讲完 , 下篇博客会继续就 NSURLCache
以及 SDWebImage
中的处理机制讲解 .
如有错误 , 欢迎指正 .
如需转载请标明出处以及跳转链接 .