美文网首页
iOS YYCache源码阅读

iOS YYCache源码阅读

作者: 某非著名程序员 | 来源:发表于2020-05-12 08:57 被阅读0次

    1. 初始化

    [[YYCache alloc] initWithName:@""];

    - (instancetype) init {
        NSLog(@"Use \"initWithName\" or \"initWithPath\" to create YYCache instance.");
        return [self initWithPath:@""];
    }
    
    - (instancetype)initWithName:(NSString *)name {
        if (name.length == 0) return nil;
        NSString *cacheFolder = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
        NSString *path = [cacheFolder stringByAppendingPathComponent:name];
        return [self initWithPath:path];
    }
    
    - (instancetype)initWithPath:(NSString *)path {
        if (path.length == 0) return nil;
        YYDiskCache *diskCache = [[YYDiskCache alloc] initWithPath:path];
        if (!diskCache) return nil;
        NSString *name = [path lastPathComponent];
        YYMemoryCache *memoryCache = [YYMemoryCache new];
        memoryCache.name = name;
        
        self = [super init];
        _name = name;
        _diskCache = diskCache;
        _memoryCache = memoryCache;
        return self;
    }
    
    + (instancetype)cacheWithName:(NSString *)name {
        return [[self alloc] initWithName:name];
    }
    
    + (instancetype)cacheWithPath:(NSString *)path {
        return [[self alloc] initWithPath:path];
    }
    
    1. YYCache提供静态方法和实例方法初始化,静态方法本质还是调用实例方法初始化。
    2. 实例方法提供name和path初始化,name会构建路径使用path方法进行初始化
    3. 初始化时有YYMemoryCache和YYDiskCache。
    4. 使用name进行初始化时,每个name对应都会生成一个文件夹,数据库。

    2. YYMemoryCache内存缓存

    - (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
        if (!key) return;
        if (!object) {
            [self removeObjectForKey:key];
            return;
        }
        pthread_mutex_lock(&_lock);
        _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);
    }
    

    从setObject:forKey:withCost看出YYMemoryCache的重要数据结构_YYLinkedMap

    2.1 _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;
    }
    
    - (void)insertNodeAtHead:(_YYLinkedMapNode *)node{
      ...
    }
    - (void)bringNodeToHead:(_YYLinkedMapNode *)node{
      ...
    }
    

    _YYLinkedMap是一个双向链接。
    insertNodeAtHead如果是新数据时,会把node插入在链表头部
    bringNodeToHead在设置缓存时,如果链表已经包含node节点,会移动节点到头部。
    CFMutableDictionaryRef _dic,存储了key,model,方便快速查找。

    2.2 setObject:forKey

    - (void)setObject:(id)object forKey:(id)key {
        [self setObject:object forKey:key withCost:0];
    }
    
    - (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
        if (!key) return;
        if (!object) {
            [self removeObjectForKey:key];
            return;
        }
        pthread_mutex_lock(&_lock);
        _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);
    }
    
    1. key不能为空,为空直接return
    2. value不能为空,为空则需要移除对应的key
    3. 获取缓存中的node
    • 如果node为空,生成一个node节点,insertNodeAtHead插入到链表头部
    • 如果node不为空,bringNodeToHead 修改时间并把node节点移动到链表头部
    • 采用LRU算法,使用过即在最前面
    1. trimToCost:移除超出Cost的节点,removeTailNode移除掉超出count节点

    2.3 _trimToCost

    - (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
            });
        }
    }
    
    - (instancetype)init {
        self = super.init;
        ...
        _countLimit = NSUIntegerMax;
        _costLimit = NSUIntegerMax;
        _ageLimit = DBL_MAX;
        ...
    }
    
    1. costLimit = 0时,标识_costLimit已经初始化;_lru->_totalCost <= costLimit 在正常范围内
    2. finish为NO,移除超出cost的节点;pthread_mutex_trylock判断是否有其他线程锁了,则10ms之后再试;否则移除removeTailNode
    - (_YYLinkedMapNode *)removeTailNode {
        if (!_tail) return nil;
        _YYLinkedMapNode *tail = _tail;
        CFDictionaryRemoveValue(_dic, (__bridge const void *)(_tail->_key));
        _totalCost -= _tail->_cost;
        _totalCount--;
        if (_head == _tail) {
            _head = _tail = nil;
        } else {
            _tail = _tail->_prev;
            _tail->_next = nil;
        }
        return tail;
    }
    
    1. 移除_dic中的key,value
    2. 尾部指针指向尾部倒数第二个指针,再把最后一个指针置nil

    2.4 countLimit

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

    这一段只移除tailNode,而没有循环判断,countLimit是精确的,每次增加或修改了countLimit.

    2.5 objectForKey:

    - (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;
    }
    
    1. 使用时从字典获取node
    2. 如果node不为空,更新时间,把节点移动到头部。
      符合LRU算法

    2.6 内存管理

    - (instancetype)init {
        self = super.init;
        pthread_mutex_init(&_lock, NULL);
        _lru = [_YYLinkedMap new];
        _queue = dispatch_queue_create("com.ibireme.cache.memory", DISPATCH_QUEUE_SERIAL);
        
        _countLimit = NSUIntegerMax;
        _costLimit = NSUIntegerMax;
        _ageLimit = DBL_MAX;
        _autoTrimInterval = 5.0;
        _shouldRemoveAllObjectsOnMemoryWarning = YES;
        _shouldRemoveAllObjectsWhenEnteringBackground = YES;
        
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidReceiveMemoryWarningNotification) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidEnterBackgroundNotification) name:UIApplicationDidEnterBackgroundNotification object:nil];
        
        [self _trimRecursively];
        return self;
    }
    

    2.6.1 清空链表数据

    收到内存警告或进入后台时,清空缓存数据

    2.6.2 _trimRecursively

    每隔_autoTrimInterval时默认5s,检查Cost,Count,Age并清空过期数据

    3. YYDiskCache持久化缓存

    - (instancetype)initWithPath:(NSString *)path {
        return [self initWithPath:path inlineThreshold:1024 * 20]; // 20KB
    }
    
    - (instancetype)initWithPath:(NSString *)path
                 inlineThreshold:(NSUInteger)threshold {
        self = [super init];
        if (!self) return nil;
        
        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 *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);
        
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appWillBeTerminated) name:UIApplicationWillTerminateNotification object:nil];
        return self;
    }
    
    1. initWithPath默认大小20kb
    2. 存储的核心类是YYKVStorage,存储分文件、数据库及混合存储。
    3. YYKVStorage封装了数据库和文件的操作。

    3.1 _trimRecursively

    磁盘60s会检查一次,也可以手动设置

    - (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 {
        __weak typeof(self) _self = self;
        dispatch_async(_queue, ^{
            __strong typeof(_self) self = _self;
            if (!self) return;
            Lock();
            [self _trimToCost:self.costLimit];
            [self _trimToCount:self.countLimit];
            [self _trimToAge:self.ageLimit];
            [self _trimToFreeDiskSpace:self.freeDiskSpaceLimit];
            Unlock();
        });
    }
    

    3.2 setObject:forKey

    - (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) {
            value = _customArchiveBlock(object);
        } else {
            @try {
                value = [NSKeyedArchiver archivedDataWithRootObject:object];//转成NSData
            }
            @catch (NSException *exception) {
                // nothing to do...
            }
        }
        if (!value) return;
        NSString *filename = nil;
        if (_kv.type != YYKVStorageTypeSQLite) {
            if (value.length > _inlineThreshold) {
                filename = [self _filenameForKey:key];
            }
        }
        
        Lock();
        [_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
        Unlock();
    }
    
    1. _customArchiveBlock自定义block,如果没有则使用系统方法object转data
    2. saveItemWithKey保存key,value
    - (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) {
            if (![self _fileWriteWithName:filename data:value]) {
                return NO;
            }
            if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
                [self _fileDeleteWithName:filename];
                return NO;
            }
            return YES;
        } else {
            if (_type != YYKVStorageTypeSQLite) {
                NSString *filename = [self _dbGetFilenameWithKey:key];
                if (filename) {
                    [self _fileDeleteWithName:filename];
                }
            }
            return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
        }
    }
    
    1. filename不为空时,_fileWriteWithName写入文件
    2. _dbSaveWithKey保存到数据库

    3.3 objectForKey:

    - (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];//data转成对象
            }
            @catch (NSException *exception) {
                // nothing to do...
            }
        }
        if (object && item.extendedData) {
            [YYDiskCache setExtendedData:item.extendedData toObject:object];
        }
        return object;
    }
    
    - (YYKVStorageItem *)getItemForKey:(NSString *)key {
        if (key.length == 0) return nil;
        YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:NO];
        if (item) {
            [self _dbUpdateAccessTimeWithKey:key];//使用后需要更新时间
            if (item.filename) {
                item.value = [self _fileReadWithName:item.filename];
                if (!item.value) {
                    [self _dbDeleteItemWithKey:key];
                    item = nil;
                }
            }
        }
        return item;
    }
    

    总结:

    1. YYCache很好的支持了对象的持久化问题,采用文件与数据库结合的方式进行存储。
    2. 同时对象存储采用序列化的方式,解决了不同对象差异的存储问题。
    3. 内存采用双链表实现LRU算法,磁盘存储采用最后更新时间的方式来实现LRU算法。

    这里我有个小疑问:磁盘采用60s的方式进行轮询清理,每个YYCache对象都有60s,当有多个YYCache对象使用时,是否需要考虑下60s的长度问题。

    相关文章

      网友评论

          本文标题:iOS YYCache源码阅读

          本文链接:https://www.haomeiwen.com/subject/kirvthtx.html