iOS NSCache & NSURLCache 机制原理探究 (一)

常常据说 HTTP 缓存 , 磁盘缓存 , 内存缓存 , 等等 . 但却搞不太清楚具体内容 ? 不要紧 , 这两篇文章咱们一块儿来探索一下 .git

1. NSCache

1.1 NSCache 定义与主要特色

  • NSCache 是苹果官方提供的缓存类,具体使用和 NSMutableDictionary 相似,在 AFNSDWebImage 框架中被使用来管理缓存
  • 官方解释 NSCache 在系统内存很低时,会自动释放对象 ( 可是注意 , 这里还有点文章 , 本文会讲 )
  • NSCache 是线程安全的,在多线程操做中,不须要对 NSCache 加锁
  • NSCacheKey 只是对对象进行 Strong 引用,不是拷贝,在清理的时候计算的是实际大小而不是引用的大小 , 其 key 不须要实现 NSCoping 协议. ( 这一点不太了解的同窗能够类比 NSMapTable 去学习)

1.2 NSCache 中比较重要的属性 & 方法

NSCache 中有几个比较重要的属性和方法 , 是你必需要了解的 :github

1.2.1 属性

  • totalCostLimitswift

    • 总消耗大小 . 当超过这个大小时 NSCache 会作一个内存修剪操做 . 默认值为0,表示没有限制缓存

  • countLimit安全

    • 可以缓存的对象的最大数量。默认值为0,表示没有限制bash

  • 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 , 以便研究它的释放机制和逻辑 .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 (可被清除) 的标识 , 具体逻辑机制咱们等下来探究 , 为何要作这个呢 ? 结合苹果硬件来讲的话 , 默认状况时 , 当咱们申请一块内存 , 当没有空闲内存时 , 系统会将一块可释放的内存中的数据置换到磁盘上而并不是是直接删除 . 那么这块内存就能够被用来存储新的内容.

  • 那么内存置换内容和建立新内容产生的开销对比 , 前者会更大 , 所以这个协议标识以后 , 这块内存会被直接释放 , 再也不进行置换 . 以此达到优化的策略 .

仍是不太清楚 ? 不要紧 . 咱们写代码来验证它的具体机制.

一样是刚刚咱们的这一份代码 . 不过增长一下几个步骤的处理.

  • 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. 直到达到上面计算出来的所需空间. 最后更新占用数等属性.
  • 3 : 建立一个新的 _GSCachedObject , 将属性赋值存储进去.

  • 4 : 将这个新建立的对象 set_objects ( NSMapTable ) 当中.

  • 5 : 总占用数更新.

1.5 Swift Foundation - NSCache 源码

swift foundation 这个是 Apple 开源的 Swift Foundation 库的源码 . 咱们来看看它里面 NSCache 的淘汰策略.

一样 , 咱们直接来到 NSCache.swift 中. 类中基本和咱们熟知的大体相同 , 有一点须要提的就是:

SwiftNSCache_entries 是使用 Dictionary 来实现的 , 只不过它的 key value 分别是 NSCacheKeyNSCacheEntry<KeyType, ObjectType> . 类比 GNUstep , 数据结构上是如出一辙, 只不过 GNUstep 使用了 NSMapTable 来存储 values.

1.5.1 key -- NSCacheKey

而这个做为 key 值的 NSCacheKey , 重写了 hashisEqual 两个方法 , 以此来定义 当前 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 是一个双向链表的数据结构 , 另外存储了用户传进来的 keyvalue 以及所花费的空间大小.

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 中的处理机制讲解 .

若有错误 , 欢迎指正 .

如需转载请标明出处以及跳转连接 .

相关文章
相关标签/搜索