深入源码理解YYCache 、SDWebImage、AFNetworking、NSCache 缓存方式与对比

深入源码理解YYCache 、SDWebImage、AFNetworking、NSCache 缓存方式与对比

转载请注明出处 http://www.jianshu.com/p/18d9fe85266d

在之前的一篇文章iOS缓存 NSCache详解及SDWebImage缓存策略源码分析中详细讲解了NSCache的用法以及SDWebImage内存和磁盘缓存的源码分析,本篇文章将简要讲解AFNetworking缓存类和YYCache并作出对比。

由于之前的一篇文章已经详细讲解了NSCacheSDWebImage缓存策略,本篇文章不再赘述,会简要介绍一下AFNetworkingYYCache的源码。

AFNetworking图片缓存AFAutoPurgingImageCache

AFNetworking也提供了同SDWebImage一样的下载图片的功能,也提供了缓存这些图片的功能,但它只提供了内存缓存,没有提供磁盘缓存功能。

看一下头文件:

//缓存协议,如果用户需要实现自定义的
@protocol AFImageCache <NSObject>

//添加图片并传递一个唯一id,一般使用图片的URL
- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier;

//删除id为identifier的图片
- (BOOL)removeImageWithIdentifier:(NSString *)identifier;

//删除所有缓存图片
- (BOOL)removeAllImages;

//根据id获取图片
- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier;
@end

//AFImageRequestCache协议,继承AFImageCache协议
@protocol AFImageRequestCache <AFImageCache>

//添加图片,传入下载图片的request和额外的id
- (void)addImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;

//删除图片,传入下载图片的request和额外的id
- (BOOL)removeImageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;

//根据下载图片的request和额外的id获取图片
- (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;

@end

//AFNetworking提供的缓存类,
@interface AFAutoPurgingImageCache : NSObject <AFImageRequestCache>

//单位是字节,能够支持最大缓存大小
@property (nonatomic, assign) UInt64 memoryCapacity;

//建议的当内存缓存要释放的时候需要释放到多大的大小
@property (nonatomic, assign) UInt64 preferredMemoryUsageAfterPurge;

//当前内存缓存占用的字节大小
@property (nonatomic, assign, readonly) UInt64 memoryUsage;

//构造函数
- (instancetype)init;

//构造函数
- (instancetype)initWithMemoryCapacity:(UInt64)memoryCapacity preferredMemoryCapacity:(UInt64)preferredMemoryCapacity;

@end

从头文件可以看出,AFNetworking提供的AFAutoPurgingImageCache接口不多,而且都很简单,只实现了内存缓存的功能。

看一下实现文件:

//缓存对象包装类
@interface AFCachedImage : NSObject
//缓存的图片
@property (nonatomic, strong) UIImage *image;
//id
@property (nonatomic, strong) NSString *identifier;
//图片字节大小
@property (nonatomic, assign) UInt64 totalBytes;
//淘汰算法是LRU所以需要记录上次访问时间
@property (nonatomic, strong) NSDate *lastAccessDate;
//没用到的属性
@property (nonatomic, assign) UInt64 currentMemoryUsage;

@end

@implementation AFCachedImage
//初始化构函数
-(instancetype)initWithImage:(UIImage *)image identifier:(NSString *)identifier {
    if (self = [self init]) {
        self.image = image;
        self.identifier = identifier;
        //计算图片的字节大小,每个像素占4字节32位
        CGSize imageSize = CGSizeMake(image.size.width * image.scale, image.size.height * image.scale);
        CGFloat bytesPerPixel = 4.0;
        CGFloat bytesPerSize = imageSize.width * imageSize.height;
        self.totalBytes = (UInt64)bytesPerPixel * (UInt64)bytesPerSize;
        self.lastAccessDate = [NSDate date];
    }
    return self;
}
//通过缓存对象获取图片时要更新上次访问时间为当前时间
- (UIImage*)accessImage {
    //直接使用NSDate
    self.lastAccessDate = [NSDate date];
    return self.image;
}

- (NSString *)description {
    NSString *descriptionString = [NSString stringWithFormat:@"Idenfitier: %@  lastAccessDate: %@ ", self.identifier, self.lastAccessDate];
    return descriptionString;

}

@end

//AFNetworking缓存类的猪脚
@interface AFAutoPurgingImageCache ()
//可变字典用于存储所有的缓存对象AFCachedImage对象,key为字符串类型
@property (nonatomic, strong) NSMutableDictionary <NSString* , AFCachedImage*> *cachedImages;
//当前缓存对象内存占用大小
@property (nonatomic, assign) UInt64 currentMemoryUsage;
//用于线程安全防止产生竞争条件,没有用锁
@property (nonatomic, strong) dispatch_queue_t synchronizationQueue;
@end

@implementation AFAutoPurgingImageCache

//构造函数,默认内存占用100M,每次清除缓存到60M
- (instancetype)init {
    return [self initWithMemoryCapacity:100 * 1024 * 1024 preferredMemoryCapacity:60 * 1024 * 1024];
}

//构造函数
- (instancetype)initWithMemoryCapacity:(UInt64)memoryCapacity preferredMemoryCapacity:(UInt64)preferredMemoryCapacity {
    if (self = [super init]) {
        self.memoryCapacity = memoryCapacity;
        self.preferredMemoryUsageAfterPurge = preferredMemoryCapacity;
        self.cachedImages = [[NSMutableDictionary alloc] init];
        //创建一个并行队列,但后面使用时都是在同步情况或barrier情况下,队列中的任务还是以串行执行
        //可以防止产生竞争条件,保证线程安全
        NSString *queueName = [NSString stringWithFormat:@"com.alamofire.autopurgingimagecache-%@", [[NSUUID UUID] UUIDString]];
        self.synchronizationQueue = dispatch_queue_create([queueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);

        //添加通知,监听收到系统的内存警告后删除所有缓存对象
        [[NSNotificationCenter defaultCenter]
         addObserver:self
         selector:@selector(removeAllImages)
         name:UIApplicationDidReceiveMemoryWarningNotification
         object:nil];

    }
    return self;
}
//析构函数,删除通知的监听
- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}
//memoryUsage的getter
- (UInt64)memoryUsage {
    __block UInt64 result = 0;
    dispatch_sync(self.synchronizationQueue, ^{
        result = self.currentMemoryUsage;
    });
    return result;
}

//添加图片到缓存中
- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier {
    //使用dispatch_barrier_async不阻塞当前线程,不阻塞向队列中添加任务
    //但队列中其他任务要执行就必须等待前一个任务结束,不管是不是并发队列
    dispatch_barrier_async(self.synchronizationQueue, ^{
        //创建AFCachedImage对象
        AFCachedImage *cacheImage = [[AFCachedImage alloc] initWithImage:image identifier:identifier];
        //判断对应id是否已经保存在缓存字典中了
        AFCachedImage *previousCachedImage = self.cachedImages[identifier];
        //如果已经保存了减去占用的内存大小
        if (previousCachedImage != nil) {
            self.currentMemoryUsage -= previousCachedImage.totalBytes;
        }
        //更新字典,更新占用内存大小
        self.cachedImages[identifier] = cacheImage;
        self.currentMemoryUsage += cacheImage.totalBytes;
    });
    //同上,该block必须等待前一个block执行完成才可以执行
    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;
            //遍历,释放缓存对象,满足要求后break
            for (AFCachedImage *cachedImage in sortedImages) {
                [self.cachedImages removeObjectForKey:cachedImage.identifier];
                bytesPurged += cachedImage.totalBytes;
                if (bytesPurged >= bytesToPurge) {
                    break ;
                }
            }
            //更新当前占用缓存大小
            self.currentMemoryUsage -= bytesPurged;
        }
    });
}

//删除图片
- (BOOL)removeImageWithIdentifier:(NSString *)identifier {
    __block BOOL removed = NO;
    //同步方法
    dispatch_barrier_sync(self.synchronizationQueue, ^{
        AFCachedImage *cachedImage = self.cachedImages[identifier];
        if (cachedImage != nil) {
            [self.cachedImages removeObjectForKey:identifier];
            self.currentMemoryUsage -= cachedImage.totalBytes;
            removed = YES;
        }
    });
    return removed;
}

//删除所有图片
- (BOOL)removeAllImages {
    __block BOOL removed = NO;
    //同步方法
    dispatch_barrier_sync(self.synchronizationQueue, ^{
        if (self.cachedImages.count > 0) {
            [self.cachedImages removeAllObjects];
            self.currentMemoryUsage = 0;
            removed = YES;
        }
    });
    return removed;
}

//根据id获取图片
- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier {
    __block UIImage *image = nil;
    dispatch_sync(self.synchronizationQueue, ^{
        AFCachedImage *cachedImage = self.cachedImages[identifier];
        //更新访问时间
        image = [cachedImage accessImage];
    });
    return image;
}
//AFImageRequestCache协议的方法,通过request构造一个key然后调用前面的方法
- (void)addImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier {
    [self addImage:image withIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]];
}
//同上
- (BOOL)removeImageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier {
    return [self removeImageWithIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]];
}
//同上
- (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier {
    return [self imageWithIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]];
}
//通过request和额外的id构造一个key
- (NSString *)imageCacheKeyFromURLRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)additionalIdentifier {
    //取图片的URI然后追加额外的id构造key
    NSString *key = request.URL.absoluteString;
    if (additionalIdentifier != nil) {
        key = [key stringByAppendingString:additionalIdentifier];
    }
    return key;
}

@end

AFAutoPurgingImageCache的实现很简单,逻辑也都很简单,不再赘述了。它的淘汰算法采用的是LRU,从源码中其实也可以看出缺点挺多的,比如上次访问时间使用NSDate类,使用UNIX时间戳应该更好,不仅内存占用小排序也更快吧。淘汰缓存时需要从缓存字典中取出所有的缓存对象然后根据NSDate排序,如果有大量缓存图片,这里似乎就是一个性能瓶颈,但它的优点就是实现简单明了,对于性能要求不高的程序选择这个也没有太多影响。

YYCache的内存缓存和磁盘缓存

本节文章将讲解YYCache的内存缓存YYMemoryCache和磁盘缓存YYDiskCache,但源码较多,而且本文的篇幅有限,所以不再和之前的文章一样贴所有的源码来讲解,这里只会贴一些比较重要的代码进行讲解,需要深入研究的读者还是要自己看一下完整的源码。

YYMemoryCache讲解

贴一下头文件的接口代码:

@interface YYMemoryCache : NSObject

//内存
@property (nullable, copy) NSString *name;
//当前缓存对象的个数
@property (readonly) NSUInteger totalCount;
//当前缓存对象的总cost数
@property (readonly) NSUInteger totalCost;

//同NSCache,支持缓存多少个对象
@property NSUInteger countLimit;
//同NSCache,每个缓存对象可以设置一个cost,这个值就标识支持缓存的最大cost总量
@property NSUInteger costLimit;
//缓存的过期时间,单位是秒
@property NSTimeInterval ageLimit;

//自动清理缓存的时间间隔,默认是5秒
@property NSTimeInterval autoTrimInterval;

//是否在程序进入后台后删除所有的缓存对象
@property BOOL shouldRemoveAllObjectsWhenEnteringBackground;

//收到系统内存警告后执行的回调块
@property (nullable, copy) void(^didReceiveMemoryWarningBlock)(YYMemoryCache *cache);

//程序进入后台后执行的回调块
@property (nullable, copy) void(^didEnterBackgroundBlock)(YYMemoryCache *cache);

//是否在主线程释放对象,如果有UIView这样的UI对象需要在主线程中释放
@property BOOL releaseOnMainThread;

//是否异步释放,默认是YES
@property BOOL releaseAsynchronously;

//key的缓存对象是否存在
- (BOOL)containsObjectForKey:(id)key;

//根据key获取缓存对象
- (nullable id)objectForKey:(id)key;

//设置key的缓存对象
- (void)setObject:(nullable id)object forKey:(id)key;

//设置key的缓存对象并设置其cost值
- (void)setObject:(nullable id)object forKey:(id)key withCost:(NSUInteger)cost;

//删除key的缓存对象
- (void)removeObjectForKey:(id)key;

//删除所有缓存对象
- (void)removeAllObjects;

//清除缓存到缓存对象只有count个
- (void)trimToCount:(NSUInteger)count;

//清除缓存到cost上限为cost
- (void)trimToCost:(NSUInteger)cost;

//清除超过age的过期缓存对象
- (void)trimToAge:(NSTimeInterval)age;

@end

YYMemeoryCache提供的接口和NSCache类似,提供缓存对象个数限制、缓存对象占用cost限制和缓存对象过期限制,实现缓存对象的清理。YYMemeoryCacheAFAutoPurgingImageCache不同,他使用链表来管理缓存对象,同样也使用LRU淘汰算法来清除缓存对象。

首先看一下链表节点定义:

//这是一个双向链表,保存prev和next指针
@interface _YYLinkedMapNode : NSObject {
    @package
    //__unsafe_unretained标识不保留其他节点对象,提高效率
    __unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
    __unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
    //记录key、value、cost和访问时间,使用unix时间戳
    id _key;
    id _value;
    NSUInteger _cost;
    NSTimeInterval _time;
}
@end

@implementation _YYLinkedMapNode
@end

上面的代码就是双向链表的节点,其实和AFAutoPurgingImageCache封装的缓存对象一样,不过YYMemeoryCache支持任意类型的缓存对象而不限于是图片,YYMemeoryCache性能和效率非常高,通过上面的定义也可以看出,能够提高效率的地方一定会用最好的方式实现,由于是YYMemeoryCache管理缓存对象的节点,所以能够保证在生命周期内不会被释放,所以使用__unsafe_unretained修饰,不会产生任何问题。

再看一下链表的定义:

@interface _YYLinkedMap : NSObject {
    @package
    //使用Core Foundation的CFMutableDictionaryRef可变字典保存上面的节点对象
    CFMutableDictionaryRef _dic; // do not set object directly
    //记录总cost
    NSUInteger _totalCost;
    //记录总count
    NSUInteger _totalCount;
    //记录双向链表的head指针
    _YYLinkedMapNode *_head; // MRU, do not change it directly
    //双向链表的tail指针
    _YYLinkedMapNode *_tail; // LRU, do not change it directly
    //是否在主线程中释放缓存对象
    BOOL _releaseOnMainThread;
    //是否异步释放缓存对象
    BOOL _releaseAsynchronously;
}

//双向链表在表头插入节点
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;

//将节点移到表头,LRU算法,访问一个缓存对象后就将这个对象移动到表头
//不需要向AFNetworking一样记录NSDate然后更新排序
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;

//删除一个节点
- (void)removeNode:(_YYLinkedMapNode *)node;

//删除尾节点
- (_YYLinkedMapNode *)removeTailNode;

//删除所有的节点
- (void)removeAll;

@end

上面是链表的定义,它使用Core FoundationCFMutableDictionaryRef来保存节点对象,并维护一个双向链表,记录了一些需要使用的参数。也就是说YYMemeoryCache会通过构造一个_YYLinkedMapNode对象来封装需要缓存的对象,然后使用_YYLinkedMap对象来维护和管理这个双向链表,由于篇幅问题不列举所有的实现方法了,这些方法都是常规的链表操作,读者可自行阅读,举一个源码:

//将一个节点移动到表头,由于使用LRU淘汰算法
//所以当我们访问一个缓存对象时就会调用这个方法将封装缓存对象的节点移动到表头
- (void)bringNodeToHead:(_YYLinkedMapNode *)node {
    //判断节点是头节点、尾节点或普通节点,然后修改指针指向
    if (_head == node) return;
    
    if (_tail == node) {
        _tail = node->_prev;
        _tail->_next = nil;
    } else {
        node->_next->_prev = node->_prev;
        node->_prev->_next = node->_next;
    }
    node->_next = _head;
    node->_prev = nil;
    _head->_prev = node;
    _head = node;
}

其他方法就不再赘述了,讲到这里再结合之前AFNetworking的缓存源码,相信大家也可以自己写出添加缓存、删除缓存的代码了。接下来看一下YYMemeoryCache如何添加缓存对象的:

//添加缓存对象
- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
    //key为空直接返回
    if (!key) return;
    //要缓存的对象object不存在就删除对应key的已经缓存的对象
    if (!object) {
        [self removeObjectForKey:key];
        return;
    }
    //使用pthread_mutext互斥锁,防止产生竞争条件保障线程安全
    pthread_mutex_lock(&_lock);
    //首先从存储缓存对象的字典中获取缓存节点
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    //获取一个时间
    NSTimeInterval now = CACurrentMediaTime();
    //如果从缓存字典中找到了节点
    if (node) {
        //更新节点的信息
        _lru->_totalCost -= node->_cost;
        _lru->_totalCost += cost;
        node->_cost = cost;
        node->_time = now;
        node->_value = object;
        //将节点移动至表头
        [_lru bringNodeToHead:node];
    } else {
        //如果节点不存在,就构造一个
        node = [_YYLinkedMapNode new];
        node->_cost = cost;
        node->_time = now;
        node->_key = key;
        node->_value = object;
        //将新创建的节点插在表头
        [_lru insertNodeAtHead:node];
    }
    //如果链表的总cost值大于限制值就清理
    if (_lru->_totalCost > _costLimit) {
        //异步在串行队列中执行清除操作
        dispatch_async(_queue, ^{
            [self trimToCost:_costLimit];
        });
    }
    /*
    如果链表缓存节点总数大于限制值就清理一个节点
    由于每次添加缓存对象的时候都会判断是否超出总数,所以只需要删除一个节点就能保持总数小于等于限制值
    */
    if (_lru->_totalCount > _countLimit) {
        //获取要删除的尾节点
        _YYLinkedMapNode *node = [_lru removeTailNode];
        //根据配置值获取队列
        if (_lru->_releaseAsynchronously) {
            dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
            dispatch_async(queue, ^{
                /*
                这里是一个小技巧,block捕获并持有node
                由于是异步方法,当前代码块会立刻结束,所以局部变量node会被释放,缓存节点的引用计数只有当前block有
                此时block执行完成以后就会由这个线程负责释放节点的内存,就实现了其他线程异步释放内存的操作
                */
                [node class]; //hold and release in queue
            });
        } else if (_lru->_releaseOnMainThread && !pthread_main_np()) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [node class]; //hold and release in queue
            });
        }
    }
    //释放锁
    pthread_mutex_unlock(&_lock);
}

YYMemoryCache对于线程安全没有使用GCD串行队列来实现,在第一个版本中使用的是性能最好的自旋锁,由于自旋锁存在一些bug,所以后来的版本改成了互斥锁,pthread_mutext上锁和释放锁的效率也很高,具体可以参考YYCache作者写的文章不再安全的 OSSpinLock,使用GCD队列来实现线程安全防止产生竞争条件很简单,但是效率没有互斥锁高,所以作者选择了使用互斥锁。

上面的代码是向YYMemeoryCache中添加缓存对象,代码也很简单,就是简单的链表操作,比较值得注意的就是在其他线程异步释放对象,有可能我们会觉得释放对象并不需要消耗太多的资源,但是累积起来也会产生一定的消耗了。

在看一下根据key获取缓存对象的方法:

- (id)objectForKey:(id)key {
    if (!key) return nil;
    //加互斥锁
    pthread_mutex_lock(&_lock);
    //从字典中获取key对应的缓存对象节点
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    //如果存在就更新访问时间并将节点移至表头
    if (node) {
        node->_time = CACurrentMediaTime();
        [_lru bringNodeToHead:node];
    }
    pthread_mutex_unlock(&_lock);
    return node ? node->_value : nil;
}

获取缓存对象也很简单,接下来看一下根据限制清除缓存的操作:

/*
递归函数,一种实现定时器的技巧
使用GCD 每_autoTrimInterval秒(默认5s)执行一次_trimInBackground方法
递归调用就可以实现每_autoTrimInterval秒进行一次缓存对象的清除操作
*/
- (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];
    });
}

//只看一个栗子
- (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) {
        //trylock会尝试上锁,上锁成功返回0,如果已经被占用返回一个非零值
        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 {
            //如果没有上锁成功就睡10ms重试
            usleep(10 * 1000); //10 ms
        }
    }
    //如果要删除的数组个数大于0,就根据配置异步或主线程中释放对象
    //这里的释放技巧和之前讲解的一致
    if (holder.count) {
        dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
        dispatch_async(queue, ^{
            [holder count]; // release in queue
        });
    }
}

上面的代码也很简单,使用GCD和递归函数实现了一个简单的计时器,每5ms执行一次清理操作,代码很简单不再赘述了。

到现在为止,我们已经熟悉了AFNetworking内存缓存、SDWebImage内存缓存和YYCache的内存缓存的实现,SDWebImage内存缓存其实就直接使用了NSCache

基于内存的缓存可以使用NSCacheNSMutableDictionary来实现,但使用NSCache其清除缓存的算法不是我们可控的,比如我们想要LRU淘汰算法,或者FILOFIFO等各种算法都没办法实现,此时只能自己实现,并且NSCache缓存的读写效率并不高,他帮我们做的只有自动清理缓存,所以在性能要求不高的情况下使用NSCache很合适,其实现简单,已经帮我们完成了所有的工作,我们只需要像操作字典一样操作他。

如果要实现自定义的淘汰算法,就需要自定义实现,如AFNetworking使用NSMutableDictionary实现LRU淘汰算法,其实通过对比YYCacheAFNetworkign不难发现,AFAutoPurgingImageCache的性能瓶颈有很多,值得提升的地方也有很多,YYMemeoryCache为了追求极致的性能很多地方都是直接使用C函数,直接使用Core FoundationCFMutableDictionaryRef可变字典,包括释放对象这样不怎么消耗资源的操作都尽量放在其他线程执行。所以,对于高性能需求的场景就需要我们深思实现方式。

YYDiskCache

YYCache的磁盘缓存YYDiskCache的实现相比就复杂一些了,作者在经过大量调研和实验后发现,SQLite对于数据的写入性能高于直接写文件,但是对于读性能来说需要考虑数据的大小,对于20KB以上的数据读文件的性能要高于读数据库的性能,所以,为了实现高性能的磁盘缓存,作者结合了SQLite和文件系统,将缓存数据的元数据保存在数据库中,对于大于20KB的数据存入文件系统中,读取时直接从文件系统中读取。到这里,需求基本都说完了,如果让读者自行实现相信你也能写出一个磁盘缓存。

因为结合了SQLite和文件系统,作者实现了YYKVStorage类封装了数据库和文件的操作,YYDiskCache类似于表现层,进一步封装并提供便于使用的接口,YYKVStorage不是线程安全的,所以保证线程安全的操作由YYDiskCache实现。

看一下YYKVStorage的头文件声明代码中比较重要的部分:

//缓存对象model
@interface YYKVStorageItem : NSObject

//唯一id key
@property (nonatomic, strong) NSString *key;                ///< key
//value
@property (nonatomic, strong) NSData *value;                ///< value
//保存在文件系统中的文件名,如果只保存在数据库中则为nil
@property (nullable, nonatomic, strong) NSString *filename; ///< filename (nil if inline)
//value的字节大小
@property (nonatomic) int size;                             ///< value's size in bytes
//修改时间,unix时间戳
@property (nonatomic) int modTime;                          ///< modification unix timestamp
//访问时间,unix时间戳
@property (nonatomic) int accessTime;                       ///< last access unix timestamp
//附加数据,如果没有为nil
@property (nullable, nonatomic, strong) NSData *extendedData; ///< extended data (nil if no extended data)
@end

//存储方式类型,key-value缓存对象的存储位置
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
    //只存在文件系统里
    YYKVStorageTypeFile = 0,
    //只存在数据库里
    YYKVStorageTypeSQLite = 1,
    //文件系统和数据库都存
    YYKVStorageTypeMixed = 2,
};

上面定义了一个封装的缓存对象类YYKVStorageItem,还定义了三种缓存对象存储方式,可以只使用文件系统、只使用数据库或在两者中都存储,用户可以按需选择,这个存储类型的设置方式是在初始化构造函数中指定的。

在看一下文件系统的目录结构和数据库表结构:

 File:
 /path/
      /manifest.sqlite
      /manifest.sqlite-shm
      /manifest.sqlite-wal
      /data/
           /e10adc3949ba59abbe56e057f20f883e
           /e10adc3949ba59abbe56e057f20f883e
      /trash/
            /unused_file_or_folder
 
 SQL:
 create table if not exists manifest (
    key                 text,
    filename            text,
    size                integer,
    inline_data         blob,
    modification_time   integer,
    last_access_time    integer,
    extended_data       blob,
    primary key(key)
 ); 
 create index if not exists last_access_time_idx on manifest(last_access_time);

类似于SDWebImage的磁盘缓存,文件名称使用MD5散列key获得,YYKVStorage类主要实现了文件和数据库的增删改查操作,由于篇幅问题这里就分别举一个栗子:

//SQLite插入一条数据
- (BOOL)_dbSaveWithKey:(NSString *)key value:(NSData *)value fileName:(NSString *)fileName extendedData:(NSData *)extendedData {
    NSString *sql = @"insert or replace into manifest (key, filename, size, inline_data, modification_time, last_access_time, extended_data) values (?1, ?2, ?3, ?4, ?5, ?6, ?7);";
    sqlite3_stmt *stmt = [self _dbPrepareStmt:sql];
    if (!stmt) return NO;
    
    int timestamp = (int)time(NULL);
    sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL);
    sqlite3_bind_text(stmt, 2, fileName.UTF8String, -1, NULL);
    sqlite3_bind_int(stmt, 3, (int)value.length);
    if (fileName.length == 0) {
        sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0);
    } else {
        sqlite3_bind_blob(stmt, 4, NULL, 0, 0);
    }
    sqlite3_bind_int(stmt, 5, timestamp);
    sqlite3_bind_int(stmt, 6, timestamp);
    sqlite3_bind_blob(stmt, 7, extendedData.bytes, (int)extendedData.length, 0);
    
    int result = sqlite3_step(stmt);
    if (result != SQLITE_DONE) {
        if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite insert error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
        return NO;
    }
    return YES;
}

//向文件中写数据
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData {
    if (key.length == 0 || value.length == 0) return NO;
    if (_type == YYKVStorageTypeFile && filename.length == 0) {
        return NO;
    }
    
    if (filename.length) {
        //将value数据写入文件系统中
        if (![self _fileWriteWithName:filename data:value]) {
            return NO;
        }
        //将数据插入到数据库中
        if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
            [self _fileDeleteWithName:filename];
            return NO;
        }
        return YES;
    } else {
        if (_type != YYKVStorageTypeSQLite) {
            NSString *filename = [self _dbGetFilenameWithKey:key];
            if (filename) {
                [self _fileDeleteWithName:filename];
            }
        }
        return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
    }
}

//通过key获取缓存对象
- (YYKVStorageItem *)getItemForKey:(NSString *)key {
    if (key.length == 0) return nil;
    //首先查找数据库,不获取
    YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:NO];
    if (item) {
        //更新访问
        [self _dbUpdateAccessTimeWithKey:key];
        //如果文件路径存在,就去读文件系统
        if (item.filename) {
            item.value = [self _fileReadWithName:item.filename];
            if (!item.value) {
                [self _dbDeleteItemWithKey:key];
                item = nil;
            }
        }
    }
    return item;
}

//清除缓存直到满足大小要求
- (BOOL)removeItemsToFitSize:(int)maxSize {
    if (maxSize == INT_MAX) return YES;
    if (maxSize <= 0) return [self removeAllItems];
    
    int total = [self _dbGetTotalItemSize];
    if (total < 0) return NO;
    if (total <= maxSize) return YES;
    
    NSArray *items = nil;
    BOOL suc = NO;
    do {
        int perCount = 16;
        //数据库查找并排序相关信息
        items = [self _dbGetItemSizeInfoOrderByTimeAscWithLimit:perCount];
        //遍历删除数据库,如果文件系统中有对应数据就删除掉
        for (YYKVStorageItem *item in items) {
            if (total > maxSize) {
                if (item.filename) {
                    [self _fileDeleteWithName:item.filename];
                }
                suc = [self _dbDeleteItemWithKey:item.key];
                total -= item.size;
            } else {
                break;
            }
            if (!suc) break;
        }
    } while (total > maxSize && items.count > 0 && suc);
    if (suc) [self _dbCheckpoint];
    return suc;
}

上面的代码就是操作数据库和文件系统的代码,不再赘述了,不过,从写文件的函数可以发现,如果选择保存在文件系统和数据库中,那么value即会被写入文件系统也会被存储在操作系统中,关于YYKVStorage的代码不再讲解了,读者可以自行查阅。

接下来看几个YYDiskCache中比较重要的方法:

//YYDiskCache初始化构造函数
- (instancetype)initWithPath:(NSString *)path
             inlineThreshold:(NSUInteger)threshold {
    self = [super init];
    if (!self) return nil;
    
    YYDiskCache *globalCache = _YYDiskCacheGetGlobal(path);
    if (globalCache) return globalCache;
    
    /*
    根据传入的threadhold判断缓存对象存储类型
    如果为0就所有数据存入文件中,如果为NSUIntegerMax存入数据库中
    其他值就混合存储
    */
    YYKVStorageType type;
    if (threshold == 0) {
        type = YYKVStorageTypeFile;
    } else if (threshold == NSUIntegerMax) {
        type = YYKVStorageTypeSQLite;
    } else {
        type = YYKVStorageTypeMixed;
    }
    
    //创建一个YYKVStorage对象
    YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];
    if (!kv) return nil;
    
    _kv = kv;
    _path = path;
    _lock = dispatch_semaphore_create(1);
    _queue = dispatch_queue_create("com.ibireme.cache.disk", DISPATCH_QUEUE_CONCURRENT);
    _inlineThreshold = threshold;
    _countLimit = NSUIntegerMax;
    _costLimit = NSUIntegerMax;
    _ageLimit = DBL_MAX;
    _freeDiskSpaceLimit = 0;
    _autoTrimInterval = 60;
    
    //开启递归清除缓存
    [self _trimRecursively];
    _YYDiskCacheSetGlobal(self);
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appWillBeTerminated) name:UIApplicationWillTerminateNotification object:nil];
    return self;
}

上面的代码是YYDiskCache的初始化构造函数,主要是确定存储类型、创建YYKVStorage对象并创建数据库表和文件目录,开启定时清除缓存操作。

//宏定义,使用信号量充当锁的作用
#define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER)
#define Unlock() dispatch_semaphore_signal(self->_lock)

//YYDiskCache获取缓存对象
- (id<NSCoding>)objectForKey:(NSString *)key {
    if (!key) return nil;
    //加锁,通过YYKVStorage获取key对应的数据
    Lock();
    YYKVStorageItem *item = [_kv getItemForKey:key];
    Unlock();
    if (!item.value) return nil;
    
    id object = nil;
    //如果有自定义的unarchiveBlock就执行反序列化操作
    if (_customUnarchiveBlock) {
        object = _customUnarchiveBlock(item.value);
    } else {
        @try {
            object = [NSKeyedUnarchiver unarchiveObjectWithData:item.value];
        }
        @catch (NSException *exception) {
            // nothing to do...
        }
    }
    if (object && item.extendedData) {
        [YYDiskCache setExtendedData:item.extendedData toObject:object];
    }
    return object;
}

//递归定时清除缓存
- (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];
    });
}

//调用YYKVStorage的方法删除缓存直到满足要求
- (void)_trimToCost:(NSUInteger)costLimit {
    if (costLimit >= INT_MAX) return;
    [_kv removeItemsToFitSize:(int)costLimit];
    
}

上面就是YYDiskCache如何通过YYKVStorage存储缓存对象到数据库和文件系统的大致操作。其他源码篇幅问题就不再讲述了,读者可自行阅读。

SDWebImageYYCache的磁盘缓存最大的区别就是应用场景,SDWebImage存储的都是图片,图片一般都比较大,所以直接采用文件系统能够保证读性能,YYCache作为第三方库,需要缓存任意类型的对象,所以提供了数据库和文件系统结合的方式实现,所以具体选择什么样的缓存策略需要考虑具体的应用场景。

备注

由于作者水平有限,难免出现纰漏,如有问题还请不吝赐教。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容