深入源碼理解YYCache 、SDWebImage、AFNetworking、NSCache 緩存方式與對(duì)比

深入源碼理解YYCache 、SDWebImage、AFNetworking、NSCache 緩存方式與對(duì)比

轉(zhuǎn)載請(qǐng)注明出處 http://www.itdecent.cn/p/18d9fe85266d

在之前的一篇文章iOS緩存 NSCache詳解及SDWebImage緩存策略源碼分析中詳細(xì)講解了NSCache的用法以及SDWebImage內(nèi)存和磁盤緩存的源碼分析,本篇文章將簡要講解AFNetworking緩存類和YYCache并作出對(duì)比。

由于之前的一篇文章已經(jīng)詳細(xì)講解了NSCacheSDWebImage緩存策略,本篇文章不再贅述,會(huì)簡要介紹一下AFNetworkingYYCache的源碼。

AFNetworking圖片緩存AFAutoPurgingImageCache

AFNetworking也提供了同SDWebImage一樣的下載圖片的功能,也提供了緩存這些圖片的功能,但它只提供了內(nèi)存緩存,沒有提供磁盤緩存功能。

看一下頭文件:

//緩存協(xié)議,如果用戶需要實(shí)現(xiàn)自定義的
@protocol AFImageCache <NSObject>

//添加圖片并傳遞一個(gè)唯一id,一般使用圖片的URL
- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier;

//刪除id為identifier的圖片
- (BOOL)removeImageWithIdentifier:(NSString *)identifier;

//刪除所有緩存圖片
- (BOOL)removeAllImages;

//根據(jù)id獲取圖片
- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier;
@end

//AFImageRequestCache協(xié)議,繼承AFImageCache協(xié)議
@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;

//根據(jù)下載圖片的request和額外的id獲取圖片
- (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;

@end

//AFNetworking提供的緩存類,
@interface AFAutoPurgingImageCache : NSObject <AFImageRequestCache>

//單位是字節(jié),能夠支持最大緩存大小
@property (nonatomic, assign) UInt64 memoryCapacity;

//建議的當(dāng)內(nèi)存緩存要釋放的時(shí)候需要釋放到多大的大小
@property (nonatomic, assign) UInt64 preferredMemoryUsageAfterPurge;

//當(dāng)前內(nèi)存緩存占用的字節(jié)大小
@property (nonatomic, assign, readonly) UInt64 memoryUsage;

//構(gòu)造函數(shù)
- (instancetype)init;

//構(gòu)造函數(shù)
- (instancetype)initWithMemoryCapacity:(UInt64)memoryCapacity preferredMemoryCapacity:(UInt64)preferredMemoryCapacity;

@end

從頭文件可以看出,AFNetworking提供的AFAutoPurgingImageCache接口不多,而且都很簡單,只實(shí)現(xiàn)了內(nèi)存緩存的功能。

看一下實(shí)現(xiàn)文件:

//緩存對(duì)象包裝類
@interface AFCachedImage : NSObject
//緩存的圖片
@property (nonatomic, strong) UIImage *image;
//id
@property (nonatomic, strong) NSString *identifier;
//圖片字節(jié)大小
@property (nonatomic, assign) UInt64 totalBytes;
//淘汰算法是LRU所以需要記錄上次訪問時(shí)間
@property (nonatomic, strong) NSDate *lastAccessDate;
//沒用到的屬性
@property (nonatomic, assign) UInt64 currentMemoryUsage;

@end

@implementation AFCachedImage
//初始化構(gòu)函數(shù)
-(instancetype)initWithImage:(UIImage *)image identifier:(NSString *)identifier {
    if (self = [self init]) {
        self.image = image;
        self.identifier = identifier;
        //計(jì)算圖片的字節(jié)大小,每個(gè)像素占4字節(jié)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;
}
//通過緩存對(duì)象獲取圖片時(shí)要更新上次訪問時(shí)間為當(dāng)前時(shí)間
- (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 ()
//可變字典用于存儲(chǔ)所有的緩存對(duì)象AFCachedImage對(duì)象,key為字符串類型
@property (nonatomic, strong) NSMutableDictionary <NSString* , AFCachedImage*> *cachedImages;
//當(dāng)前緩存對(duì)象內(nèi)存占用大小
@property (nonatomic, assign) UInt64 currentMemoryUsage;
//用于線程安全防止產(chǎn)生競(jìng)爭條件,沒有用鎖
@property (nonatomic, strong) dispatch_queue_t synchronizationQueue;
@end

@implementation AFAutoPurgingImageCache

//構(gòu)造函數(shù),默認(rèn)內(nèi)存占用100M,每次清除緩存到60M
- (instancetype)init {
    return [self initWithMemoryCapacity:100 * 1024 * 1024 preferredMemoryCapacity:60 * 1024 * 1024];
}

//構(gòu)造函數(shù)
- (instancetype)initWithMemoryCapacity:(UInt64)memoryCapacity preferredMemoryCapacity:(UInt64)preferredMemoryCapacity {
    if (self = [super init]) {
        self.memoryCapacity = memoryCapacity;
        self.preferredMemoryUsageAfterPurge = preferredMemoryCapacity;
        self.cachedImages = [[NSMutableDictionary alloc] init];
        //創(chuàng)建一個(gè)并行隊(duì)列,但后面使用時(shí)都是在同步情況或barrier情況下,隊(duì)列中的任務(wù)還是以串行執(zhí)行
        //可以防止產(chǎn)生競(jìng)爭條件,保證線程安全
        NSString *queueName = [NSString stringWithFormat:@"com.alamofire.autopurgingimagecache-%@", [[NSUUID UUID] UUIDString]];
        self.synchronizationQueue = dispatch_queue_create([queueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);

        //添加通知,監(jiān)聽收到系統(tǒng)的內(nèi)存警告后刪除所有緩存對(duì)象
        [[NSNotificationCenter defaultCenter]
         addObserver:self
         selector:@selector(removeAllImages)
         name:UIApplicationDidReceiveMemoryWarningNotification
         object:nil];

    }
    return self;
}
//析構(gòu)函數(shù),刪除通知的監(jiān)聽
- (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不阻塞當(dāng)前線程,不阻塞向隊(duì)列中添加任務(wù)
    //但隊(duì)列中其他任務(wù)要執(zhí)行就必須等待前一個(gè)任務(wù)結(jié)束,不管是不是并發(fā)隊(duì)列
    dispatch_barrier_async(self.synchronizationQueue, ^{
        //創(chuàng)建AFCachedImage對(duì)象
        AFCachedImage *cacheImage = [[AFCachedImage alloc] initWithImage:image identifier:identifier];
        //判斷對(duì)應(yīng)id是否已經(jīng)保存在緩存字典中了
        AFCachedImage *previousCachedImage = self.cachedImages[identifier];
        //如果已經(jīng)保存了減去占用的內(nèi)存大小
        if (previousCachedImage != nil) {
            self.currentMemoryUsage -= previousCachedImage.totalBytes;
        }
        //更新字典,更新占用內(nèi)存大小
        self.cachedImages[identifier] = cacheImage;
        self.currentMemoryUsage += cacheImage.totalBytes;
    });
    //同上,該block必須等待前一個(gè)block執(zhí)行完成才可以執(zhí)行
    dispatch_barrier_async(self.synchronizationQueue, ^{
        //判斷當(dāng)前占用內(nèi)存大小是否超過了設(shè)置的內(nèi)存緩存總大小
        if (self.currentMemoryUsage > self.memoryCapacity) {
            //計(jì)算需要釋放多少空間
            UInt64 bytesToPurge = self.currentMemoryUsage - self.preferredMemoryUsageAfterPurge;
            //把緩存字典中的所有緩存對(duì)象取出
            NSMutableArray <AFCachedImage*> *sortedImages = [NSMutableArray arrayWithArray:self.cachedImages.allValues];
           //設(shè)置排序描述器,按照上次訪問時(shí)間排序
            NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastAccessDate"
                                                                           ascending:YES];
            //排序取出的所有緩存對(duì)象
            [sortedImages sortUsingDescriptors:@[sortDescriptor]];

            UInt64 bytesPurged = 0;
            //遍歷,釋放緩存對(duì)象,滿足要求后break
            for (AFCachedImage *cachedImage in sortedImages) {
                [self.cachedImages removeObjectForKey:cachedImage.identifier];
                bytesPurged += cachedImage.totalBytes;
                if (bytesPurged >= bytesToPurge) {
                    break ;
                }
            }
            //更新當(dāng)前占用緩存大小
            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;
}

//根據(jù)id獲取圖片
- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier {
    __block UIImage *image = nil;
    dispatch_sync(self.synchronizationQueue, ^{
        AFCachedImage *cachedImage = self.cachedImages[identifier];
        //更新訪問時(shí)間
        image = [cachedImage accessImage];
    });
    return image;
}
//AFImageRequestCache協(xié)議的方法,通過request構(gòu)造一個(gè)key然后調(diào)用前面的方法
- (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構(gòu)造一個(gè)key
- (NSString *)imageCacheKeyFromURLRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)additionalIdentifier {
    //取圖片的URI然后追加額外的id構(gòu)造key
    NSString *key = request.URL.absoluteString;
    if (additionalIdentifier != nil) {
        key = [key stringByAppendingString:additionalIdentifier];
    }
    return key;
}

@end

AFAutoPurgingImageCache的實(shí)現(xiàn)很簡單,邏輯也都很簡單,不再贅述了。它的淘汰算法采用的是LRU,從源碼中其實(shí)也可以看出缺點(diǎn)挺多的,比如上次訪問時(shí)間使用NSDate類,使用UNIX時(shí)間戳應(yīng)該更好,不僅內(nèi)存占用小排序也更快吧。淘汰緩存時(shí)需要從緩存字典中取出所有的緩存對(duì)象然后根據(jù)NSDate排序,如果有大量緩存圖片,這里似乎就是一個(gè)性能瓶頸,但它的優(yōu)點(diǎn)就是實(shí)現(xiàn)簡單明了,對(duì)于性能要求不高的程序選擇這個(gè)也沒有太多影響。

YYCache的內(nèi)存緩存和磁盤緩存

本節(jié)文章將講解YYCache的內(nèi)存緩存YYMemoryCache和磁盤緩存YYDiskCache,但源碼較多,而且本文的篇幅有限,所以不再和之前的文章一樣貼所有的源碼來講解,這里只會(huì)貼一些比較重要的代碼進(jìn)行講解,需要深入研究的讀者還是要自己看一下完整的源碼。

YYMemoryCache講解

貼一下頭文件的接口代碼:

@interface YYMemoryCache : NSObject

//內(nèi)存
@property (nullable, copy) NSString *name;
//當(dāng)前緩存對(duì)象的個(gè)數(shù)
@property (readonly) NSUInteger totalCount;
//當(dāng)前緩存對(duì)象的總cost數(shù)
@property (readonly) NSUInteger totalCost;

//同NSCache,支持緩存多少個(gè)對(duì)象
@property NSUInteger countLimit;
//同NSCache,每個(gè)緩存對(duì)象可以設(shè)置一個(gè)cost,這個(gè)值就標(biāo)識(shí)支持緩存的最大cost總量
@property NSUInteger costLimit;
//緩存的過期時(shí)間,單位是秒
@property NSTimeInterval ageLimit;

//自動(dòng)清理緩存的時(shí)間間隔,默認(rèn)是5秒
@property NSTimeInterval autoTrimInterval;

//是否在程序進(jìn)入后臺(tái)后刪除所有的緩存對(duì)象
@property BOOL shouldRemoveAllObjectsWhenEnteringBackground;

//收到系統(tǒng)內(nèi)存警告后執(zhí)行的回調(diào)塊
@property (nullable, copy) void(^didReceiveMemoryWarningBlock)(YYMemoryCache *cache);

//程序進(jìn)入后臺(tái)后執(zhí)行的回調(diào)塊
@property (nullable, copy) void(^didEnterBackgroundBlock)(YYMemoryCache *cache);

//是否在主線程釋放對(duì)象,如果有UIView這樣的UI對(duì)象需要在主線程中釋放
@property BOOL releaseOnMainThread;

//是否異步釋放,默認(rèn)是YES
@property BOOL releaseAsynchronously;

//key的緩存對(duì)象是否存在
- (BOOL)containsObjectForKey:(id)key;

//根據(jù)key獲取緩存對(duì)象
- (nullable id)objectForKey:(id)key;

//設(shè)置key的緩存對(duì)象
- (void)setObject:(nullable id)object forKey:(id)key;

//設(shè)置key的緩存對(duì)象并設(shè)置其cost值
- (void)setObject:(nullable id)object forKey:(id)key withCost:(NSUInteger)cost;

//刪除key的緩存對(duì)象
- (void)removeObjectForKey:(id)key;

//刪除所有緩存對(duì)象
- (void)removeAllObjects;

//清除緩存到緩存對(duì)象只有count個(gè)
- (void)trimToCount:(NSUInteger)count;

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

//清除超過age的過期緩存對(duì)象
- (void)trimToAge:(NSTimeInterval)age;

@end

YYMemeoryCache提供的接口和NSCache類似,提供緩存對(duì)象個(gè)數(shù)限制、緩存對(duì)象占用cost限制和緩存對(duì)象過期限制,實(shí)現(xiàn)緩存對(duì)象的清理。YYMemeoryCacheAFAutoPurgingImageCache不同,他使用鏈表來管理緩存對(duì)象,同樣也使用LRU淘汰算法來清除緩存對(duì)象。

首先看一下鏈表節(jié)點(diǎn)定義:

//這是一個(gè)雙向鏈表,保存prev和next指針
@interface _YYLinkedMapNode : NSObject {
    @package
    //__unsafe_unretained標(biāo)識(shí)不保留其他節(jié)點(diǎn)對(duì)象,提高效率
    __unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
    __unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
    //記錄key、value、cost和訪問時(shí)間,使用unix時(shí)間戳
    id _key;
    id _value;
    NSUInteger _cost;
    NSTimeInterval _time;
}
@end

@implementation _YYLinkedMapNode
@end

上面的代碼就是雙向鏈表的節(jié)點(diǎn),其實(shí)和AFAutoPurgingImageCache封裝的緩存對(duì)象一樣,不過YYMemeoryCache支持任意類型的緩存對(duì)象而不限于是圖片,YYMemeoryCache性能和效率非常高,通過上面的定義也可以看出,能夠提高效率的地方一定會(huì)用最好的方式實(shí)現(xiàn),由于是YYMemeoryCache管理緩存對(duì)象的節(jié)點(diǎn),所以能夠保證在生命周期內(nèi)不會(huì)被釋放,所以使用__unsafe_unretained修飾,不會(huì)產(chǎn)生任何問題。

再看一下鏈表的定義:

@interface _YYLinkedMap : NSObject {
    @package
    //使用Core Foundation的CFMutableDictionaryRef可變字典保存上面的節(jié)點(diǎn)對(duì)象
    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
    //是否在主線程中釋放緩存對(duì)象
    BOOL _releaseOnMainThread;
    //是否異步釋放緩存對(duì)象
    BOOL _releaseAsynchronously;
}

//雙向鏈表在表頭插入節(jié)點(diǎn)
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;

//將節(jié)點(diǎn)移到表頭,LRU算法,訪問一個(gè)緩存對(duì)象后就將這個(gè)對(duì)象移動(dòng)到表頭
//不需要向AFNetworking一樣記錄NSDate然后更新排序
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;

//刪除一個(gè)節(jié)點(diǎn)
- (void)removeNode:(_YYLinkedMapNode *)node;

//刪除尾節(jié)點(diǎn)
- (_YYLinkedMapNode *)removeTailNode;

//刪除所有的節(jié)點(diǎn)
- (void)removeAll;

@end

上面是鏈表的定義,它使用Core FoundationCFMutableDictionaryRef來保存節(jié)點(diǎn)對(duì)象,并維護(hù)一個(gè)雙向鏈表,記錄了一些需要使用的參數(shù)。也就是說YYMemeoryCache會(huì)通過構(gòu)造一個(gè)_YYLinkedMapNode對(duì)象來封裝需要緩存的對(duì)象,然后使用_YYLinkedMap對(duì)象來維護(hù)和管理這個(gè)雙向鏈表,由于篇幅問題不列舉所有的實(shí)現(xiàn)方法了,這些方法都是常規(guī)的鏈表操作,讀者可自行閱讀,舉一個(gè)源碼:

//將一個(gè)節(jié)點(diǎn)移動(dòng)到表頭,由于使用LRU淘汰算法
//所以當(dāng)我們?cè)L問一個(gè)緩存對(duì)象時(shí)就會(huì)調(diào)用這個(gè)方法將封裝緩存對(duì)象的節(jié)點(diǎn)移動(dòng)到表頭
- (void)bringNodeToHead:(_YYLinkedMapNode *)node {
    //判斷節(jié)點(diǎn)是頭節(jié)點(diǎn)、尾節(jié)點(diǎn)或普通節(jié)點(diǎn),然后修改指針指向
    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;
}

其他方法就不再贅述了,講到這里再結(jié)合之前AFNetworking的緩存源碼,相信大家也可以自己寫出添加緩存、刪除緩存的代碼了。接下來看一下YYMemeoryCache如何添加緩存對(duì)象的:

//添加緩存對(duì)象
- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
    //key為空直接返回
    if (!key) return;
    //要緩存的對(duì)象object不存在就刪除對(duì)應(yīng)key的已經(jīng)緩存的對(duì)象
    if (!object) {
        [self removeObjectForKey:key];
        return;
    }
    //使用pthread_mutext互斥鎖,防止產(chǎn)生競(jìng)爭條件保障線程安全
    pthread_mutex_lock(&_lock);
    //首先從存儲(chǔ)緩存對(duì)象的字典中獲取緩存節(jié)點(diǎn)
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    //獲取一個(gè)時(shí)間
    NSTimeInterval now = CACurrentMediaTime();
    //如果從緩存字典中找到了節(jié)點(diǎn)
    if (node) {
        //更新節(jié)點(diǎn)的信息
        _lru->_totalCost -= node->_cost;
        _lru->_totalCost += cost;
        node->_cost = cost;
        node->_time = now;
        node->_value = object;
        //將節(jié)點(diǎn)移動(dòng)至表頭
        [_lru bringNodeToHead:node];
    } else {
        //如果節(jié)點(diǎn)不存在,就構(gòu)造一個(gè)
        node = [_YYLinkedMapNode new];
        node->_cost = cost;
        node->_time = now;
        node->_key = key;
        node->_value = object;
        //將新創(chuàng)建的節(jié)點(diǎn)插在表頭
        [_lru insertNodeAtHead:node];
    }
    //如果鏈表的總cost值大于限制值就清理
    if (_lru->_totalCost > _costLimit) {
        //異步在串行隊(duì)列中執(zhí)行清除操作
        dispatch_async(_queue, ^{
            [self trimToCost:_costLimit];
        });
    }
    /*
    如果鏈表緩存節(jié)點(diǎn)總數(shù)大于限制值就清理一個(gè)節(jié)點(diǎn)
    由于每次添加緩存對(duì)象的時(shí)候都會(huì)判斷是否超出總數(shù),所以只需要?jiǎng)h除一個(gè)節(jié)點(diǎn)就能保持總數(shù)小于等于限制值
    */
    if (_lru->_totalCount > _countLimit) {
        //獲取要?jiǎng)h除的尾節(jié)點(diǎn)
        _YYLinkedMapNode *node = [_lru removeTailNode];
        //根據(jù)配置值獲取隊(duì)列
        if (_lru->_releaseAsynchronously) {
            dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
            dispatch_async(queue, ^{
                /*
                這里是一個(gè)小技巧,block捕獲并持有node
                由于是異步方法,當(dāng)前代碼塊會(huì)立刻結(jié)束,所以局部變量node會(huì)被釋放,緩存節(jié)點(diǎn)的引用計(jì)數(shù)只有當(dāng)前block有
                此時(shí)block執(zhí)行完成以后就會(huì)由這個(gè)線程負(fù)責(zé)釋放節(jié)點(diǎn)的內(nèi)存,就實(shí)現(xiàn)了其他線程異步釋放內(nèi)存的操作
                */
                [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對(duì)于線程安全沒有使用GCD串行隊(duì)列來實(shí)現(xiàn),在第一個(gè)版本中使用的是性能最好的自旋鎖,由于自旋鎖存在一些bug,所以后來的版本改成了互斥鎖,pthread_mutext上鎖和釋放鎖的效率也很高,具體可以參考YYCache作者寫的文章不再安全的 OSSpinLock,使用GCD隊(duì)列來實(shí)現(xiàn)線程安全防止產(chǎn)生競(jìng)爭條件很簡單,但是效率沒有互斥鎖高,所以作者選擇了使用互斥鎖。

上面的代碼是向YYMemeoryCache中添加緩存對(duì)象,代碼也很簡單,就是簡單的鏈表操作,比較值得注意的就是在其他線程異步釋放對(duì)象,有可能我們會(huì)覺得釋放對(duì)象并不需要消耗太多的資源,但是累積起來也會(huì)產(chǎn)生一定的消耗了。

在看一下根據(jù)key獲取緩存對(duì)象的方法:

- (id)objectForKey:(id)key {
    if (!key) return nil;
    //加互斥鎖
    pthread_mutex_lock(&_lock);
    //從字典中獲取key對(duì)應(yīng)的緩存對(duì)象節(jié)點(diǎn)
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    //如果存在就更新訪問時(shí)間并將節(jié)點(diǎn)移至表頭
    if (node) {
        node->_time = CACurrentMediaTime();
        [_lru bringNodeToHead:node];
    }
    pthread_mutex_unlock(&_lock);
    return node ? node->_value : nil;
}

獲取緩存對(duì)象也很簡單,接下來看一下根據(jù)限制清除緩存的操作:

/*
遞歸函數(shù),一種實(shí)現(xiàn)定時(shí)器的技巧
使用GCD 每_autoTrimInterval秒(默認(rèn)5s)執(zhí)行一次_trimInBackground方法
遞歸調(diào)用就可以實(shí)現(xiàn)每_autoTrimInterval秒進(jìn)行一次緩存對(duì)象的清除操作
*/
- (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];
    });
}

//調(diào)用三種清除策略
- (void)_trimInBackground {
    dispatch_async(_queue, ^{
        [self _trimToCost:self->_costLimit];
        [self _trimToCount:self->_countLimit];
        [self _trimToAge:self->_ageLimit];
    });
}

//只看一個(gè)栗子
- (void)_trimToCost:(NSUInteger)costLimit {
    BOOL finish = NO;
    //加鎖,并判斷是否已經(jīng)滿足限制的要求
    pthread_mutex_lock(&_lock);
    if (costLimit == 0) {
        [_lru removeAll];
        finish = YES;
    } else if (_lru->_totalCost <= costLimit) {
        finish = YES;
    }
    //釋放所
    pthread_mutex_unlock(&_lock);
    //如果已經(jīng)滿足要求就直接返回,不需要做清除操作
    if (finish) return;
    //定義一個(gè)數(shù)組用于存儲(chǔ)要?jiǎng)h除的緩存節(jié)點(diǎn)對(duì)象
    NSMutableArray *holder = [NSMutableArray new];
    //循環(huán)
    while (!finish) {
        //trylock會(huì)嘗試上鎖,上鎖成功返回0,如果已經(jīng)被占用返回一個(gè)非零值
        if (pthread_mutex_trylock(&_lock) == 0) {
                //判斷是否滿足限制要求
            if (_lru->_totalCost > costLimit) {
                //刪除尾節(jié)點(diǎn),加入到要?jiǎng)h除的數(shù)組中
                _YYLinkedMapNode *node = [_lru removeTailNode];
                if (node) [holder addObject:node];
            } else {
                finish = YES;
            }
            pthread_mutex_unlock(&_lock);
        } else {
            //如果沒有上鎖成功就睡10ms重試
            usleep(10 * 1000); //10 ms
        }
    }
    //如果要?jiǎng)h除的數(shù)組個(gè)數(shù)大于0,就根據(jù)配置異步或主線程中釋放對(duì)象
    //這里的釋放技巧和之前講解的一致
    if (holder.count) {
        dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
        dispatch_async(queue, ^{
            [holder count]; // release in queue
        });
    }
}

上面的代碼也很簡單,使用GCD和遞歸函數(shù)實(shí)現(xiàn)了一個(gè)簡單的計(jì)時(shí)器,每5ms執(zhí)行一次清理操作,代碼很簡單不再贅述了。

到現(xiàn)在為止,我們已經(jīng)熟悉了AFNetworking內(nèi)存緩存、SDWebImage內(nèi)存緩存和YYCache的內(nèi)存緩存的實(shí)現(xiàn),SDWebImage內(nèi)存緩存其實(shí)就直接使用了NSCache。

基于內(nèi)存的緩存可以使用NSCacheNSMutableDictionary來實(shí)現(xiàn),但使用NSCache其清除緩存的算法不是我們可控的,比如我們想要LRU淘汰算法,或者FILO、FIFO等各種算法都沒辦法實(shí)現(xiàn),此時(shí)只能自己實(shí)現(xiàn),并且NSCache緩存的讀寫效率并不高,他幫我們做的只有自動(dòng)清理緩存,所以在性能要求不高的情況下使用NSCache很合適,其實(shí)現(xiàn)簡單,已經(jīng)幫我們完成了所有的工作,我們只需要像操作字典一樣操作他。

如果要實(shí)現(xiàn)自定義的淘汰算法,就需要自定義實(shí)現(xiàn),如AFNetworking使用NSMutableDictionary實(shí)現(xiàn)LRU淘汰算法,其實(shí)通過對(duì)比YYCacheAFNetworkign不難發(fā)現(xiàn),AFAutoPurgingImageCache的性能瓶頸有很多,值得提升的地方也有很多,YYMemeoryCache為了追求極致的性能很多地方都是直接使用C函數(shù),直接使用Core FoundationCFMutableDictionaryRef可變字典,包括釋放對(duì)象這樣不怎么消耗資源的操作都盡量放在其他線程執(zhí)行。所以,對(duì)于高性能需求的場(chǎng)景就需要我們深思實(shí)現(xiàn)方式。

YYDiskCache

YYCache的磁盤緩存YYDiskCache的實(shí)現(xiàn)相比就復(fù)雜一些了,作者在經(jīng)過大量調(diào)研和實(shí)驗(yàn)后發(fā)現(xiàn),SQLite對(duì)于數(shù)據(jù)的寫入性能高于直接寫文件,但是對(duì)于讀性能來說需要考慮數(shù)據(jù)的大小,對(duì)于20KB以上的數(shù)據(jù)讀文件的性能要高于讀數(shù)據(jù)庫的性能,所以,為了實(shí)現(xiàn)高性能的磁盤緩存,作者結(jié)合了SQLite和文件系統(tǒng),將緩存數(shù)據(jù)的元數(shù)據(jù)保存在數(shù)據(jù)庫中,對(duì)于大于20KB的數(shù)據(jù)存入文件系統(tǒng)中,讀取時(shí)直接從文件系統(tǒng)中讀取。到這里,需求基本都說完了,如果讓讀者自行實(shí)現(xiàn)相信你也能寫出一個(gè)磁盤緩存。

因?yàn)榻Y(jié)合了SQLite和文件系統(tǒng),作者實(shí)現(xiàn)了YYKVStorage類封裝了數(shù)據(jù)庫和文件的操作,YYDiskCache類似于表現(xiàn)層,進(jìn)一步封裝并提供便于使用的接口,YYKVStorage不是線程安全的,所以保證線程安全的操作由YYDiskCache實(shí)現(xiàn)。

看一下YYKVStorage的頭文件聲明代碼中比較重要的部分:

//緩存對(duì)象model
@interface YYKVStorageItem : NSObject

//唯一id key
@property (nonatomic, strong) NSString *key;                ///< key
//value
@property (nonatomic, strong) NSData *value;                ///< value
//保存在文件系統(tǒng)中的文件名,如果只保存在數(shù)據(jù)庫中則為nil
@property (nullable, nonatomic, strong) NSString *filename; ///< filename (nil if inline)
//value的字節(jié)大小
@property (nonatomic) int size;                             ///< value's size in bytes
//修改時(shí)間,unix時(shí)間戳
@property (nonatomic) int modTime;                          ///< modification unix timestamp
//訪問時(shí)間,unix時(shí)間戳
@property (nonatomic) int accessTime;                       ///< last access unix timestamp
//附加數(shù)據(jù),如果沒有為nil
@property (nullable, nonatomic, strong) NSData *extendedData; ///< extended data (nil if no extended data)
@end

//存儲(chǔ)方式類型,key-value緩存對(duì)象的存儲(chǔ)位置
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
    //只存在文件系統(tǒng)里
    YYKVStorageTypeFile = 0,
    //只存在數(shù)據(jù)庫里
    YYKVStorageTypeSQLite = 1,
    //文件系統(tǒng)和數(shù)據(jù)庫都存
    YYKVStorageTypeMixed = 2,
};

上面定義了一個(gè)封裝的緩存對(duì)象類YYKVStorageItem,還定義了三種緩存對(duì)象存儲(chǔ)方式,可以只使用文件系統(tǒng)、只使用數(shù)據(jù)庫或在兩者中都存儲(chǔ),用戶可以按需選擇,這個(gè)存儲(chǔ)類型的設(shè)置方式是在初始化構(gòu)造函數(shù)中指定的。

在看一下文件系統(tǒng)的目錄結(jié)構(gòu)和數(shù)據(jù)庫表結(jié)構(gòu):

 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類主要實(shí)現(xiàn)了文件和數(shù)據(jù)庫的增刪改查操作,由于篇幅問題這里就分別舉一個(gè)栗子:

//SQLite插入一條數(shù)據(jù)
- (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;
}

//向文件中寫數(shù)據(jù)
- (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數(shù)據(jù)寫入文件系統(tǒng)中
        if (![self _fileWriteWithName:filename data:value]) {
            return NO;
        }
        //將數(shù)據(jù)插入到數(shù)據(jù)庫中
        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獲取緩存對(duì)象
- (YYKVStorageItem *)getItemForKey:(NSString *)key {
    if (key.length == 0) return nil;
    //首先查找數(shù)據(jù)庫,不獲取
    YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:NO];
    if (item) {
        //更新訪問
        [self _dbUpdateAccessTimeWithKey:key];
        //如果文件路徑存在,就去讀文件系統(tǒng)
        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;
        //數(shù)據(jù)庫查找并排序相關(guān)信息
        items = [self _dbGetItemSizeInfoOrderByTimeAscWithLimit:perCount];
        //遍歷刪除數(shù)據(jù)庫,如果文件系統(tǒng)中有對(duì)應(yīng)數(shù)據(jù)就刪除掉
        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;
}

上面的代碼就是操作數(shù)據(jù)庫和文件系統(tǒng)的代碼,不再贅述了,不過,從寫文件的函數(shù)可以發(fā)現(xiàn),如果選擇保存在文件系統(tǒng)和數(shù)據(jù)庫中,那么value即會(huì)被寫入文件系統(tǒng)也會(huì)被存儲(chǔ)在操作系統(tǒng)中,關(guān)于YYKVStorage的代碼不再講解了,讀者可以自行查閱。

接下來看幾個(gè)YYDiskCache中比較重要的方法:

//YYDiskCache初始化構(gòu)造函數(shù)
- (instancetype)initWithPath:(NSString *)path
             inlineThreshold:(NSUInteger)threshold {
    self = [super init];
    if (!self) return nil;
    
    YYDiskCache *globalCache = _YYDiskCacheGetGlobal(path);
    if (globalCache) return globalCache;
    
    /*
    根據(jù)傳入的threadhold判斷緩存對(duì)象存儲(chǔ)類型
    如果為0就所有數(shù)據(jù)存入文件中,如果為NSUIntegerMax存入數(shù)據(jù)庫中
    其他值就混合存儲(chǔ)
    */
    YYKVStorageType type;
    if (threshold == 0) {
        type = YYKVStorageTypeFile;
    } else if (threshold == NSUIntegerMax) {
        type = YYKVStorageTypeSQLite;
    } else {
        type = YYKVStorageTypeMixed;
    }
    
    //創(chuàng)建一個(gè)YYKVStorage對(duì)象
    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的初始化構(gòu)造函數(shù),主要是確定存儲(chǔ)類型、創(chuàng)建YYKVStorage對(duì)象并創(chuàng)建數(shù)據(jù)庫表和文件目錄,開啟定時(shí)清除緩存操作。

//宏定義,使用信號(hào)量充當(dāng)鎖的作用
#define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER)
#define Unlock() dispatch_semaphore_signal(self->_lock)

//YYDiskCache獲取緩存對(duì)象
- (id<NSCoding>)objectForKey:(NSString *)key {
    if (!key) return nil;
    //加鎖,通過YYKVStorage獲取key對(duì)應(yīng)的數(shù)據(jù)
    Lock();
    YYKVStorageItem *item = [_kv getItemForKey:key];
    Unlock();
    if (!item.value) return nil;
    
    id object = nil;
    //如果有自定義的unarchiveBlock就執(zhí)行反序列化操作
    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;
}

//遞歸定時(shí)清除緩存
- (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];
    });
}

//調(diào)用YYKVStorage的方法刪除緩存直到滿足要求
- (void)_trimToCost:(NSUInteger)costLimit {
    if (costLimit >= INT_MAX) return;
    [_kv removeItemsToFitSize:(int)costLimit];
    
}

上面就是YYDiskCache如何通過YYKVStorage存儲(chǔ)緩存對(duì)象到數(shù)據(jù)庫和文件系統(tǒng)的大致操作。其他源碼篇幅問題就不再講述了,讀者可自行閱讀。

SDWebImageYYCache的磁盤緩存最大的區(qū)別就是應(yīng)用場(chǎng)景,SDWebImage存儲(chǔ)的都是圖片,圖片一般都比較大,所以直接采用文件系統(tǒng)能夠保證讀性能,YYCache作為第三方庫,需要緩存任意類型的對(duì)象,所以提供了數(shù)據(jù)庫和文件系統(tǒng)結(jié)合的方式實(shí)現(xiàn),所以具體選擇什么樣的緩存策略需要考慮具體的應(yīng)用場(chǎng)景。

備注

由于作者水平有限,難免出現(xiàn)紕漏,如有問題還請(qǐng)不吝賜教。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容