其實最近是在重新熟練Swift的使用,我想出了一個比較實用的方法,那就是一邊看OC的項目,看懂之后用Swift實現(xiàn)一遍。這樣既學(xué)習(xí)了優(yōu)秀的源碼又練習(xí)了Swift,一舉兩得。
之前看過幾篇文章是剖析YYKit里面的一些小模塊,對源碼對一些解讀。不得不說作者ibireme的設(shè)計思維和技術(shù)細(xì)節(jié)的處理都非常的棒。所以就選了YYKit里面的一些小模塊入手。
YYCache主要分為了兩部分:YYMemoryCache內(nèi)存緩存和磁盤緩存YYDiskCache。平常使用的時候我們一般都只直接操作YYCache這個類,他是對內(nèi)存緩存和磁盤緩存的封裝。
這篇文章主要是講解YYCache模塊里面的YYMemoryCache部分。
API
我們先可以看一下YYMemoryCache的.h文件,瀏覽一起屬性和方法。大多數(shù)的都可以見名知意的。
@interface YYMemoryCache : NSObject
#pragma mark - Attribute
///=============================================================================
/// @name Attribute
///=============================================================================
@property (nullable, copy) NSString *name;
@property (readonly) NSUInteger totalCount;
@property (readonly) NSUInteger totalCost;
#pragma mark - Limit
///=============================================================================
/// @name Limit
///=============================================================================
@property NSUInteger countLimit;
@property NSUInteger costLimit;
@property NSTimeInterval ageLimit; //過期時間
@property NSTimeInterval autoTrimInterval;//自動處理的間隔時間
@property BOOL shouldRemoveAllObjectsOnMemoryWarning;
@property BOOL shouldRemoveAllObjectsWhenEnteringBackground;
@property (nullable, copy) void(^didReceiveMemoryWarningBlock)(YYMemoryCache *cache);
@property (nullable, copy) void(^didEnterBackgroundBlock)(YYMemoryCache *cache);
@property BOOL releaseOnMainThread;
@property BOOL releaseAsynchronously;
#pragma mark - Access Methods
///=============================================================================
/// @name Access Methods
///=============================================================================
- (BOOL)containsObjectForKey:(id)key;
- (nullable id)objectForKey:(id)key;
- (void)setObject:(nullable id)object forKey:(id)key;
- (void)setObject:(nullable id)object forKey:(id)key withCost:(NSUInteger)cost;
- (void)removeObjectForKey:(id)key;
- (void)removeAllObjects;
#pragma mark - Trim
///=============================================================================
/// @name Trim
///=============================================================================
- (void)trimToCount:(NSUInteger)count;
- (void)trimToCost:(NSUInteger)cost;
- (void)trimToAge:(NSTimeInterval)age;
@end
我把亂七八糟的注釋都刪掉了,這樣可以直觀的來看,api分為四個部分,前兩部分都是一些屬性,后面兩個是方法。
第一個Attribute部分是YYMemoryCache類儲存的一些基本的屬性:name,totalCount(儲存對象的總個數(shù)),totalCost(儲存的總占內(nèi)存)。Limit部分是一些限制條件,就不一一的說了,單說一個releaseOnMainThread這個屬性,可能會有因為,如果如果能異步釋放,為什么還要強(qiáng)制去主線程釋放呢 ? 這是因為有一些類,像UIView/CALayer這種是要在主線程中釋放的,源碼注釋中也有提到。
第三部分就是一些跟儲存相關(guān)的方法,最后一部分就是根據(jù)限制條件修剪處理內(nèi)存的方法了 ~
.m代碼剖析
LRU 緩存淘汰算法
YYMemoryCache是提供了內(nèi)存修剪的方法的,既然有修剪,那么我們得有一個算法來確定是修剪掉哪一些。YYMemoryCache 和 YYDiskCache 都是實現(xiàn)的 LRU (least-recently-used) ,即最近最少使用淘汰算法。具體怎么樣實現(xiàn)我們往后再說。
實現(xiàn)緩存方式
.m的最前面是實現(xiàn)了兩個內(nèi)部類_YYLinkedMap和_YYLinkedMapNode。 可以看出具體的緩存方法是通過一個雙向列表和散列容器來實現(xiàn)的。
_YYLinkedMap中給出來操作結(jié)點的方法
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;
- (void)removeNode:(_YYLinkedMapNode *)node;
- (_YYLinkedMapNode *)removeTailNode;
- (void)removeAll;
具體細(xì)節(jié)剖析
先總起來說一下這些實現(xiàn)的代碼,其實很容易讀懂,就是通過一個鏈表的形式來處理緩存的數(shù)據(jù),添加緩存的時候,就往鏈表的尾部添加一個節(jié)點,(通過節(jié)點來表示我們實際要儲存的數(shù)據(jù)),如果要根據(jù)限制條件修剪內(nèi)存的話,也是循環(huán)的刪除尾部的那個節(jié)點,直到符合限制條件。那我們的LRU 緩存淘汰算法具體怎么使用,我發(fā)現(xiàn)在每一個讀取了一個數(shù)據(jù)之后,會把這個數(shù)據(jù)在鏈表中對應(yīng)的結(jié)點移動到頭部,這樣在大概率的情況下使用頻率高的緩存數(shù)據(jù)會在鏈表的前面。所以修剪的時候可以從尾部修剪。
在具體的實現(xiàn)代碼中,作者有很多很亮眼的操作,我們來欣賞一下。
1.定時修剪內(nèi)存
// 根據(jù)限制條件修剪內(nèi)存的占用 并根絕設(shè)定的時間遞歸調(diào)用
- (void)_trimRecursively {
__weak typeof(self) _self = self;
//注意這個dispatch_after后面使用的dispatch_get_global_queue 異步
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)用修剪內(nèi)存的方法,然后在遞歸調(diào)用本身。注意的是dispatch_after后面使用的dispatch_get_global_queue 來進(jìn)行異步操作。
2.修剪內(nèi)存的邏輯
- (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
});
}
}
我們以這個按照占內(nèi)存大小的限制來修剪內(nèi)存為例子來看一下具體的實現(xiàn)方法。
開始先做幾個判斷,看是否超過限制需要修剪,不需要修剪就直接return。
修剪的過程就跟上面說到的是一樣的,判斷是否超過內(nèi)存限制,超過就刪掉尾部的結(jié)點,如此循環(huán)操作,只要符合限制要求。
異步釋放資源:
我們可以看到在removeNode的時候,會使用一個holder數(shù)組來接收被移除的這些node,然后最后釋放這些結(jié)點。為什么要這樣做?其實這就是通過異步釋放這些資源來減少主線程資源的開銷。這里作者在異步中調(diào)用了[holder count]; 其實最開始我也不知道這個是什么意思,但是作者標(biāo)注了release in queue,我猜測是通過調(diào)用你這個holder的隨便一個方法,讓這個異步的線程來管理這個holder,進(jìn)而通過此異步線程來實現(xiàn)holder中對象的釋放。
鎖的使用:
還有一個比較重要的點,為什么使用pthread_mutex_trylock這個方式加鎖,然后在失敗之后,線程要sleep。
這個問題就需要我們?nèi)パ芯恳幌赂鞣N鎖了。很慚愧我對鎖的了解不是很深刻,但是通過看了大神的博客,有了一些了解。(下面內(nèi)容引用自大神的博客,文末有地址)
作者都是使用的pthread_mutex_t互斥鎖,這個鎖有一個特性,在多個線程競爭一個資源的時候,除了競爭成功的線程,其他的線程都會被動掛起狀態(tài),當(dāng)競爭成功的線程解鎖是,會去主動將掛起的其他線程激活,這個過程包含了上下文切換,CPU搶占,信號發(fā)送等開銷,很明顯,開銷有些大。
所以作者使用了pthread_mutex_trylock()嘗試解鎖,若解鎖失敗該方法會立即返回,讓當(dāng)前線程不會進(jìn)入被動的掛起狀態(tài)(也可以說阻塞),在下一次循環(huán)時又繼續(xù)嘗試獲取鎖。這個過程很有意思,感覺是手動實現(xiàn)了一個自旋鎖。而自旋鎖有個需要注意的問題是:死循環(huán)等待的時間越長,對 cpu 的消耗越大。所以作者做了一個很短的睡眠 usleep(10 * 1000),有效的減小了循環(huán)的調(diào)用次數(shù),至于這個睡眠時間的長度為什么是 10ms, 作者應(yīng)該做了測試。
其他部分
其他部分就不一一細(xì)說了,作者整體思路很清晰,然后代碼邏輯也很好懂,像上面提到的一些細(xì)節(jié)的處理可見作者的技術(shù)水平了。