YYKit源码分析之YYCache

标题

最近YYKit在IOS各大论坛讨论得火热,其代码简单、高效令人惊叹。我也凑凑热闹,抱着学习为目的的心态解析下ibireme的代码。这里从比较简单的YYCache开始入手,下面是该目录结构。

YYCache目录结构

YYCache


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

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

  • YYMemoryCache:内存缓存,并且所有API都是线程安全的。

  • YYDiskCache:磁盘缓存,主要用SQLite和文件存储,并且所有API都是线程安全的

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

LRU算法


在YYCache的YYMemoryCacheYYDiskCache中都采用LRU算法进行快速存取,主要是通过双向链表NSMutableDictionry来实现。下面这张图很好诠释了LRU算法。

LRU算法原理
  • 用双向链表来表示堆栈
  • 新加入的数据存在栈顶
  • 使用缓存的时候,从栈中查找,如果命中,就把数据移到栈顶
  • 可以设置栈最大长度,超过长度就把栈尾数据删除

通过以上的规则,一个简单的LRU算法就得以实现。

线程安全控制(锁)


分析YYCache的时候,我发现作者用了很多锁来保证线程安全。这是值得我学习的地方,因为以前我根本没有考虑过线程问题。

在这里YYCache主要用了2种锁:pthread_mutexdispatch_semaphore,下面是作者自己的分析:

OSSpinLock 自旋锁,性能最高的锁。原理很简单,就是一直 do while 忙等。它的缺点是当等待时会消耗大量 CPU 资源,所以它不适用于较长时间的任务。对于内存缓存的存取来说,它非常合适。

dispatch_semaphore 是信号量,但当信号总量设为 1 时也可以当作锁来。在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。对磁盘缓存来说,它比较合适。

为此我也特地补了下课,pthread_mutex其实也是利用OSSpinLock实现的,还有其他的一些锁比如NSLock@synchronized,这些使用也很方便,网上资料也很多。我简单测试了下,OSSpinLock相对性能最高,@synchronized相对性能差些,具体的也可以自己实验一下。

线程安全就是说多线程访问同一代码,不会产生不确定的结果。如果在执行代码前加锁,只有等这段代码完成后才解锁,这样就不会出现因多线程而出现竞争资源等问题,从而实现线程安全。

双向链表结构


我们先来看下链表的节点,可以看出主要是上一个节点指针,下一个节点指针,key值value值,节点开销大小,缓存时间戳等部分

@interface _YYLinkedMapNode : NSObject {
   @package
  __unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic 上一个节点
 __unsafe_unretained _YYLinkedMapNode *_next; // retained by dic 下一个节点
  id _key; //节点key值
  id _value; //节点value值
  NSUInteger _cost;//内存开销大小
  NSTimeInterval _time;//缓存时间
}

我们再来看下链表的结构,代码都添上了中文注释:

@interface _YYLinkedMap : NSObject {
     @package
      CFMutableDictionaryRef _dic; // 字典的Ref(理解成字典标示)
      NSUInteger _totalCost; //链表总开销
      NSUInteger _totalCount; //链表个数
      _YYLinkedMapNode *_head; // 链表首个节点指针
      _YYLinkedMapNode *_tail; // 链表末尾节点指针
      BOOL _releaseOnMainThread; //是否在主线程释放内存
      BOOL _releaseAsynchronously;//是否异步释放内存
    }

    /// 插入一个节点,并且更新链表总开销
    /// Node and node.key should not be nil.
    - (void)insertNodeAtHead:(_YYLinkedMapNode *)node;

    /// 将一个节点放到链表顶部
    /// Node should already inside the dic.
   - (void)bringNodeToHead:(_YYLinkedMapNode *)node;

    /// 移除一个节点
    /// Node should already inside the dic.
    - (void)removeNode:(_YYLinkedMapNode *)node;

    ///移除尾部节点,淘汰数据
    - (_YYLinkedMapNode *)removeTailNode;

    /// 移除所有节点
    - (void)removeAll;

    @end

下面这张图很好得解释了整个链表结构,如果有数据结构基础读懂这个双向链表应该很容易。

链表结构

YYMemoryCache


由于代码还是比较简单的,所以我打算用在源码上注释的方式解释,也就不画流程图了。

添加数据

- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
  if (!key) return;
  if (!object) {
      [self removeObjectForKey:key];
        return;
 }
 
    //这里开始加锁
    pthread_mutex_lock(&_lock);
      
     //这句话代码其实就是相当于 NSMutableDictionary objecyForKey,取出链表节点,这个NSMutableDictionary里面装的是<_YYLinkedMapNode *>
     _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
     
     
     NSTimeInterval now = CACurrentMediaTime();
     if (node) {
     
          //如果有节点,就把总内存开销更新,并重新给节点各个数据赋值
          _lru->_totalCost -= node->_cost;
          _lru->_totalCost += cost;
          node->_cost = cost;
          node->_time = now;
          node->_value = object;
          
          //再拿到链表顶部
         [_lru bringNodeToHead:node];
     } else {
     
        //如果原来链表没有,就新建节点,各种赋值
         node = [_YYLinkedMapNode new];
         node->_cost = cost;
         node->_time = now;
         node->_key = key;
         node->_value = object;
         //插入到顶部
         [_lru insertNodeAtHead:node];
    }
        if (_lru->_totalCost > _costLimit) {
        
        //如果链表个数大于最大个数限制,就把末尾的删掉
         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);
    }

取出数据

- (id)objectForKey:(id)key {
     if (!key) return nil;
     //加锁
    pthread_mutex_lock(&_lock);
    
   _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
   
     if (node) {
     //如果存在就取出,并把节点添加到链表头部
        node->_time = CACurrentMediaTime();
        [_lru bringNodeToHead:node];
     }
     //解锁
     pthread_mutex_unlock(&_lock);
     //没有返回Nil
    return node ? node->_value : nil;
}

定时清理

这里就是区别普通NSDictionary缓存的地方之一,不断在后台更新缓存数据,清理过去数据,只要设置一个_autoTrimInterval时间间隔就好。

- (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, ^{
         //清理直到达到大小限制
         [self _trimToCost:self->_costLimit];
         //清理直到达到个数限制
         [self _trimToCount:self->_countLimit];
         //清理直到达到时间限制
         [self _trimToAge:self->_ageLimit];
      });
    }   

YYKVStorage


要解析YYDiskCache首先得解析YYKVStorage,我发现这里主要用了2种存储方式,sqlLite文件存储。一开始并不明白为何这么做,后来参考网上资料:

该文件主要以两种方式来实现磁盘存储:SQLite、File,使用两种方式混合进行存储主要为了提高读写效率。写入数据时,SQLite要比文件的方式更快;读取数据的速度主要取决于文件的大小。据测试,在iPhone6中,当文件大小超过20kb时,File要比SQLite快的多。所以当大文件存储时建议用File的方式,小文件更适合用SQLite。

所以,主要还是要顾及到存储速度吧。

添加数据

- (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两种方式并行
         // 用文件进行存储
         if (![self _fileWriteWithName:filename data:value]) {
            return NO;
        }
         // 用SQLite进行存储
        if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
        // 当使用SQLite方式存储失败时,删除本地文件存储
           [self _fileDeleteWithName:filename];
          return NO;
       }
     return YES;
  } else {            
    // filename不存在采用SQLite进行存储
     if (_type != YYKVStorageTypeSQLite) {
        // 这边去到filename后,删除filename对应的file文件
          NSString *filename = [self _dbGetFilenameWithKey:key];
          if (filename) {
               [self _fileDeleteWithName:filename];
          }
       }
     // SQLite 进行存储
        return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
 }
}

获取数据

- (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) {
            
                 // 根据filename获取File
                 value = [self _fileReadWithName:filename];
                 if (!value) {
                 
                      // 当value不存在,用对应的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) {
        
          // 更新文件操作时间
          [self _dbUpdateAccessTimeWithKey:key];
     }
        return value;
}

总得来说就是根据对文件进行file和sqlLite方式进行存储。

YYDiskCache


YYDiskCache的核心内容就是 YYKVStorage,它是YYKVStorage的拓展。

存储

- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
     if (!key) return;
     if (!object) {
         [self removeObjectForKey:key];
         return;
  }
//获取要扩展的数据信息(就是后面跟一段数据)
     NSData *extendedData = [YYDiskCache getExtendedDataFromObject:object];

      NSData *value = nil;
     if (_customArchiveBlock) {
         //如果有定义customArchiveBlock这个block就回调
         value = _customArchiveBlock(object);
    } else {
    @try {
        //将数据对象解析成NSData
        value = [NSKeyedArchiver archivedDataWithRootObject:object];
    }
    @catch (NSException *exception) {
        // nothing to do...
    }
}
    if (!value) return;
    NSString *filename = nil;
    
    //这里的_kv就是上面提到的YYKVStorage类型
    if (_kv.type != YYKVStorageTypeSQLite) {
        if (value.length > _inlineThreshold) {
        //如果数据长度达到一定条件就sqlite和文件存储2种方式同时进行,这里的filename就是关键字md5加密
            filename = [self _filenameForKey:key];
        }
    }
    //设置锁,这里的Lock是宏定义用的是dispatch_semaphore_wait
    Lock();
    //用YYKVStorage存储
    [_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
    //解锁
    Unlock();
}

#pragma mark 用runtime添加扩展属性
+ (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);
}

获取数据

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

总结


YYCache还是比较简单的,解析起来并不难。也有很多值得学习的地方,比如线程安全sqlLite和文件并行存储LRU算法的实现

参考文献

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

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

我是翻滚的牛宝宝,欢迎大家评论交流~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容

  • YYCache是用于Objective-C中用于缓存的第三方框架。此文主要用来讲解该框架的实现细节,性能分析、设计...
    JonesCxy阅读 519评论 0 2
  • YYCache简介 YYCache由YYMemoryCache(高速内存缓存)和YYDiskCache(低速磁盘缓...
    简书lu阅读 1,347评论 0 5
  • YYCache是用于Objective-C中用于缓存的第三方框架。此文主要用来讲解该框架的实现细节,性能分析、设计...
    Panda_iOS阅读 796评论 1 4
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,001评论 11 349
  • 感悟一 再多的理论都抵不过实践来的教育快。人要学会反思和总结,这点很重要。客人跟我买东西,这些东西是我的,价格也是...
    小木人_1b70阅读 213评论 0 1