常常據說 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
中的處理機制講解 .
若有錯誤 , 歡迎指正 .
如需轉載請標明出處以及跳轉連接 .