YYCache 源碼分析 拾遺

YYCache扎运,作為一個(gè)非常優(yōu)秀的開源iOS緩存框架世吨,其代碼非常值得學(xué)習(xí)户魏。網(wǎng)上已經(jīng)有大量的源碼分析文章驶臊,再加上原作者也有一篇非常優(yōu)秀的博文,因此我也不再復(fù)述叼丑,在這里推薦給大家?guī)灼矣X的寫的不錯(cuò)的源碼分析的博客:

那么這篇文章說什么纵寝,拾遺!就是那些我在讀代碼時(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ì)展開了贸典,有興趣的可以參考下面這兩篇文章视卢,包括

@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)試宋彼,除此之外弄砍,還有TIMEFILE输涕、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)用

屏幕快照 2018-06-14 上午10.46.31.png

如果你想使用SQLite,那么這也是一個(gè)非常好的范本搭综,很多API可以拿來直接使用垢箕。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市兑巾,隨后出現(xiàn)的幾起案子条获,更是在濱河造成了極大的恐慌,老刑警劉巖蒋歌,帶你破解...
    沈念sama閱讀 212,816評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件帅掘,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡堂油,警方通過查閱死者的電腦和手機(jī)修档,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來府框,“玉大人吱窝,你說我怎么就攤上這事。” “怎么了院峡?”我有些...
    開封第一講書人閱讀 158,300評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵兴使,是天一觀的道長。 經(jīng)常有香客問我照激,道長发魄,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,780評(píng)論 1 285
  • 正文 為了忘掉前任俩垃,我火速辦了婚禮励幼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘口柳。我一直安慰自己苹粟,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,890評(píng)論 6 385
  • 文/花漫 我一把揭開白布啄清。 她就那樣靜靜地躺著六水,像睡著了一般俺孙。 火紅的嫁衣襯著肌膚如雪辣卒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,084評(píng)論 1 291
  • 那天睛榄,我揣著相機(jī)與錄音荣茫,去河邊找鬼。 笑死场靴,一個(gè)胖子當(dāng)著我的面吹牛啡莉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播旨剥,決...
    沈念sama閱讀 39,151評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼咧欣,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了轨帜?” 一聲冷哼從身側(cè)響起魄咕,我...
    開封第一講書人閱讀 37,912評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蚌父,沒想到半個(gè)月后哮兰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,355評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡苟弛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,666評(píng)論 2 327
  • 正文 我和宋清朗相戀三年喝滞,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片膏秫。...
    茶點(diǎn)故事閱讀 38,809評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡右遭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情窘哈,我是刑警寧澤言蛇,帶...
    沈念sama閱讀 34,504評(píng)論 4 334
  • 正文 年R本政府宣布,位于F島的核電站宵距,受9級(jí)特大地震影響腊尚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜满哪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,150評(píng)論 3 317
  • 文/蒙蒙 一婿斥、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧哨鸭,春花似錦民宿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至只估,卻和暖如春志群,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蛔钙。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評(píng)論 1 267
  • 我被黑心中介騙來泰國打工锌云, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人吁脱。 一個(gè)月前我還...
    沈念sama閱讀 46,628評(píng)論 2 362
  • 正文 我出身青樓桑涎,卻偏偏與公主長得像,于是被迫代替她去往敵國和親兼贡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子攻冷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,724評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容