YYKit源碼分析之YYCache

標(biāo)題

最近YYKit在IOS各大論壇討論得火熱,其代碼簡(jiǎn)單、高效令人驚嘆。我也湊湊熱鬧,抱著學(xué)習(xí)為目的的心態(tài)解析下ibireme的代碼。這里從比較簡(jiǎn)單的YYCache開(kāi)始入手,下面是該目錄結(jié)構(gòu)。

YYCache目錄結(jié)構(gòu)

YYCache


  • github地址:https://github.com/ibireme/YYCache

  • YYCache是用于Objective-C中用于緩存的第三方框架。

  • YYMemoryCache:內(nèi)存緩存,并且所有API都是線程安全的。

  • YYDiskCache:磁盤(pán)緩存,主要用SQLite和文件存儲(chǔ),并且所有API都是線程安全的

  • LRU算法:Least recently used,最近最少使用

LRU算法


在YYCache的YYMemoryCacheYYDiskCache中都采用LRU算法進(jìn)行快速存取,主要是通過(guò)雙向鏈表NSMutableDictionry來(lái)實(shí)現(xiàn)。下面這張圖很好詮釋了LRU算法。

LRU算法原理
  • 用雙向鏈表來(lái)表示堆棧
  • 新加入的數(shù)據(jù)存在棧頂
  • 使用緩存的時(shí)候,從棧中查找,如果命中,就把數(shù)據(jù)移到棧頂
  • 可以設(shè)置棧最大長(zhǎng)度,超過(guò)長(zhǎng)度就把棧尾數(shù)據(jù)刪除

通過(guò)以上的規(guī)則,一個(gè)簡(jiǎn)單的LRU算法就得以實(shí)現(xiàn)。

線程安全控制(鎖)


分析YYCache的時(shí)候,我發(fā)現(xiàn)作者用了很多鎖來(lái)保證線程安全。這是值得我學(xué)習(xí)的地方,因?yàn)橐郧拔腋緵](méi)有考慮過(guò)線程問(wèn)題。

在這里YYCache主要用了2種鎖:pthread_mutexdispatch_semaphore,下面是作者自己的分析:

OSSpinLock 自旋鎖,性能最高的鎖。原理很簡(jiǎn)單,就是一直 do while 忙等。它的缺點(diǎn)是當(dāng)?shù)却龝r(shí)會(huì)消耗大量 CPU 資源,所以它不適用于較長(zhǎng)時(shí)間的任務(wù)。對(duì)于內(nèi)存緩存的存取來(lái)說(shuō),它非常合適。

dispatch_semaphore 是信號(hào)量,但當(dāng)信號(hào)總量設(shè)為 1 時(shí)也可以當(dāng)作鎖來(lái)。在沒(méi)有等待情況出現(xiàn)時(shí),它的性能比 pthread_mutex 還要高,但一旦有等待情況出現(xiàn)時(shí),性能就會(huì)下降許多。相對(duì)于 OSSpinLock 來(lái)說(shuō),它的優(yōu)勢(shì)在于等待時(shí)不會(huì)消耗 CPU 資源。對(duì)磁盤(pán)緩存來(lái)說(shuō),它比較合適。

為此我也特地補(bǔ)了下課,pthread_mutex其實(shí)也是利用OSSpinLock實(shí)現(xiàn)的,還有其他的一些鎖比如NSLock、@synchronized,這些使用也很方便,網(wǎng)上資料也很多。我簡(jiǎn)單測(cè)試了下,OSSpinLock相對(duì)性能最高,@synchronized相對(duì)性能差些,具體的也可以自己實(shí)驗(yàn)一下。

線程安全就是說(shuō)多線程訪問(wèn)同一代碼,不會(huì)產(chǎn)生不確定的結(jié)果。如果在執(zhí)行代碼前加鎖,只有等這段代碼完成后才解鎖,這樣就不會(huì)出現(xiàn)因多線程而出現(xiàn)競(jìng)爭(zhēng)資源等問(wèn)題,從而實(shí)現(xiàn)線程安全。

雙向鏈表結(jié)構(gòu)


我們先來(lái)看下鏈表的節(jié)點(diǎn),可以看出主要是上一個(gè)節(jié)點(diǎn)指針,下一個(gè)節(jié)點(diǎn)指針,key值,value值,節(jié)點(diǎn)開(kāi)銷(xiāo)大小,緩存時(shí)間戳等部分

@interface _YYLinkedMapNode : NSObject {
   @package
  __unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic 上一個(gè)節(jié)點(diǎn)
 __unsafe_unretained _YYLinkedMapNode *_next; // retained by dic 下一個(gè)節(jié)點(diǎn)
  id _key; //節(jié)點(diǎn)key值
  id _value; //節(jié)點(diǎn)value值
  NSUInteger _cost;//內(nèi)存開(kāi)銷(xiāo)大小
  NSTimeInterval _time;//緩存時(shí)間
}

我們?cè)賮?lái)看下鏈表的結(jié)構(gòu),代碼都添上了中文注釋?zhuān)?/p>

@interface _YYLinkedMap : NSObject {
     @package
      CFMutableDictionaryRef _dic; // 字典的Ref(理解成字典標(biāo)示)
      NSUInteger _totalCost; //鏈表總開(kāi)銷(xiāo)
      NSUInteger _totalCount; //鏈表個(gè)數(shù)
      _YYLinkedMapNode *_head; // 鏈表首個(gè)節(jié)點(diǎn)指針
      _YYLinkedMapNode *_tail; // 鏈表末尾節(jié)點(diǎn)指針
      BOOL _releaseOnMainThread; //是否在主線程釋放內(nèi)存
      BOOL _releaseAsynchronously;//是否異步釋放內(nèi)存
    }

    /// 插入一個(gè)節(jié)點(diǎn),并且更新鏈表總開(kāi)銷(xiāo)
    /// Node and node.key should not be nil.
    - (void)insertNodeAtHead:(_YYLinkedMapNode *)node;

    /// 將一個(gè)節(jié)點(diǎn)放到鏈表頂部
    /// Node should already inside the dic.
   - (void)bringNodeToHead:(_YYLinkedMapNode *)node;

    /// 移除一個(gè)節(jié)點(diǎn)
    /// Node should already inside the dic.
    - (void)removeNode:(_YYLinkedMapNode *)node;

    ///移除尾部節(jié)點(diǎn),淘汰數(shù)據(jù)
    - (_YYLinkedMapNode *)removeTailNode;

    /// 移除所有節(jié)點(diǎn)
    - (void)removeAll;

    @end

下面這張圖很好得解釋了整個(gè)鏈表結(jié)構(gòu),如果有數(shù)據(jù)結(jié)構(gòu)基礎(chǔ)讀懂這個(gè)雙向鏈表應(yīng)該很容易。

鏈表結(jié)構(gòu)

YYMemoryCache


由于代碼還是比較簡(jiǎn)單的,所以我打算用在源碼上注釋的方式解釋?zhuān)簿筒划?huà)流程圖了。

添加數(shù)據(jù)

- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
  if (!key) return;
  if (!object) {
      [self removeObjectForKey:key];
        return;
 }
 
    //這里開(kāi)始加鎖
    pthread_mutex_lock(&_lock);
      
     //這句話代碼其實(shí)就是相當(dāng)于 NSMutableDictionary objecyForKey,取出鏈表節(jié)點(diǎn),這個(gè)NSMutableDictionary里面裝的是<_YYLinkedMapNode *>
     _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
     
     
     NSTimeInterval now = CACurrentMediaTime();
     if (node) {
     
          //如果有節(jié)點(diǎn),就把總內(nèi)存開(kāi)銷(xiāo)更新,并重新給節(jié)點(diǎn)各個(gè)數(shù)據(jù)賦值
          _lru->_totalCost -= node->_cost;
          _lru->_totalCost += cost;
          node->_cost = cost;
          node->_time = now;
          node->_value = object;
          
          //再拿到鏈表頂部
         [_lru bringNodeToHead:node];
     } else {
     
        //如果原來(lái)鏈表沒(méi)有,就新建節(jié)點(diǎn),各種賦值
         node = [_YYLinkedMapNode new];
         node->_cost = cost;
         node->_time = now;
         node->_key = key;
         node->_value = object;
         //插入到頂部
         [_lru insertNodeAtHead:node];
    }
        if (_lru->_totalCost > _costLimit) {
        
        //如果鏈表個(gè)數(shù)大于最大個(gè)數(shù)限制,就把末尾的刪掉
         dispatch_async(_queue, ^{
             [self trimToCost:_costLimit];
         });
      }
        if (_lru->_totalCount > _countLimit) {
          _YYLinkedMapNode *node = [_lru removeTailNode];
         if (_lru->_releaseAsynchronously) {
            dispatch_queue_t queue = _lru-  >_releaseOnMainThread ? dispatch_get_main_queue() :     YYMemoryCacheGetReleaseQueue();
          dispatch_async(queue, ^{
             [node class]; //hold and release in queue
         });
         } else if (_lru->_releaseOnMainThread && !pthread_main_np()) {
              dispatch_async(dispatch_get_main_queue(), ^{
                 [node class]; //hold and release in queue
            });
           }
         }
         
        //解鎖
       pthread_mutex_unlock(&_lock);
    }

取出數(shù)據(jù)

- (id)objectForKey:(id)key {
     if (!key) return nil;
     //加鎖
    pthread_mutex_lock(&_lock);
    
   _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
   
     if (node) {
     //如果存在就取出,并把節(jié)點(diǎn)添加到鏈表頭部
        node->_time = CACurrentMediaTime();
        [_lru bringNodeToHead:node];
     }
     //解鎖
     pthread_mutex_unlock(&_lock);
     //沒(méi)有返回Nil
    return node ? node->_value : nil;
}

定時(shí)清理

這里就是區(qū)別普通NSDictionary緩存的地方之一,不斷在后臺(tái)更新緩存數(shù)據(jù),清理過(guò)去數(shù)據(jù),只要設(shè)置一個(gè)_autoTrimInterval時(shí)間間隔就好。

- (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];
     });
    }

- (void)_trimInBackground {
     dispatch_async(_queue, ^{
         //清理直到達(dá)到大小限制
         [self _trimToCost:self->_costLimit];
         //清理直到達(dá)到個(gè)數(shù)限制
         [self _trimToCount:self->_countLimit];
         //清理直到達(dá)到時(shí)間限制
         [self _trimToAge:self->_ageLimit];
      });
    }   

YYKVStorage


要解析YYDiskCache首先得解析YYKVStorage,我發(fā)現(xiàn)這里主要用了2種存儲(chǔ)方式,sqlLite文件存儲(chǔ)。一開(kāi)始并不明白為何這么做,后來(lái)參考網(wǎng)上資料:

該文件主要以?xún)煞N方式來(lái)實(shí)現(xiàn)磁盤(pán)存儲(chǔ):SQLite、File,使用兩種方式混合進(jìn)行存儲(chǔ)主要為了提高讀寫(xiě)效率。寫(xiě)入數(shù)據(jù)時(shí),SQLite要比文件的方式更快;讀取數(shù)據(jù)的速度主要取決于文件的大小。據(jù)測(cè)試,在iPhone6中,當(dāng)文件大小超過(guò)20kb時(shí),F(xiàn)ile要比SQLite快的多。所以當(dāng)大文件存儲(chǔ)時(shí)建議用File的方式,小文件更適合用SQLite。

所以,主要還是要顧及到存儲(chǔ)速度吧。

添加數(shù)據(jù)

- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData {
    if (key.length == 0 || value.length == 0) return NO;
  if (_type == YYKVStorageTypeFile && filename.length == 0) {
       return NO;
     }
     if (filename.length) {   
      // filename存在 SQLite File兩種方式并行
         // 用文件進(jìn)行存儲(chǔ)
         if (![self _fileWriteWithName:filename data:value]) {
            return NO;
        }
         // 用SQLite進(jìn)行存儲(chǔ)
        if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
        // 當(dāng)使用SQLite方式存儲(chǔ)失敗時(shí),刪除本地文件存儲(chǔ)
           [self _fileDeleteWithName:filename];
          return NO;
       }
     return YES;
  } else {            
    // filename不存在采用SQLite進(jìn)行存儲(chǔ)
     if (_type != YYKVStorageTypeSQLite) {
        // 這邊去到filename后,刪除filename對(duì)應(yīng)的file文件
          NSString *filename = [self _dbGetFilenameWithKey:key];
          if (filename) {
               [self _fileDeleteWithName:filename];
          }
       }
     // SQLite 進(jìn)行存儲(chǔ)
        return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
 }
}

獲取數(shù)據(jù)

- (NSData *)getItemValueForKey:(NSString *)key {
    if (key.length == 0) return nil;
     NSData *value = nil;
    switch (_type) {
         case YYKVStorageTypeFile: { //File
              NSString *filename = [self _dbGetFilenameWithKey:key];
            if (filename) {
            
                 // 根據(jù)filename獲取File
                 value = [self _fileReadWithName:filename];
                 if (!value) {
                 
                      // 當(dāng)value不存在,用對(duì)應(yīng)的key刪除SQLite文件
                     [self _dbDeleteItemWithKey:key];
                    value = nil;
                 }
            }
         } break;
         case YYKVStorageTypeSQLite: {
         
             // SQLite 方式獲取
             value = [self _dbGetValueWithKey:key];
         } break;
         case YYKVStorageTypeMixed: {
             NSString *filename = [self _dbGetFilenameWithKey:key];
             
             // filename 存在文件獲取,不存在SQLite方式獲取
                if (filename) {
                 value = [self _fileReadWithName:filename];
                 if (!value) {
                    [self _dbDeleteItemWithKey:key];
                    value = nil;
              }
             } else {
              value = [self _dbGetValueWithKey:key];
             }
      } break;
     }
        if (value) {
        
          // 更新文件操作時(shí)間
          [self _dbUpdateAccessTimeWithKey:key];
     }
        return value;
}

總得來(lái)說(shuō)就是根據(jù)對(duì)文件進(jìn)行file和sqlLite方式進(jìn)行存儲(chǔ)。

YYDiskCache


YYDiskCache的核心內(nèi)容就是 YYKVStorage,它是YYKVStorage的拓展。

存儲(chǔ)

- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
     if (!key) return;
     if (!object) {
         [self removeObjectForKey:key];
         return;
  }
//獲取要擴(kuò)展的數(shù)據(jù)信息(就是后面跟一段數(shù)據(jù))
     NSData *extendedData = [YYDiskCache getExtendedDataFromObject:object];

      NSData *value = nil;
     if (_customArchiveBlock) {
         //如果有定義customArchiveBlock這個(gè)block就回調(diào)
         value = _customArchiveBlock(object);
    } else {
    @try {
        //將數(shù)據(jù)對(duì)象解析成NSData
        value = [NSKeyedArchiver archivedDataWithRootObject:object];
    }
    @catch (NSException *exception) {
        // nothing to do...
    }
}
    if (!value) return;
    NSString *filename = nil;
    
    //這里的_kv就是上面提到的YYKVStorage類(lèi)型
    if (_kv.type != YYKVStorageTypeSQLite) {
        if (value.length > _inlineThreshold) {
        //如果數(shù)據(jù)長(zhǎng)度達(dá)到一定條件就sqlite和文件存儲(chǔ)2種方式同時(shí)進(jìn)行,這里的filename就是關(guān)鍵字md5加密
            filename = [self _filenameForKey:key];
        }
    }
    //設(shè)置鎖,這里的Lock是宏定義用的是dispatch_semaphore_wait
    Lock();
    //用YYKVStorage存儲(chǔ)
    [_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
    //解鎖
    Unlock();
}

#pragma mark 用runtime添加擴(kuò)展屬性
+ (NSData *)getExtendedDataFromObject:(id)object {
     if (!object) return nil;
     return (NSData *)objc_getAssociatedObject(object, &extended_data_key);
}

+ (void)setExtendedData:(NSData *)extendedData toObject:(id)object {
     if (!object) return;
     objc_setAssociatedObject(object, &extended_data_key, extendedData, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

獲取數(shù)據(jù)

- (id<NSCoding>)objectForKey:(NSString *)key {
     if (!key) return nil;
     Lock();
     YYKVStorageItem *item = [_kv getItemForKey:key];
     Unlock();
     if (!item.value) return nil;

     id object = nil;
     if (_customUnarchiveBlock) {
         object = _customUnarchiveBlock(item.value);
     } else {
         @try {
             object = [NSKeyedUnarchiver unarchiveObjectWithData:item.value];
          }
         @catch (NSException *exception) {
         // nothing to do...
         }
    }
     if (object && item.extendedData) {
            [YYDiskCache setExtendedData:item.extendedData toObject:object];
     }
     return object;
}

總結(jié)


YYCache還是比較簡(jiǎn)單的,解析起來(lái)并不難。也有很多值得學(xué)習(xí)的地方,比如線程安全 、sqlLite和文件并行存儲(chǔ)LRU算法的實(shí)現(xiàn)。

參考文獻(xiàn)

http://www.cocoachina.com/ios/20160810/17335.html

http://blog.ibireme.com/2015/10/26/yycache/

我是翻滾的牛寶寶,歡迎大家評(píng)論交流~

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

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

  • YYCache是用于Objective-C中用于緩存的第三方框架。此文主要用來(lái)講解該框架的實(shí)現(xiàn)細(xì)節(jié),性能分析、設(shè)計(jì)...
    JonesCxy閱讀 689評(píng)論 0 2
  • YYCache簡(jiǎn)介 YYCache由YYMemoryCache(高速內(nèi)存緩存)和YYDiskCache(低速磁盤(pán)緩...
    簡(jiǎn)書(shū)lu閱讀 1,529評(píng)論 0 5
  • YYCache是用于Objective-C中用于緩存的第三方框架。此文主要用來(lái)講解該框架的實(shí)現(xiàn)細(xì)節(jié),性能分析、設(shè)計(jì)...
    Panda_iOS閱讀 987評(píng)論 1 4
  • 從三月份找實(shí)習(xí)到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂(lè)視家的研發(fā)崗...
    時(shí)芥藍(lán)閱讀 42,808評(píng)論 11 349
  • 感悟一 再多的理論都抵不過(guò)實(shí)踐來(lái)的教育快。人要學(xué)會(huì)反思和總結(jié),這點(diǎn)很重要??腿烁屹I(mǎi)東西,這些東西是我的,價(jià)格也是...
    小木人_1b70閱讀 249評(píng)論 0 1

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