主要緩存圖片方式針對經常使用的主流庫: SDWebImage、 Kingfisher、 AFNetworking(AlamofireImage)以及YYCache作分析。node
NSCache
是一個可變的集合類型,用於臨時存放鍵值對,當資源不足時會被移除。算法
class NSCache<KeyType, ObjectType> : NSObject where KeyType : AnyObject, ObjectType : AnyObject
這裏要注意的是,鍵值對必須是一個AnyObject,因此swift中的String類型不能當作Key用,須要轉換爲NSString。swift
NSCache
還有幾點與普通集合不一樣:數組
NSCache類包含各類自動收回策略,這些策略能夠確保緩存不會佔用過多的系統內存。若是其餘應用程序須要內存,則這些策略會從Cache中刪除某些項目,從而最大程度地減小其內存佔用量。緩存
您能夠從不一樣的線程添加,刪除和查詢Cache中的項目,沒必要本身對Cache加鎖。bash
與NSMutableDictionary對象不一樣,Cache不會複製放入其中的鍵對象。網絡
另外還有兩個關鍵屬性:數據結構
var countLimit: Int
能最大存放對象的數量
var totalCostLimit: Int
在開始釋放對象以前,cache最多能持有cost的數量
複製代碼
須要注意的是totalCostLimit中的cost是根據setObject(ObjectType, forKey: KeyType, cost: Int)
函數中的cost參數進行計算,也就是說cost須要使用者計算後存入。多線程
在初始化中,使用Config配置NSCache的緩存容量,以及清除週期。併發
public struct Config {
public var totalCostLimit: Int
public var countLimit: Int = .max
//單個緩存持續時間
public var expiration: StorageExpiration = .seconds(300)
//緩存清理間隔時間
public let cleanInterval: TimeInterval
public init(totalCostLimit: Int, cleanInterval: TimeInterval = 120) {
self.totalCostLimit = totalCostLimit
self.cleanInterval = cleanInterval
}
}
...
public init(config: Config) {
self.config = config
storage.totalCostLimit = config.totalCostLimit
storage.countLimit = config.countLimit
cleanTimer = .scheduledTimer(withTimeInterval: config.cleanInterval, repeats: true) { [weak self] _ in
guard let self = self else { return }
self.removeExpired()
}
}
複製代碼
存放的對象遵照CacheCostCalculable協議,對Image採用默認擴展的方式,直接計算圖片大小。
/// Represents types which cost in memory can be calculated.
public protocol CacheCostCalculable {
var cacheCost: Int { get }
}
...
extension KFCrossPlatformImage: CacheCostCalculable {
/// Cost of an image
public var cacheCost: Int { return kf.cost }
}
...
public class Backend<T: CacheCostCalculable> {
let storage = NSCache<NSString, StorageObject<T>>()
...
storage.setObject(object, forKey: key as NSString, cost: value.cacheCost)
複製代碼
對於在NSCache中存放的value,加上StorageObject封裝,將過時策略也寫入,方便在clearTimer清除時,移除過時對象。
let object = StorageObject(value, key: key, expiration: expiration)
storage.setObject(object, forKey: key as NSString, cost: value.cacheCost)
...
class StorageObject<T> {
let value: T
let expiration: StorageExpiration
let key: String
private(set) var estimatedExpiration: Date
init(_ value: T, key: String, expiration: StorageExpiration) {
self.value = value
self.key = key
self.expiration = expiration
self.estimatedExpiration = expiration.estimatedExpirationSinceNow
}
func extendExpiration(_ extendingExpiration: ExpirationExtending = .cacheTime) {
switch extendingExpiration {
case .none:
return
case .cacheTime:
self.estimatedExpiration = expiration.estimatedExpirationSinceNow
case .expirationTime(let expirationTime):
self.estimatedExpiration = expirationTime.estimatedExpirationSinceNow
}
}
var expired: Bool {
return estimatedExpiration.isPast
}
}
複製代碼
在官方文檔中,已經指出對於NSCache的操做能夠不用加鎖,那麼爲何做者仍是使用了NSLock?
NSMapTable
與字典類似,可是有更多的關於內存的意義。
NSMapTable
是根據NSDictionary建模的,但具備如下差別:
key/value可使用弱引用方式保存,以便在回收對象時刪除。
它的key/value能夠在輸入時複製,也可使用指針標識進行相等性和哈希處理。
它能夠包含任意指針(其內容不限於對象)。
在SD中,NSMapTable進行了這樣的初始化:
[[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
複製代碼
初始化時須要對key與value的option進行設定,這裏將key值設爲強引用,value設爲弱引用。
若對NSPointerFunctions.Options
進行深刻查看,還有更多選項。
主要分爲Memory Options, Personality Options,與Copy Option。使用時Memory Options, Personality Options只能各使用最多一個, 不能NSPointerFunctionsWeakMemory | NSPointerFunctionsStrongMemory
或是NSPointerFunctionsCStringPersonality | NSPointerFunctionsIntegerPersonality
。
與kingfisher相同,SDWebImage也有一個Config文件,裏面條目衆多,但事實上對於SDMemoryCache
來講,只有三個選項是有用的:
@interface SDImageCacheConfig : NSObject <NSCopying>
@property (assign, nonatomic) BOOL shouldUseWeakMemoryCache;
@property (assign, nonatomic) NSUInteger maxMemoryCost;
@property (assign, nonatomic) NSUInteger maxMemoryCount;
@end
複製代碼
maxMemoryCost與maxMemoryCount已經在前文中講過,那麼shouldUseWeakMemoryCache是用來幹嗎的呢?
相對於kf中直接持有一個NSCache,sd是直接寫了一個子類繼承了NSCache:
@interface SDMemoryCache <KeyType, ObjectType> : NSCache <KeyType, ObjectType> <SDMemoryCache>
@property (nonatomic, strong, nonnull, readonly) SDImageCacheConfig *config;
@end
複製代碼
而且在m文件中,聲明瞭兩個屬性,分別是NSMapTable與dispatch_semaphore_t
@property (nonatomic, strong, nonnull) NSMapTable<KeyType, ObjectType> *weakCache; // strong-weak cache
@property (nonatomic, strong, nonnull) dispatch_semaphore_t weakCacheLock; // a lock to keep the access to `weakCache`
複製代碼
在get、set、remove時override以前的方法:
- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g {
[super setObject:obj forKey:key cost:g];
if (!self.config.shouldUseWeakMemoryCache) {
return;
}
if (key && obj) {
// Store weak cache
SD_LOCK(self.weakCacheLock);
[self.weakCache setObject:obj forKey:key];
SD_UNLOCK(self.weakCacheLock);
}
}
- (id)objectForKey:(id)key {
id obj = [super objectForKey:key];
if (!self.config.shouldUseWeakMemoryCache) {
return obj;
}
if (key && !obj) {
// Check weak cache
SD_LOCK(self.weakCacheLock);
obj = [self.weakCache objectForKey:key];
SD_UNLOCK(self.weakCacheLock);
if (obj) {
// Sync cache
NSUInteger cost = 0;
if ([obj isKindOfClass:[UIImage class]]) {
cost = [(UIImage *)obj sd_memoryCost];
}
[super setObject:obj forKey:key cost:cost];
}
}
return obj;
}
複製代碼
因此shouldUseWeakMemoryCache實際上是一個控制是否使用NSMapTable的開關,若是打開了,在添加圖片時,會在NSMapTable中也緩存一份。這樣作的目的是,若是由於超過了cache數量的上線或者容量的上線,那麼緩存中先存入的對象則會被清除。這時再從緩存中獲取不到數據,須要從網絡上從新加載,可能會產生白屏的現象。而NSMapTable就是爲了這種狀況才使用的。
若沒有從cache中找到value,則會從NSMapTable中再找一次,若是有就再塞進cache中。
kf用了NSLock,sd用了dispatch_semaphore_t,它們的區別是什麼?
在處理圖片時,AF沒有用NSLock或者dispatch_semaphore_t去加鎖,只是經過建立GCD的方式訪問。
因爲GCD比較常見,因此就快速複習一遍。
在GCD中隊列的概念,分爲串行隊列與並行隊列。
在串行隊列中,丟進去的任務只能在單個線程中順序處理,在並行隊列中,會丟給不一樣的線程同時處理。
操做隊列時也有同步與異步的概念。
同步是指sync,須要等待隊列中任務處理結束,纔會走以後的處理。
異步是指async,不須要等待隊列中任務處理結束,也能走以後的處理。
在整個AFAutoPurgingImageCache
文件中,屢次用到了dispatch_barrier
的概念,主要功能是但願在併發隊列中,單獨處理一個任務,將其與以前以後的任務隔開,就意味着,當以前全部隊列中的任務執行完成以後,才能執行dispatch_barrier
的任務,當dispatch_barrier
的任務完成以後,才能執行隊列以後的任務。
在SD中存放的value爲UIImage,KF中是對Image封裝一層,爲了統一過時策略記錄了緩存持續時間,這AF中,也是 與以前KF相同的是,這裏使用了AFCachedImage
對Image封裝,更可能是記錄了lastAccessDate
字段,也就是上一次的訪問日期,清理緩存時,也是根據這個字段作處理。
@interface AFCachedImage : NSObject
@property (nonatomic, strong) UIImage *image;
@property (nonatomic, copy) NSString *identifier;
@property (nonatomic, assign) UInt64 totalBytes;
@property (nonatomic, strong) NSDate *lastAccessDate;
@property (nonatomic, assign) UInt64 currentMemoryUsage;
@end
複製代碼
在整個AFAutoPurgingImageCache
文件中,用NSMutableDictionary保存圖片,而且建立了一個併發隊列synchronizationQueue
,管理圖片的訪問,增長與刪除。
self.cachedImages = [[NSMutableDictionary alloc] init];
NSString *queueName = [NSString stringWithFormat:@"com.alamofire.autopurgingimagecache-%@", [[NSUUID UUID] UUIDString]];
self.synchronizationQueue = dispatch_queue_create([queueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);
複製代碼
在初始化時,設定了最大內存大小,與推薦內存大小。
- (instancetype)init {
return [self initWithMemoryCapacity:100 * 1024 * 1024 preferredMemoryCapacity:60 * 1024 * 1024];
}
複製代碼
在添加圖片時會用dispatch_barrier
方式,防止多線程訪問同一資源,每次添加圖片時,對其管理的當前內存大小更新,在更新結束後,查看是否超過最大內存大小限定,若是超過,則對已有的資源排序,從舊到新進行刪除,直到小於推薦使用內存大小。
- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier {
dispatch_barrier_async(self.synchronizationQueue, ^{
AFCachedImage *cacheImage = [[AFCachedImage alloc] initWithImage:image identifier:identifier];
AFCachedImage *previousCachedImage = self.cachedImages[identifier];
if (previousCachedImage != nil) {
self.currentMemoryUsage -= previousCachedImage.totalBytes;
}
self.cachedImages[identifier] = cacheImage;
self.currentMemoryUsage += cacheImage.totalBytes;
});
dispatch_barrier_async(self.synchronizationQueue, ^{
if (self.currentMemoryUsage > self.memoryCapacity) {
UInt64 bytesToPurge = self.currentMemoryUsage - self.preferredMemoryUsageAfterPurge;
NSMutableArray <AFCachedImage*> *sortedImages = [NSMutableArray arrayWithArray:self.cachedImages.allValues];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastAccessDate"
ascending:YES];
[sortedImages sortUsingDescriptors:@[sortDescriptor]];
UInt64 bytesPurged = 0;
for (AFCachedImage *cachedImage in sortedImages) {
[self.cachedImages removeObjectForKey:cachedImage.identifier];
bytesPurged += cachedImage.totalBytes;
if (bytesPurged >= bytesToPurge) {
break;
}
}
self.currentMemoryUsage -= bytesPurged;
}
});
}
複製代碼
AlamofireImage
與AF中緩存思想基本相同,因此再也不單獨解釋。
爲何這裏使用了併發隊列+barrier的組合,直接使用串行隊列進行訪問資源很差麼?
LRU是Least Recently Used的縮寫,即最近最少使用,是一種經常使用的頁面置換算法,選擇最近最久未使用的頁面予以淘汰。該算法賦予每一個頁面一個訪問字段,用來記錄一個頁面自上次被訪問以來所經歷的時間 t,當須淘汰一個頁面時,選擇現有頁面中其 t 值最大的,即最近最少使用的頁面予以淘汰。
須要注意的是,這個算法,增刪改查的時間複雜度都是O(1)。
經常使用的實現方式是經過字典+雙向鏈表的方式,存儲數據。
class LRUCache {
//單個節點的數據結構
class Node {
var pre: Node?
var next: Node?
var value: Int?
var key: Int?
}
//最大存儲數量
let maxSize: Int
//字典模型
var dic = [Int: Node]()
//頭節點
var head: Node?
//尾節點
var tail: Node?
//當前存儲內容的大小
var size = 0
init(_ capacity: Int) {
maxSize = capacity
head = Node()
tail = Node()
head?.next = tail
tail?.pre = head
}
func get(_ key: Int) -> Int {
if let node = dic[key] {
//若是訪問的key有值,則移動到前排
moveTohead(node)
}
return dic[key]?.value ?? -1
}
func put(_ key: Int, _ value: Int) {
if let node = dic[key] {
//若是放入的內容已經保存過了,則直接移動到前排
node.value = value
moveTohead(node)
return
}
let node = Node()
node.key = key
node.value = value
dic[key] = node
size += 1
add(node)
if size > maxSize {
removeLast()
size -= 1
}
}
private func moveTohead(_ node: Node) {
node.pre?.next = node.next
node.next?.pre = node.pre
add(node)
}
private func add(_ node: Node) {
node.pre = head
node.next = head?.next
head?.next?.pre = node
head?.next = node
}
private func removeLast() {
let last = tail?.pre
last?.pre?.next = tail
tail?.pre = last?.pre
if let key = last?.key {
dic.removeValue(forKey: key)
}
}
}
複製代碼
想練習算法的同窗能夠去leetcode上練手。leetcode-LRU算法
pthread_mutex_t是一種自旋鎖。
pthread_mutex_t _lock; \\聲明
pthread_mutex_init(&_lock, NULL);\\初始化
pthread_mutex_lock(&_lock);\\加鎖
pthread_mutex_unlock(&_lock);\\解鎖
pthread_mutex_trylock(&_lock) == 0\\查詢是否能加鎖
複製代碼
當互斥鎖已經被鎖定,這是再調用pthread_mutex_lock會形成線程堵塞。
函數pthread_mutex_trylock是pthread_mutex_lock的非阻塞版本。若是mutex參數所指定的互斥鎖已經被鎖定的話,調用pthread_mutex_trylock函數不會阻塞當前線程,而是當即返回一個值來描述互斥鎖的情況。
初始化時,聲明一個串行隊列以及互斥鎖管理線程,以及countLimit、costLimit、ageLimit管理緩存的生命週期,以及autoTrimInterval管理清理週期。
- (instancetype)init {
self = super.init;
pthread_mutex_init(&_lock, NULL);
_lru = [_YYLinkedMap new];
_queue = dispatch_queue_create("com.ibireme.cache.memory", DISPATCH_QUEUE_SERIAL);
_countLimit = NSUIntegerMax;
_costLimit = NSUIntegerMax;
_ageLimit = DBL_MAX;
_autoTrimInterval = 5.0;
......
[self _trimRecursively];
}
複製代碼
在初始化完成後,會直接調用_trimRecursively方法,在定義的串行隊列中清除緩存。
- (void)_trimRecursively {
__weak typeof(self) _self = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_autoTrimInterval * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
__strong typeof(_self) self = _self;
if (!self) return;
[self _trimInBackground];
[self _trimRecursively];
});
}
- (void)_trimInBackground {
dispatch_async(_queue, ^{
[self _trimToCost:self->_costLimit];
[self _trimToCount:self->_countLimit];
[self _trimToAge:self->_ageLimit];
});
}
複製代碼
雖然根據三個參數清除緩存,但主要邏輯差很少,因此只舉一個例子。
首先設定一個flag->finish
,標記是否清除完成,若緩存消耗最大值爲0則直接移除全部緩存,若當前消耗小於要求的最大值則直接return。
再經過循環的方式將對象從lru中移除,並保存到新建的數組中。 最後的dispatch_async中調用[holder count],是爲了一直持有holder,不讓它提早釋放。
- (void)_trimToCost:(NSUInteger)costLimit {
BOOL finish = NO;
pthread_mutex_lock(&_lock);
if (costLimit == 0) {
[_lru removeAll];
finish = YES;
} else if (_lru->_totalCost <= costLimit) {
finish = YES;
}
pthread_mutex_unlock(&_lock);
if (finish) return;
NSMutableArray *holder = [NSMutableArray new];
while (!finish) {
if (pthread_mutex_trylock(&_lock) == 0) {
if (_lru->_totalCost > costLimit) {
_YYLinkedMapNode *node = [_lru removeTailNode];
if (node) [holder addObject:node];
} else {
finish = YES;
}
pthread_mutex_unlock(&_lock);
} else {
usleep(10 * 1000); //10 ms
}
}
if (holder.count) {
dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
[holder count]; // release in queue
});
}
}
複製代碼