PINCache-源碼分析與仿寫(四)

前言

閱讀優(yōu)秀的開源項目是提高編程能力的有效手段,我們能夠從中開拓思維、拓寬視野,學習到很多不同的設(shè)計思想以及最佳實踐。閱讀他人代碼很重要,但動手仿寫、練習卻也是很有必要的,它能進一步加深我們對項目的理解,將這些東西內(nèi)化為自己的知識和能力。然而真正做起來卻很不容易,開源項目閱讀起來還是比較困難,需要一些技術(shù)基礎(chǔ)和耐心。
本系列將對一些著名的iOS開源類庫進行深入閱讀及分析,并仿寫這些類庫的基本實現(xiàn),加深我們對底層實現(xiàn)的理解和認識,提升我們iOS開發(fā)的編程技能。

PINCache

PINCache是線程安全的鍵值對緩存框架,用于緩存一些臨時數(shù)據(jù)或需要頻繁加載的數(shù)據(jù),比如某些下載的數(shù)據(jù)或一些臨時處理結(jié)果。它是在Tumblr 宣布不在維護 TMCache 后,由 Pinterest 維護和改進的一個緩存框架。它基于GCD支持多線程存取緩存數(shù)據(jù)。PINCache由兩個部分構(gòu)成,一個是內(nèi)存緩存(PINMemoryCache),另一個是硬盤緩存(PINDiskCache)。如果使用內(nèi)存緩存,當APP接收到內(nèi)存警告或進入后臺,PINCache將清理所有的內(nèi)存緩存。
PINCache的地址:https://github.com/pinterest/PINCache

實現(xiàn)原理

PINCache使用鍵/值設(shè)計存儲緩存數(shù)據(jù)。在內(nèi)存緩存PINMemoryCache中,使用字典來存儲緩存數(shù)據(jù),一般使用多個字典配合管理數(shù)據(jù)各項信息,其中一個存儲緩存內(nèi)容,一個存儲緩存創(chuàng)建日期,一個存儲緩存緩存大小以及其他的緩存信息。對于磁盤緩存PINDiskCache,緩存數(shù)據(jù)存儲到文件系統(tǒng)中,使用字典保存數(shù)據(jù)的其他信息,如文件修改日期、文件大小等。
PINCache使用異步方式存取緩存數(shù)據(jù)。在PINCache中,常見的操作如get、set、remove,都會把操作任務(wù)放到自定義的并行隊列中。操作任務(wù)異步執(zhí)行,執(zhí)行結(jié)果通過block回調(diào)到上層。為了避免資源爭奪問題,PINCache給數(shù)據(jù)操作加鎖,保證多線程安全。

仿寫PINCache

理解了PINCache的實現(xiàn)原理,我們動手模仿寫一個緩存框架demo,以加深對PINCache的理解,掌握它的設(shè)計思想和實現(xiàn)過程。
在這個demo中,我們簡化了大部分工作,只實現(xiàn)基本的功能,包括緩存的存儲、讀取和刪除功能。如需要了解更詳細的內(nèi)容請看PINCache源碼。
首先,創(chuàng)建一個項目,設(shè)置如下:

創(chuàng)建項目
項目設(shè)置

仿照PINCache創(chuàng)建緩存實現(xiàn)類,ZCJCache對外提供緩存存取的接口,ZCJMemoryCache是內(nèi)存緩存實現(xiàn)類,ZCJDiskCache是磁盤緩存實現(xiàn)類。

類結(jié)構(gòu)

ZCJCache

定義ZCJCache對外的接口,包括set、get、remove緩存數(shù)據(jù),單例方法以及只允許帶名字的初始化方法:
@interface ZCJCache : NSObject

+(instancetype)sharedInstance;

- (instancetype)initWithName:(NSString *)name;

//標記init方法不可用
-(instancetype)init UNAVAILABLE_ATTRIBUTE;
+(instancetype)new UNAVAILABLE_ATTRIBUTE;

//根據(jù)key異步取緩存數(shù)據(jù)
- (void)objectForKey:(NSString *)key block:(ZCJCacheObjectBlock)block;

//異步存儲緩存數(shù)據(jù)
-(void)setObject:(id)object forKey:(NSString *)key block:(ZCJCacheObjectBlock)block;

//刪除緩存數(shù)據(jù)
-(void)removeObjectForKey:(NSString *)key;

@end

ZCJCache類和屬性的初始化,其中currentQueue是自定義的并行隊列

+(instancetype)sharedInstance {
    static id instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] initWithName:@"ZCJDiskCacheShared"];
    });
    return instance;
}

-(instancetype)initWithName:(NSString *)name {
    if (!name) {
        return nil;
    }
    
    self = [super init];
    if (self) {
        _diskCache = [[ZCJDiskCache alloc] initWithName:name];
        _memoryCache = [[ZCJMemoryCache alloc] init];
        
        NSString *queueName = [[NSString alloc] initWithFormat:@"%@.%p", ZCJCachePrefix, (void *)self];
        _currentQueue = dispatch_queue_create([[NSString stringWithFormat:@"%@ Asynchronous Queue", queueName] UTF8String], DISPATCH_QUEUE_CONCURRENT);

    }
    return self;
}

緩存存儲方法,入?yún)╧ey,object以及回調(diào)block。object對象要符合NSCoding協(xié)議,才能完成數(shù)據(jù)的歸檔和解檔。這里簡單處理不做過多要求。

-(void)setObject:(id)object forKey:(NSString *)key block:(ZCJCacheObjectBlock)block {
    if (!key || !object) {
        return;
    }
    
    //向group追加任務(wù)隊列,如果所有的任務(wù)都執(zhí)行或者超時,它發(fā)出通知
    dispatch_group_t group = nil;
    ZCJMemoryCacheObjectBlock memBlock = nil;
    ZCJDiskCacheObjectBlock diskBlock = nil;
    
    if (block) {
        group = dispatch_group_create();
        dispatch_group_enter(group);
        dispatch_group_enter(group);
        
        memBlock = ^(ZCJMemoryCache *memoryCache, NSString *memoryCacheKey, id memoryCacheObject) {
            dispatch_group_leave(group);
        };
        
        diskBlock = ^(ZCJDiskCache *diskCache, NSString *key, id object) {
            dispatch_group_leave(group);
        };
    }
    [_memoryCache setObject:object forKey:key block:memBlock];
    [_diskCache setObject:object forKey:key block:diskBlock];
    
    if (group) {
        __weak ZCJCache *weakSelf = self;
        dispatch_group_notify(group, _currentQueue, ^{
            ZCJCache *strongSelf = weakSelf;
            if (strongSelf)
                block(strongSelf, key, object);
        });
    }
}

讀取緩存數(shù)據(jù),先在內(nèi)存緩存中查找,找不到再去磁盤緩存中搜索。
- (void)objectForKey:(NSString *)key block:(ZCJCacheObjectBlock)block {
if (!key) {
return;
}

    __weak ZCJCache *weakSelf = self;
    dispatch_sync(_currentQueue, ^{
        ZCJCache *strongSelf = weakSelf;
        [strongSelf.memoryCache objectForKey:key block:^(ZCJMemoryCache *memoryCache, NSString *key, id object) {
            if (object) {
                dispatch_sync(_currentQueue, ^{
                    ZCJCache *strongSelf = weakSelf;
                    block(strongSelf, key, object);
                });
            }
            else {
                [strongSelf.diskCache objectForKey:key block:^(ZCJDiskCache *diskCache, NSString *key, id object) {
                    if (object) {
                        dispatch_sync(_currentQueue, ^{
                            ZCJCache *strongSelf = weakSelf;
                            block(strongSelf, key, object);
                        });
                    }
                }];
            }
        }];
    });
}

刪除指定鍵值的緩存,這里簡單處理,沒用異步的方式

-(void)removeObjectForKey:(NSString *)key {
    if (!key) {
        return;
    }
    
    [_memoryCache removeObjectForKey:key];
    [_diskCache removeObjectForKey:key];
}

ZCJMemoryCache

ZCJMemoryCache的接口跟ZCJCache類似,代碼就不貼了。具體看一下它的緩存set、get、remove方法。主要內(nèi)容是將緩存內(nèi)容放入字典中,key是唯一的關(guān)鍵字,value是緩存內(nèi)容。
為了在多線程訪問時,保證結(jié)果的安全,避免資源爭奪問題,在關(guān)鍵設(shè)值取值處加鎖。

-(void)setObject:(id)object forKey:(NSString *)key block:(ZCJMemoryCacheObjectBlock)block {
    if (!key || !object) {
        return;
    }
    
    __weak ZCJMemoryCache *weakSelf = self;
    dispatch_sync(_currentQueue, ^{
        pthread_mutex_lock(&_mutex);
        [weakSelf.cacheDic setObject:object forKey:key];
        pthread_mutex_unlock(&_mutex);
        if (block) {
            block(weakSelf, key, object);
        }
    });
}

-(id)objectForKey:(NSString *)key {
    
    id object = nil;
    pthread_mutex_lock(&_mutex);
    object = _cacheDic[key];
    pthread_mutex_unlock(&_mutex);
    return object;
}

- (void)objectForKey:(NSString *)key block:(ZCJMemoryCacheObjectBlock)block{
    __weak ZCJMemoryCache *weakSelf = self;
    dispatch_async(_currentQueue, ^{
        id object = [weakSelf objectForKey:key];
        if (block) {
            block(weakSelf, key, object);
        }
    });
}


-(void)removeObjectForKey:(NSString *)key {
    if (!key) {
        return;
    }
    
    pthread_mutex_lock(&_mutex);
    [_cacheDic removeObjectForKey:key];
    pthread_mutex_unlock(&_mutex);
}

ZCJMemoryCache在程序進入后臺或收到內(nèi)存警告時,清空內(nèi)存緩存。以下是代碼實現(xiàn)

//注冊通知
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveEnterBackgroundNotification:) name:UIApplicationDidEnterBackgroundNotification object:nil];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];

//清空內(nèi)存緩存
- (void)didReceiveEnterBackgroundNotification:(NSNotification *)notification {
    [self removeAllObjects];
}

- (void)didReceiveMemoryWarning:(NSNotification *)notification {
    [self removeAllObjects];
}

- (void)removeAllObjects {
    pthread_mutex_lock(&_mutex);
    [_cacheDic removeAllObjects];
    pthread_mutex_unlock(&_mutex);
}

ZCJDiskCache

ZCJDiskCache與ZCJMemoryCache的過程類似,不同的地方在于ZCJMemoryCache將緩存數(shù)據(jù)存儲在字典中,而ZCJDiskCache將緩存數(shù)據(jù)存儲到文件系統(tǒng)中。ZCJDiskCache在存取緩存時需要將字符串形式的key轉(zhuǎn)換成磁盤緩存路徑。
看代碼:
- (void)setObject:(id)object forKey:(NSString *)key block:(ZCJDiskCacheObjectBlock)block {
if (!key || !object) {
return;
}

    __weak ZCJDiskCache *weakSelf= self;
    dispatch_sync(_currentQueue, ^{
        NSURL *fileUrl = nil;
        dispatch_semaphore_wait(_lockSemaphore, DISPATCH_TIME_FOREVER);
        fileUrl = [self encodedFileURLForKey:key];
        
        NSData *data = [NSKeyedArchiver archivedDataWithRootObject:object];
        NSError *writeErr = nil;
        BOOL written = [data writeToURL:fileUrl options:NSDataWritingAtomic error:&writeErr];
        if (!written) {
            fileUrl = nil;
        }
        dispatch_semaphore_signal(_lockSemaphore);
        
        if (block) {
            block(weakSelf, key, object);
        }
    });
}

demo的基本功能就這些,它的使用方式與PINCache基本一致。在ZCJCacheTest中,有相關(guān)的單元測試。如下圖:

ZCJCache類的單元測試

demo的完整代碼已上傳到Github,地址:https://github.com/superzcj/ZCJCache

總結(jié)

PINCache 異步執(zhí)行緩存存取,它的實現(xiàn)過程給我們很多啟發(fā),在我們?nèi)粘i_發(fā)與設(shè)計中有很多可以學習的地方,比如字典存儲、GCD的使用。閱讀和仿寫這個類庫的實現(xiàn)也讓我受益匪淺,我也會在今后繼續(xù)用這種方式閱讀和仿寫其它的著名類庫,希望大家多多支持。
如果覺得我的這篇文章對你有幫助,請在下方點個贊支持一下,謝謝!

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

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

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