YYCache源码分析

YYCache是OC用于缓存的第三方框架。

  • YYCache:同时实现内存缓存和磁盘缓存,且是线程安全的。
  • YYDiskCache:实现磁盘缓存,所有的API是线程安卓的,内部也采用了LRU淘汰算法,主要是SQLite和文件存储两种方式。
  • YYKVStorage:实现磁盘缓存,不推荐直接使用此类,该类不是线程安全的。
  • YYMemoryCache:实现内存缓存,所有的API是线程安全的,也是采用了LRU淘汰算法来提高性能。

LRU淘汰算法:

LRU(Least recently used,最近最少使用)算法,根据访问的历史记录来对数据进行淘汰。

  • 有新数据加入时添加到链表的头部。
  • 每当缓存命中(缓存数据被访问),则将数据移动到链表头部。
  • 每当链表满的时候,将链表尾部的数据丢弃。

在YYMemoryCache中使用双向链表和NSDictionary实现了LRU淘汰算法。

线程安全-锁:

YYCache使用到了两种锁

  • OSSpinLock:自旋锁
  • dispatch_semaphore:信号量,当信号量为1时充当锁进行使用

内存缓存使用的是pthread_mutex,由于pthread_mutex相当于do while忙等,等待时会消耗大量的CPU资源。

@interface _YYLinkedMapNode : NSObject {
    @package
    __unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic,指向上一个节点
    __unsafe_unretained _YYLinkedMapNode *_next; // retained by dic,指向下一个节点
    id _key;// 缓存的key
    id _value;// 缓存的对象
    NSUInteger _cost;// 内存消耗
    NSTimeInterval _time;// 缓存时间
}
@end

_YYLinkedMapNode:链表的节点

  • _prev、_next:分别表示指向上一个节点、下一个节点
  • _key:缓存的key
  • _value:缓存对象
  • _cost:内存消耗
  • _time:缓存时间
@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;// 是否在主线程释放_YYLinkedMapNode
    BOOL _releaseAsynchronously;// 是否异步释放_YYLinkedMapNode
}

_YYLinkedMap:链表

  • _dic:用来保存节点
  • _totalCost:总缓存开销
  • _head、_tail:头节点、尾节点
  • _releaseOnMainThread:是否在主线程释放_YYLinkedMapNode
  • _releaseAsynchronously:是否异步释放_YYLinkedMapNode

  • 插入节点到头部(- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;)
  • 将某个节点移动到头部(- (void)bringNodeToHead:(_YYLinkedMapNode *)node;)
  • 移除特定节点(- (void)removeNode:(_YYLinkedMapNode *)node;)
  • 移除尾部节点(- (_YYLinkedMapNode *)removeTailNode;)
  • 移除所有节点(- (void)removeAll;)

移除所有节点源码:

- (void)removeAll {
    _totalCost = 0;
    _totalCount = 0;
    _head = nil;
    _tail = nil;
    if (CFDictionaryGetCount(_dic) > 0) {
        CFMutableDictionaryRef holder = _dic;
        _dic = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
        
        if (_releaseAsynchronously) {
            dispatch_queue_t queue = _releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
            dispatch_async(queue, ^{
                CFRelease(holder); // hold and release in specified queue
            });
        } else if (_releaseOnMainThread && !pthread_main_np()) {
            dispatch_async(dispatch_get_main_queue(), ^{
                CFRelease(holder); // hold and release in specified queue
            });
        } else {
            CFRelease(holder);
        }
    }
}

通过双向链表来对数据进行操作,以及使用NSDictionary实现了LRU淘汰算法。时间复杂度O(1),5种操作基本是都是对节点的基本操作。

YYMemoryCache

这边介绍两个主要操作:添加缓存,查找缓存

添加缓存

-(void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost
{
    if(!key)return;
    if(!object)
    {
        [self removeObjectForKey:key];// 缓存对象为nil时,直接移除
        return;
    }
    pthread_mutex_lock(&_lock);// 为了保证数据安全,数据操作前加锁
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic,(__briddge 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->_totalCount > _countLimit){// 判断当前的缓存进行是否超出了设定值,若超出则进行整理
        dispatch_async(_queue,^{
            [self trimToCost:_costLimit];
        });
    }
    if(_lru->_totalCost > _costLimit)// 每次添加数据仅有一个,数量上超出时,直接移除尾部那个object
    {
        _YYLinkedMapNode *node = [_lru removeTailNode];
        //线程操作,异步释放
        if(_lru->_releaseAsynchronousely)
        {
            dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue : YYMemoryCacgeGetReleaseQueue();
            dispatch_async(queue,^{
                [node class];//通过node来对其进行持有,以至于不会在方法调用结束的时候被销毁
            });
        }else if(_lru->_releaseOnMainThread && !pthread_main_np)
        {
            dispatch_async(dispatch_get_main_queue(),^{
                [node class];//通过node来对其进行持有,以至于不会在方法调用结束的时候被销毁
            });
        }
    }
    pthread_mutex_unlock(&_lock);// 操作结束,解锁
}

获取缓存

// 从memory 中取数据时,根据LRU原则,将最新取出的obejct放在栈头
-(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);
    return node ? node->_value : nil;
}    

YYKVStore

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

Save:

- (BOOL)saveItemWithKey:(NSString *)key
                  value:(NSData *)value
               filename:(nullable NSString *)filename
           extendedData:(nullable NSData *)extendedData
{
    //条件不符合
    if(key.length == 0 || value.length == 0)return NO;
    if(_type == YYKVStorageTypeFile && filename.length == 0)
    {
        return NO;    
    }
    if(filename.length)// filename 存在,sqlite 和filename两种方式进行
    {
        // 用文件进行存储
        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 != YYKVStorageTypeSDLite)
        {
            // 根据key删除对应的file文件
            NSString *filename = [self _dbGetFilenameWithKey:key];
            if(filename)
            {
                [self _fileDeleteWithName:filename];
            }
        }
        // sqlite 进行存储
        return [self _dbSaveWithKey:key value:value filename:nil extendedData:extendedData];
    }
}

Remove

-(BOOL)removeItemForkey:(NSString *)key
{
    if(key.length == 0) return NO;
    switch(_type)
    {
        case YYKVStorageSQLite:
        {
            return [self _dbDeleteItemWithKey:key];// 删除SQLite文件
        }break;
        case YYKVStorageTypeFile:
        case YYKVStorageTypeMixed:
        {
            NSString *filename = [self _dbGetFilenameWithKey:key];// 获取filename
            if(filename)
            {
                [self _dbDeleteWithName:filename];// 删除filename对应的file
            }
            return [self _dbDeleteItemWithKey:key];// 删除sqlite
        }break;
        default:return NO;
    }
}

Get

- (NSData *)getItemValueForKey:(NSString *)key
{
    if(key.length == 0) return nil;
    NSData *value = nil;
    switch(_type)
    {
        case YYKVStorageTypeFile:
        {
            NSString *filename = [self _dbGetFilenameWithKey:key];
            if(filename)// 根据filename获取File
            {
                value = [self _fileReadWithName:filename];
                if(!value)// 当value 不存在,用对应的key删除SQLite数据
                {
                    [self _dbDeleteItemWithKey:key];
                    return nil;
                }
            }
        }break;
        case YYKVStorageTypeSQLite:// SQLite方式获取
        {
            value = [self _dbGetValueWithKey:key];
        }break;
        case YYKVStorageTypeMixed:
        {
            NSString *filename = [self _dbGetFilenameWithKey:key];
            if(filename)//filename 存在文件获取,不存在SQLite方式获取
            {
                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方式主要是用的writeToFile进行存储,SQLite之际是用sqlite3来对文件进行操作。

YYDiskCache

YYDiskCache是对YYKVStorage进行的一次封装,是线程安全的,这边的操作使用的是dispatch_semaphore_signal来确保线程的安全。另外结合LRU淘汰算法,根据文件的大小自动选择存储方式来表达到更好的性能。

- (instancetype)initWithPath:(NSString *)path
             inlineThreshold:(NSUInteger)threshold
{
    self = [super init];
    if(!self) return nil;
    //获取缓存的 YYDiskCache
    YYDiskCache *globalCache = _YYDiskCacheGetGlobal(path);
    if(globalCache) return globalCache;

    //确定存储的方式
    YYKVStorageType type;
    if(threshold == 0){
        type = YYKVStorageTypeFile;
    }else if(threshold == NSUIntegerMax)
    {
        type = YYKVStorageTypeSQLite;    
    }else{
        type = YYKVStorageTypeMixed;
    }

    //初始化 YYKVStorage
    YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];
    if(!kv) return nil;
    
    //初始化数据
    _kv = kv;
    _path = path;
    _lock = dispatch_semaphore_create(1);
    _queue = dispatch_queue_create("com.ibireme.cache.disk",DISPATCH_QUEUE_CONCURRENT);
    _inlineThreshold = threshold;
    _countLimit = NSUIntegerMax;
    _costLimit = NSUIntegerMax;
    _ageLimit = DBL_MAX;
    _freeDiskSpaceLimit = 0;
    _autoTrimInterval = 60;
    
    // 递归的去整理文件
    [self _trimRecursively];
    //对当前对象进行缓存
    _YYDiskCacheSetGlobal(self);

    //通知 APP即将被杀死时
     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appWillBeTerminated) name:UIApplicationWillTerminateNotification object:nil];
    return self;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值