常常据说 HTTP
缓存 , 磁盘缓存 , 内存缓存 , 等等 . 但却搞不太清楚具体内容 ? 不要紧 , 这两篇文章咱们一块儿来探索一下 .git
NSCache
是苹果官方提供的缓存类,具体使用和NSMutableDictionary
相似,在AFN
和SDWebImage
框架中被使用来管理缓存- 官方解释
NSCache
在系统内存很低时,会自动释放对象 ( 可是注意 , 这里还有点文章 , 本文会讲 )NSCache
是线程安全的,在多线程操做中,不须要对NSCache
加锁NSCache
的Key
只是对对象进行Strong
引用,不是拷贝,在清理的时候计算的是实际大小而不是引用的大小 , 其key
不须要实现NSCoping
协议. ( 这一点不太了解的同窗能够类比NSMapTable
去学习)
NSCache
中有几个比较重要的属性和方法 , 是你必需要了解的 :github
totalCostLimit
swift
总消耗大小 . 当超过这个大小时
NSCache
会作一个内存修剪操做 . 默认值为0,表示没有限制缓存
countLimit
安全
可以缓存的对象的最大数量。默认值为0,表示没有限制bash
evictsObjectsWithDiscardedContent
数据结构
标识缓存是否回收废弃的内容多线程
//在缓存中设置指定键名对应的值,0成本
- (void)setObject:(ObjectType)obj forKey:(KeyType)key;
/* · 在缓存中设置指定键名对应的值,而且指定该键值对的成本, 用于计算记录在缓存中的全部对象的总成本 · 当出现内存警告或者超出缓存总成本上限的时候,缓存会开启一个回收过程,释放部份内容 */
- (void)setObject:(ObjectType)obj forKey:(KeyType)keycost:(NSUInteger)g;
//删除缓存中指定键名的对象
- (void)removeObjectForKey:(KeyType)key;
//删除缓存中全部的对象
- (void)removeAllObjects;
复制代码
简单的了解了 NSCache
这个类 , 咱们来写个 demo
, 以便研究它的释放机制和逻辑 .app
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
(可被清除) 的标识 , 具体逻辑机制咱们等下来探究 , 为何要作这个呢 ? 结合苹果硬件来讲的话 , 默认状况时 , 当咱们申请一块内存 , 当没有空闲内存时 , 系统会将一块可释放的内存中的数据置换到磁盘上而并不是是直接删除 . 那么这块内存就能够被用来存储新的内容.那么内存置换内容和建立新内容产生的开销对比 , 前者会更大 , 所以这个协议标识以后 , 这块内存会被直接释放 , 再也不进行置换 . 以此达到优化的策略 .
仍是不太清楚 ? 不要紧 . 咱们写代码来验证它的具体机制.
一样是刚刚咱们的这一份代码 . 不过增长一下几个步骤的处理.
NSPurgeableData
类型的属性 testPurgeableData
.@property (nonatomic, strong) NSPurgeableData *testPurgeableData;
复制代码
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];
}
复制代码
- (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"];
}
复制代码
#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
源码.
直接搜索 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
管理的 .
一样这个类中找到 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 : 先根据 key
查找有无旧值 , 有则先移除 , 后设置新值
2 : 根据传过来的 cost
进行缓存淘汰 _evictObjectsToMakeSpaceForObjectWithCost
( 这个方法源码过长 , 我就不放了, 简单概述一下他的淘汰策略 , 你们结合源码方法来看 )
set
开销 - 限制的大小averageAccesses
= ((_totalAccesses / (double)count) * 0.2) + 1;
取平均数的百分之二十 , 用了一个二八定律 . 其实它的淘汰策略的根本原理也就是咱们常常说的 LRU
.value
. 直到达到上面计算出来的所需空间. 最后更新占用数等属性.3 : 建立一个新的 _GSCachedObject
, 将属性赋值存储进去.
4 : 将这个新建立的对象 set
进 _objects
( NSMapTable ) 当中.
5 : 总占用数更新.
swift foundation 这个是 Apple 开源的 Swift Foundation
库的源码 . 咱们来看看它里面 NSCache
的淘汰策略.
一样 , 咱们直接来到 NSCache.swift
中. 类中基本和咱们熟知的大体相同 , 有一点须要提的就是:
Swift
中NSCache
的_entries
是使用Dictionary
来实现的 , 只不过它的key
value
分别是NSCacheKey
和NSCacheEntry<KeyType, ObjectType>
. 类比GNUstep
, 数据结构上是如出一辙, 只不过GNUstep
使用了NSMapTable
来存储values
.
而这个做为 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)
}
}
复制代码
这个 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
}
}
复制代码
那么接下来咱们一样来到赋值的方法.
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
, 释放对象 , 更新链表结构.
GNUstep
提供的源码 , 咱们得知其对于 NSCache
的处理是计算出一个平均访问次数 , 而后释放的是访问次数较少的对象 , 直到知足须要释放大小 . LRU
的机制.swift-corelibs-foundation
源码 , 咱们得知其首先 , 存储链表结构中是按对象花费内存大小排序的 .
totalCostLimit
大小限制来依次释放 , ( 先释放占用较小的对象 ) , 直到知足须要释放大小 .至此 , NSCache
的淘汰策略和结构原理咱们已经讲完 , 下篇博客会继续就 NSURLCache
以及 SDWebImage
中的处理机制讲解 .
若有错误 , 欢迎指正 .
如需转载请标明出处以及跳转连接 .