本篇文章將帶來YYCache的解讀,YYCache支持內存和本地兩種方式的數據存儲。咱們先拋出兩個問題:node
此次的解讀跟以前的源碼解讀不一樣,我只會展現重要部分的代碼,由於咱們學習YYCache的目的是學習做者的思路,順便學習一下實現這些功能所用到的技術。算法
咱們使用YYMemoryCache能夠把數據緩存進內存之中,它內部會建立了一個YYMemoryCache對象,而後把數據保存進這個對象之中。sql
但凡涉及到相似這樣的操做,代碼都須要設計成線程安全的。所謂的線程安全就是指充分考慮多線程條件下的增刪改查操做。數據庫
咱們應該養成這樣的習慣:在寫任何類的時候都把該類當作框架來寫,所以須要設計好暴露出來的接口,這也正符合代碼封裝的思想。編程
YYMemoryCache暴露出來的接口咱們在此就略過了,咱們都知道要想高效的查詢數據,使用字典是一個很好的方法。字典的原理跟哈希有關,總之就是把key直接映射成內存地址,而後處理衝突和和擴容的問題。對這方面有興趣的能夠自行搜索資料。swift
YYMemoryCache內部封裝了一個對象_YYLinkedMap
,包含了下邊這些屬性:數組
@interface _YYLinkedMap : NSObject { @package CFMutableDictionaryRef _dic; // do not set object directly NSUInteger _totalCost; NSUInteger _totalCount; _YYLinkedMapNode *_head; // MRU, do not change it directly _YYLinkedMapNode *_tail; // LRU, do not change it directly BOOL _releaseOnMainThread; BOOL _releaseAsynchronously; }
能夠看出來,CFMutableDictionaryRef _dic
將被用來保存數據。這裏使用了CoreFoundation的字典,性能更好。字典裏邊保存着的是_YYLinkedMapNode
對象。緩存
/** A node in linked map. Typically, you should not use this class directly. */ @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
但看上邊的代碼,就能知道使用了鏈表的知識。可是有一個疑問,單用字典咱們就能很快的查詢出數據,爲何還要實現鏈表這一數據結構呢?安全
答案就是淘汰算法,YYMemoryCache使用了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; }
若是有一列數據已經按順序排好了,我使用了中間的某個數據,那麼就要把這個數據插入到最開始的位置,這就是一條規則,越是最近使用的越靠前。
在設計上,YYMemoryCache還提供了是否異步釋放數據這一選項,在這裏就不提了,咱們在來看看在YYMemoryCache中用到的鎖的知識。
pthread_mutex_lock是一種互斥所:
pthread_mutex_init(&_lock, NULL); // 初始化 pthread_mutex_lock(&_lock); // 加鎖 pthread_mutex_unlock(&_lock); // 解鎖 pthread_mutex_trylock(&_lock) == 0 // 是否加鎖,0:未鎖住,其餘值:鎖住
在OC中有不少種鎖能夠用,pthread_mutex_lock就是其中的一種。YYMemoryCache有這樣一種設置,每隔一個固定的時間就要處理數據,代碼以下:
- (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]; }); }
上邊的代碼中,每隔_autoTrimInterval時間就會在後臺嘗試處理數據,而後再次調用自身,這樣就實現了一個相似定時器的功能。這一個小技巧能夠學習一下。
- (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) { 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 { usleep(10 * 1000); //10 ms } } if (holder.count) { dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue(); dispatch_async(queue, ^{ [holder count]; // release in queue }); } }
這段代碼很經典,能夠直接拿來用,咱們在某個處理數據的類中,能夠直接使用相似這樣的代碼。若是鎖正在使用,那麼可使用usleep(10 * 1000); //10 ms
等待一小段時間。上邊的代碼把須要刪除的數據,首先添加到一個數組中,而後使用[holder count]; // release in queue
釋放了資源。
當某個變量在出了本身的做用域以後,正常狀況下就會被自動釋放。
我發現隨着編碼經驗的不斷增長,會不經意間學會模仿這一技能。但有一點,咱們必須發現那些出彩的地方,所以,我認爲深刻理解的本質就是學習該框架的核心思想。
上一小節中,咱們已經明白了YYMemoryCache實際上就是建立了一個對象實例,該對象內部使用字典和雙向鏈表實現。YYKVStorage最核心的思想是KV
這兩個字母,表示key-value的意思,目的是讓使用者像使用字典同樣操做數據。
咱們應該明白,封裝具備層次性,不建議用一層封裝來封裝複雜的功能。
YYKVStorage讓咱們只關心3件事:
同理,YYKVStorage在設計接口的時候,也從這3個方面進行了考慮。這數據功能設計層面的思想。
在真實的編程中,每每須要把數據封裝成一個對象:
/** YYKVStorageItem is used by `YYKVStorage` to store key-value pair and meta data. Typically, you should not use this class directly. */ @interface YYKVStorageItem : NSObject @property (nonatomic, strong) NSString *key; ///< key @property (nonatomic, strong) NSData *value; ///< value @property (nullable, nonatomic, strong) NSString *filename; ///< filename (nil if inline) @property (nonatomic) int size; ///< value's size in bytes @property (nonatomic) int modTime; ///< modification unix timestamp @property (nonatomic) int accessTime; ///< last access unix timestamp @property (nullable, nonatomic, strong) NSData *extendedData; ///< extended data (nil if no extended data) @end
上邊的代碼就是對每條數據的一個封裝,在我封裝的MCDownloader(iOS下載器)說明書中,也是用了相似的技術。固然,在YYKVStorage中,咱們並不須要是用上邊的對象。
咱們看一些藉口設計方面的內容:
#pragma mark - Attribute ///============================================================================= /// @name Attribute ///============================================================================= @property (nonatomic, readonly) NSString *path; ///< The path of this storage. @property (nonatomic, readonly) YYKVStorageType type; ///< The type of this storage. @property (nonatomic) BOOL errorLogsEnabled; ///< Set `YES` to enable error logs for debug. #pragma mark - Initializer ///============================================================================= /// @name Initializer ///============================================================================= - (instancetype)init UNAVAILABLE_ATTRIBUTE; + (instancetype)new UNAVAILABLE_ATTRIBUTE; /** The designated initializer. @param path Full path of a directory in which the storage will write data. If the directory is not exists, it will try to create one, otherwise it will read the data in this directory. @param type The storage type. After first initialized you should not change the type of the specified path. @return A new storage object, or nil if an error occurs. @warning Multiple instances with the same path will make the storage unstable. */ - (nullable instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type NS_DESIGNATED_INITIALIZER;
接口中的屬性都是很重要的信息,咱們應該儘可能利用好它的讀寫屬性,儘可能設計成只讀屬性。默認狀況下,不是隻讀的,都很容易讓其餘開發者認爲,該屬性是能夠設置的。
對於初始化方法而言,若是某個類須要提供一個指定的初始化方法,那麼就要使用NS_DESIGNATED_INITIALIZER
給予提示。同時使用UNAVAILABLE_ATTRIBUTE
禁用掉默認的方法。接下來要重寫禁用的初始化方法,在其內部拋出異常:
- (instancetype)init { @throw [NSException exceptionWithName:@"YYKVStorage init error" reason:@"Please use the designated initializer and pass the 'path' and 'type'." userInfo:nil]; return [self initWithPath:@"" type:YYKVStorageTypeFile]; }
上邊的代碼你們能夠直接拿來用,千萬不要怕程序拋出異常,在發佈以前,可以發現潛在的問題是一件好事。使用了上邊的一個小技巧後呢,編碼水平是否是有所提高?
再給你們簡單分析分析下邊同樣代碼:
- (nullable instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type NS_DESIGNATED_INITIALIZER;
上邊咱們關心的是nullable
關鍵字,表示可能爲空,與之對應的是nonnull
,表示不爲空。能夠說,他們都跟swift有關係,swift中屬性或參數是否爲空都有嚴格的要求。所以咱們在設計屬性,參數,返回值等等的時候,要考慮這些可能爲空的狀況。
// 設置中間的內容默認都是nonnull NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END
咱們如今來分析YYKVStorage.m
的代碼:
static const NSUInteger kMaxErrorRetryCount = 8; static const NSTimeInterval kMinRetryTimeInterval = 2.0; static const int kPathLengthMax = PATH_MAX - 64; static NSString *const kDBFileName = @"manifest.sqlite"; static NSString *const kDBShmFileName = @"manifest.sqlite-shm"; static NSString *const kDBWalFileName = @"manifest.sqlite-wal"; static NSString *const kDataDirectoryName = @"data"; static NSString *const kTrashDirectoryName = @"trash";
代碼的這種寫法,應該不用我說了吧,若是你平時開發沒用到過,那麼就要認真去查資料了。
/* 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); */
在我看來這是超級讚的註釋了。在我我的角度來講,我認爲大多數人的註釋都寫很差,也包括我本身。從上邊的註釋的內容,咱們可以很容易明白YYKVStorage的數據保存結構,和數據庫的設計細節。
上圖中這些函數都是跟數據庫有關的函數,咱們在這裏也不會把代碼弄上來。我我的對這些函數的總結是:
_
這也許就是函數的魅力,有了這些函數,那麼在給接口中的函數寫邏輯的時候就會變得很簡單。
有一個很重要的前提,這些函數都是線程不安全的。所以在使用中須要考慮多線程的問題,這也正是咱們下一小節YYDiskCache的內容。
數據庫增刪改查的思想基本上都差很少,我之後會寫一篇介紹數據庫的文章。
建議你們必定要讀讀YYKVStorage這個類的源碼,這是一個類的典型設計。它內部使用了兩種方式保存數據:一種是保存到數據庫中,另外一種是直接寫入文件。當數據較大時,使用文件寫入性能更好,反之數據庫更好。
上一小節咱們已經明白了YYKVStorage實現了全部的數據存儲的功能,但缺點是它不是線程安全的,所以在YYKVStorage的基礎之上,YYDiskCache保證了線程的安全。
一個類提供什麼樣的功能,這屬於程序設計的範疇,YYDiskCache的接口設計在YYKVStorage的基礎上添加了一些新的特性。好比:
/** If this block is not nil, then the block will be used to archive object instead of NSKeyedArchiver. You can use this block to support the objects which do not conform to the `NSCoding` protocol. The default value is nil. */ @property (nullable, copy) NSData *(^customArchiveBlock)(id object); /** If this block is not nil, then the block will be used to unarchive object instead of NSKeyedUnarchiver. You can use this block to support the objects which do not conform to the `NSCoding` protocol. The default value is nil. */ @property (nullable, copy) id (^customUnarchiveBlock)(NSData *data);
使用上邊的屬性能夠設置對象與NSData之間轉化的規則,這和不少框架同樣,目的是給該類增長一些額外的特性。
仍是那句話,設計一個存儲類,須要考慮下邊幾個特性:
咱們來看看YYDiskCache.m的核心內容。咱們來分析分析下邊這段代碼:
static YYDiskCache *_YYDiskCacheGetGlobal(NSString *path) { if (path.length == 0) return nil; _YYDiskCacheInitGlobal(); dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER); id cache = [_globalInstances objectForKey:path]; dispatch_semaphore_signal(_globalInstancesLock); return cache; }
YYDiskCache內部實現了一種這樣的機制,他會把開發者建立的每個YYDiskCache對象保存到一個全局的集合中,YYDiskCache根據path建立,若是開發者建立了相同path的YYDiskCache,那麼就會返回全局集合中的YYDiskCache。
這裏就產生了一個很重要的概念,在全局對象中的YYDiskCache是能夠釋放的。爲何會發生這種事呢?按理說全局對象引用了YYDiskCache,它就不該該被釋放的。這個問題咱們立刻就會給出答案。
繼續分析上邊的代碼:
static YYDiskCache *_YYDiskCacheGetGlobal(NSString *path)
這種風格的代碼是值得學習的第一點,若是在一個文件中,有一些方法是不依賴某個對象的,那麼咱們就能夠寫成這種形式,它能夠跨對象調用,所以這算是私有函數的一種寫法吧。
if (path.length == 0) return nil;
這個不用多說,健壯的函數內部都要有檢驗參數的代碼。
_YYDiskCacheInitGlobal();
從函數的名字,咱們能夠猜想出它是一個初始化全局對象的方法,它內部引出了一個很重要的對象:
static void _YYDiskCacheInitGlobal() { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _globalInstancesLock = dispatch_semaphore_create(1); _globalInstances = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0]; }); }
你們對NSMapTable可能不太熟悉,他其實和NSMutableDictionary很是類似,咱們都知道字典的key值copy的,他必須實現NSCopying協議,若是key的值改變了,就沒法獲取value了。而NSMapTable使用起來更加自由,咱們能夠操縱key,value的weak和strong特性,關於NSMapTable的詳細使用方法,你們能夠自行去搜索相關的內容。在上邊的代碼中,_globalInstances的中value被設置爲NSPointerFunctionsWeakMemory,也就是說,當_globalInstances添加了一個對象後,該對象的引用計數器不會加1.當該對象沒有被任何其餘對象引用的時候就會釋放。
在網上看着這樣一個例子:
Person *p1 = [[Person alloc] initWithName:@"jack"]; Favourite *f1 = [[Favourite alloc] initWithName:@"ObjC"]; Person *p2 = [[Person alloc] initWithName:@"rose"]; Favourite *f2 = [[Favourite alloc] initWithName:@"Swift"]; NSMapTable *MapTable = [NSMapTable mapTableWithKeyOptions:NSMapTableWeakMemory valueOptions:NSMapTableWeakMemory]; // 設置對應關係表 // p1 => f1; // p2 => f2 [MapTable setObject:f1 forKey:p1]; [MapTable setObject:f2 forKey:p2]; NSLog(@"%@ %@", p1, [MapTable objectForKey:p1]); NSLog(@"%@ %@", p2, [MapTable objectForKey:p2]);
上邊的代碼中,使用NSMapTable讓不一樣類型的對象一一對應起來,這種方式的最大好處是咱們能夠把一個View或者Controller當作key都沒問題,怎麼使用全憑想象啊。
在網上看到一個這樣的例子,他把一些控制器保存到了MapTable之中,而後在想要使用的時候直接讀取出來就好了。不會對控制器形成任何影響。
咱們繼續分析代碼:
dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER); id cache = [_globalInstances objectForKey:path]; dispatch_semaphore_signal(_globalInstancesLock);
dispatch_semaphore_wait配合dispatch_semaphore_signal實現加鎖解鎖的功能,這個沒什麼好說的,能夠大膽使用。
沒有讀過源碼的同窗,必定要讀一讀YYDiskCache的源碼,和YYKVStorage同樣有不少代碼能夠直接拿來用。
當咱們讀到YYCache的時候,感受一會兒就輕鬆了不少,YYCache就是對YYMemoryCache和YYDiskCache的綜合運用,建立YYCache對象後,就建立了一個YYMemoryCache對象和一個YYDiskCache對象。惟一新增的特性就是能夠根據name來建立YYCache,內部會根據那麼來建立一個path,本質上仍是使用path定位的。
第一次以這樣的方式寫博客,我發現好處不少,把很大一部分不是學習重點的代碼過濾掉爲我節省了大量時間。咱們不可能記住全部的代碼,當要用某些知識的時候,知道去哪找就能夠了。
寫代碼就是一個不斷模仿,不斷進步的過程。
感謝YYCache的做者開源了這麼好的東西