YYCache扎运,作為一個(gè)非常優(yōu)秀的開源iOS緩存框架世吨,其代碼非常值得學(xué)習(xí)户魏。網(wǎng)上已經(jīng)有大量的源碼分析文章驶臊,再加上原作者也有一篇非常優(yōu)秀的博文,因此我也不再復(fù)述叼丑,在這里推薦給大家?guī)灼矣X的寫的不錯(cuò)的源碼分析的博客:
- 首先當(dāng)然是YYCache作者ibireme寫的YYCache 設(shè)計(jì)思路
- 然后是愛生活的小悅悅的博客寫的YYCache源碼分析系列:
- 最后一篇是來自馬在路上的# 深入理解YYCache关翎,因?yàn)橐暯呛蜕弦黄晕⒂悬c(diǎn)不一樣,所以在這里也推薦給大家鸠信。
那么這篇文章說什么纵寝,拾遺!就是那些我在讀代碼時(shí)症副,覺得有意思的或者我們值得借鑒的店雅,又或者有所拓展的內(nèi)容翠忠。他們不一定是YYCache的核心內(nèi)容充择,但卻體現(xiàn)了作者的嚴(yán)謹(jǐn)和對(duì)iOS/C的理解和運(yùn)用的能力。
「__has_include」宏
#if __has_include(<YYCache/YYCache.h>)
FOUNDATION_EXPORT double YYCacheVersionNumber;
FOUNDATION_EXPORT const unsigned char YYCacheVersionString[];
#import <YYCache/YYMemoryCache.h>
#import <YYCache/YYDiskCache.h>
#import <YYCache/YYKVStorage.h>
#elif __has_include(<YYWebImage/YYCache.h>)
#import <YYWebImage/YYMemoryCache.h>
#import <YYWebImage/YYDiskCache.h>
#import <YYWebImage/YYKVStorage.h>
#else
#import "YYMemoryCache.h"
#import "YYDiskCache.h"
#import "YYKVStorage.h"
#endif
先來看這段代碼色乾, 在#if/#elif/#else/#endif宏中出現(xiàn)了__has_include()這個(gè)宏辕坝,此宏傳入一個(gè)你想引入文件的名稱作為參數(shù)窍奋,如果該文件能夠被引入則返回1,否則返回0酱畅。所以上面這段的意思就是
- 首先檢查是否存在YYCache框架琳袄,如果存在,則引入YYCache框架下的三個(gè)頭文件
- 否則檢查是否存在YYWebImage框架纺酸,如果存在窖逗,則引入YYWebImage框架下的三個(gè)頭文件。
- 否則直接引入三個(gè)頭文件餐蔬。
可以看出碎紊,引入框架下的頭文件,使用了左右尖括號(hào)< >
并添加了框架目錄樊诺,而非框架下的引入則使用了雙引號(hào)" "
FOUNDATION_EXPORT
FOUNDATION_EXPORT具體是什么仗考,來看看這個(gè)宏的定義
#if defined(__cplusplus)
#define FOUNDATION_EXTERN extern "C"
#else
#define FOUNDATION_EXTERN extern
#endif
#if TARGET_OS_WIN32
#if defined(NSBUILDINGFOUNDATION)
#define FOUNDATION_EXPORT FOUNDATION_EXTERN __declspec(dllexport)
#else
#define FOUNDATION_EXPORT FOUNDATION_EXTERN __declspec(dllimport)
#endif
#define FOUNDATION_IMPORT FOUNDATION_EXTERN __declspec(dllimport)
#else
#define FOUNDATION_EXPORT FOUNDATION_EXTERN
#define FOUNDATION_IMPORT FOUNDATION_EXTERN
#endif
可以看到,在通常的iOS開發(fā)中词爬,F(xiàn)OUNDATION_EXPORT或者FOUNDATION_IMPORT等同于extern秃嗜。使用FOUNDATION_EXPORT或者FOUNDATION_IMPORT更具有平臺(tái)或者語言兼容性,你可以看到在C++環(huán)境中顿膨,又或者Windows環(huán)境中锅锨,他們的定義會(huì)發(fā)生改變。
NS_ASSUME_NONNULL_BEGIN/NS_ASSUME_NONNULL_END
在Swift中存在Option類型恋沃,也就是使用橡类?和!聲明的變量芽唇。但是OC里面沒有這個(gè)特征 ,因此在XCODE6.3之后出現(xiàn)新的關(guān)鍵詞(__nullable && ___nonnull)定義用于OC轉(zhuǎn)SWIFT時(shí)候可以區(qū)分到底是什么類型
- __nullable指代對(duì)象可以為NULL或者為NIL
- __nonnull指代對(duì)象不能為null
當(dāng)我們不遵循這一規(guī)則時(shí)顾画,編譯器就會(huì)給出警告。
但如果需要每個(gè)屬性或每個(gè)方法都去指定nonnull和nullable匆笤,是一件非常繁瑣的事研侣。蘋果為了減輕我們的工作量,專門提供了兩個(gè)宏:NS_ASSUME_NONNULL_BEGIN和NS_ASSUME_NONNULL_END炮捧。在這兩個(gè)宏之間的代碼庶诡,所有簡單指針對(duì)象都被假定為nonnull,因此我們只需要去指定那些nullable的指針咆课。
atomic
@interface YYCache : NSObject
/** The name of the cache, readonly. */
@property (copy, readonly) NSString *name;
/** The underlying memory cache. see `YYMemoryCache` for more information.*/
@property (strong, readonly) YYMemoryCache *memoryCache;
/** The underlying disk cache. see `YYDiskCache` for more information.*/
@property (strong, readonly) YYDiskCache *diskCache;
可以看到末誓,YYCache類的屬性定義扯俱,并沒有使用我們常用的nonatomic屬性,而是使用了默認(rèn)的atomic喇澡。同樣迅栅,你可以看到在YYMemoryCache,YYDiskCache這些需要線程安全的類中晴玖,屬性都使用了atomic读存。而YYKVStorage類中,屬性使用的是nonatomic呕屎,也就是說YYKVStorage是線程不安全的让簿。
當(dāng)然atomic屬性只能保證屬性的原子性,也就是說在屬性訪問/設(shè)置時(shí)保證屬性被唯一訪問秀睛。但并不能保證類是線程安全的尔当。
UNAVAILABLE_ATTRIBUTE
- (instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (instancetype)new UNAVAILABLE_ATTRIBUTE;
C++中我們可以聲明構(gòu)造函數(shù)為私有,從而來禁止用通用的構(gòu)造函數(shù)來生成一個(gè)對(duì)象蹂安。那么在OC中椭迎,如果我們需要來屏蔽init或者new這些方法,從而告知使用者必須用我們指定的構(gòu)造方法來生成一個(gè)對(duì)象藤抡,這該怎么做呢侠碧?我們來看看UNAVAILABLE_ATTRIBUTE的定義
#if defined(__GNUC__) && ((__GNUC__ >= 4) || ((__GNUC__ == 3) && (__GNUC_MINOR__ >= 1)))
#define UNAVAILABLE_ATTRIBUTE __attribute__((unavailable))
#else
#define UNAVAILABLE_ATTRIBUTE
#endif
由于attribute是GNU C特色之一,可以看到如果不是GNU C,UNAVAILABLE_ATTRIBUTE相當(dāng)于空缠黍,而在GNU C環(huán)境下弄兜,為attribute((unavailable))。告訴編譯器該方法不可用瓷式,如果強(qiáng)行調(diào)用編譯器會(huì)提示錯(cuò)誤替饿。
關(guān)于attribute,這里就不詳細(xì)展開了贸典,有興趣的可以參考下面這兩篇文章视卢,包括
- 來自nshipster的attribute
- 以及來自簡書的attribute 總結(jié)
@package
@interface _YYLinkedMapNode : NSObject {
@package
__unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
__unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
id _key;
id _value;
NSUInteger _cost;
NSTimeInterval _time;
}
@end
我們?cè)赺YYLinkedMapNode的定義中看到了@package關(guān)鍵字,簡單來說廊驼, @package變量据过,對(duì)于framework內(nèi)部,相當(dāng)于@protected妒挎, 對(duì)于framework外部绳锅,相當(dāng)于@private。
這個(gè)特性酝掩,很適合用于開發(fā)第三方框架鳞芙,因?yàn)槲覀儾⒉幌M寗e人知道自己屬性的值。
dispatch_after實(shí)現(xiàn)重復(fù)的延時(shí)觸發(fā)器
- (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];
//遞歸的調(diào)用
[self _trimRecursively];
});
}
上面代碼可以用作一個(gè)重復(fù)的延時(shí)觸發(fā)器。當(dāng)然我們可以用NSTimer來實(shí)現(xiàn)一個(gè)重復(fù)的延時(shí)觸發(fā)器原朝,但NSTimer基于Runloop驯嘱,而在自線程中默認(rèn)的Runloop并沒有開啟。而dispatch_after并沒有這種問題喳坠。
同時(shí)可以看到鞠评,這邊的執(zhí)行的隊(duì)列使用了系統(tǒng)定義的DISPATCH_QUEUE_PRIORITY_LOW并發(fā)隊(duì)列。
pthread_mutex
由于OSSpinLock不再線程安全的緣故 (不再安全的 OSSpinLock)丙笋,因此在內(nèi)存加鎖上使用了pthread_mutex谢澈。
pthread_mutex 互斥鎖是一種超級(jí)易用的互斥鎖煌贴,使用的時(shí)候御板,只需要初始化一個(gè) pthread_mutex_t 用 pthread_mutex_lock 來鎖定 pthread_mutex_unlock 來解鎖,當(dāng)使用完成后牛郑,記得調(diào)用 pthread_mutex_destroy 來銷毀鎖怠肋。
pthread_mutex_init(&lock,NULL);
pthread_mutex_lock(&lock);
//do your stuff
pthread_mutex_unlock(&lock);
pthread_mutex_destroy(&lock);
在閱讀YYMemoryCache.m源碼的時(shí)候,你可以看到淹朋,為了保證線程安全笙各,所有的緩存相關(guān)的操作,都進(jìn)行了加鎖/解鎖操作础芍。
dispatch_semaphore_t
在YYDiskCache.m文件中我們可以看到杈抢,在初始化函數(shù)中,信號(hào)總量被定義為1仑性,所以dispatch_semaphore_wait和dispatch_semaphore_signal函數(shù)可以類似的用作加鎖/解鎖操作惶楼,可以看到該文件頭部定義的兩個(gè)宏。
_lock = dispatch_semaphore_create(1);
#define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER)
#define Unlock() dispatch_semaphore_signal(self->_lock)
作者認(rèn)為在沒有等待情況出現(xiàn)時(shí)诊杆,它的性能比 pthread_mutex 還要高歼捐,但一旦有等待情況出現(xiàn)時(shí),性能就會(huì)下降許多晨汹。相對(duì)于 pthread_mutex 來說豹储,它的優(yōu)勢(shì)在于等待時(shí)不會(huì)消耗 CPU 資源。因此對(duì)磁盤緩存來說淘这,它比較合適剥扣。
beginBackgroundTaskWithExpirationHandler
- (void)dealloc {
UIBackgroundTaskIdentifier taskID = [_YYSharedApplication() beginBackgroundTaskWithExpirationHandler:^{}];
[self _dbClose];
if (taskID != UIBackgroundTaskInvalid) {
[_YYSharedApplication() endBackgroundTask:taskID];
}
}
在YYKVStorage.m的dealloc代碼中,我們看到了beginBackgroundTaskWithExpirationHandler/endBackgroundTask的調(diào)用铝穷。beginBackgroundTaskWithExpirationHandler方法允許你的APP在退至后臺(tái)后繼續(xù)運(yùn)行一段時(shí)間钠怯。在這里我們看到處理就是需要將DB關(guān)閉。
獲取UIApplication實(shí)例
/// Returns nil in App Extension.
static UIApplication *_YYSharedApplication() {
static BOOL isAppExtension = NO;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = NSClassFromString(@"UIApplication");
if(!cls || ![cls respondsToSelector:@selector(sharedApplication)]) isAppExtension = YES;
if ([[[NSBundle mainBundle] bundlePath] hasSuffix:@".appex"]) isAppExtension = YES;
});
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
return isAppExtension ? nil : [UIApplication performSelector:@selector(sharedApplication)];
#pragma clang diagnostic pop
}
在這里氧骤,我們可以看到作者的考慮的全面呻疹。在獲取SharedApplication的時(shí)候,考慮了是否為App Extension。
同時(shí)注意的細(xì)節(jié)刽锤,clang diagnostic ignored配合clang diagnostic push/pop镊尺,讓編譯器在這一行忽略u(píng)ndeclared-selector的警告。
FUNCTION/LINE
我們?cè)赮YKVStorage.m的源碼中并思,可以看到下面這種NSLog庐氮。
NSLog(@"%s line:%d sqlite open failed (%d).", __FUNCTION__, __LINE__, result);
這是GCC預(yù)定義的宏,方便調(diào)試宋彼,除此之外弄砍,還有TIME、FILE输涕、DATE等音婶。
MD5
/// String's md5 hash.
static NSString *_YYNSStringMD5(NSString *string) {
if (!string) return nil;
NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
unsigned char result[CC_MD5_DIGEST_LENGTH];
CC_MD5(data.bytes, (CC_LONG)data.length, result);
return [NSString stringWithFormat:
@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
result[0], result[1], result[2], result[3],
result[4], result[5], result[6], result[7],
result[8], result[9], result[10], result[11],
result[12], result[13], result[14], result[15]
];
}
MD5消息摘要算法5,蘋果自帶的CC_MD5就支持這種算法莱坎。在這里衣式,作者用MD5算法來生成文件名,來用于文件類型的緩存存儲(chǔ)檐什。
具體的MD5的詳細(xì)解釋碴卧,可以參考:iOS MD5 (消息摘要算法5)
良好的注釋/完美的代碼結(jié)構(gòu)
#pragma mark - Save Items
///=============================================================================
/// @name Save Items
///=============================================================================
/**
Save an item or update the item with 'key' if it already exists.
@discussion This method will save the item.key, item.value, item.filename and
item.extendedData to disk or sqlite, other properties will be ignored. item.key
and item.value should not be empty (nil or zero length).
If the `type` is YYKVStorageTypeFile, then the item.filename should not be empty.
If the `type` is YYKVStorageTypeSQLite, then the item.filename will be ignored.
It the `type` is YYKVStorageTypeMixed, then the item.value will be saved to file
system if the item.filename is not empty, otherwise it will be saved to sqlite.
@param item An item.
@return Whether succeed.
*/
- (BOOL)saveItem:(YYKVStorageItem *)item;
#pragma mark - db
- (BOOL)_dbCheck {
if (!_db) {
if (_dbOpenErrorCount < kMaxErrorRetryCount &&
CACurrentMediaTime() - _dbLastOpenErrorTime > kMinRetryTimeInterval) {
return [self _dbOpen] && [self _dbInitialize];
} else {
return NO;
}
}
return YES;
}
上面僅僅是兩個(gè)例子,大家可以看到作者對(duì)函數(shù)的注釋寫的非常漂亮乃正。此外住册,私有函數(shù)命名以_開始,并用#pragma mark分割功能塊瓮具。這都是非常值得借鑒學(xué)習(xí)的荧飞。
SQLite的運(yùn)用
如果你想使用SQLite,那么這也是一個(gè)非常好的范本搭综,很多API可以拿來直接使用垢箕。