主要缓存图片方式针对经常使用的主流库: 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
});
}
}
复制代码