YYKit 源码分析(7)-YYCache

今天开始分析YYCache

包含的文件类

YYCache

YYMemoryCache

YYDiskCache

YYKVStorage


接下来分析每个文件的结构

YYCache

文件结构


这个类结构很简单,就三个属性,

1缓存的 name ,NSString

2记忆缓存memoryCache ,YYMemoryCache, 应该是代表内存,

3磁盘缓存diskCache,YYDiskCache,应该是代表磁盘

看这个结构,yycache 是memory 和disk的管理者。


缓存一般的策略先去内存查找数据,要是内存中有,就直接返回数据,要是内存中没有数据,那么我们就到磁盘上找数据,要是磁盘上没有就返回,要是磁盘上有,就将磁盘上的数据写入到内存中。



接下来我们看初始化

- (nullable instancetype)initWithName:(NSString *)name;

- (nullable instancetype)initWithPath:(NSString *)path NS_DESIGNATED_INITIALIZER;

+ (nullable instancetype)cacheWithName:(NSString *)name;

+ (nullable instancetype)cacheWithPath:(NSString *)path;

- (instancetype)init UNAVAILABLE_ATTRIBUTE;

+ (instancetype)new UNAVAILABLE_ATTRIBUTE;

六个方法,两个废弃的,废弃的不看。

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

}

这个函数我们看出来,通过该函数生成的YYCache 路径是NSCachesDirectory 路径下。

最后调用- (instancetype)initWithPath:(NSString *)path最终生成 YYCache  函数

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

}

这个函数真正生成YYCache对象

1 生成 YYDiskCache 对象

2生成YYMemoryCache 对象,并且设置name 

3生成self ,给相关变量赋值

+ (instancetype)cacheWithName:(NSString *)name {

return [[self alloc] initWithName:name];

}

+ (instancetype)cacheWithPath:(NSString *)path {

    return [[self alloc] initWithPath:path];

}


这个就是YYCache的类方法。

缓存么那么肯定就设计到增删改查

1 增

- (void)setObject:(id)object forKey:(NSString *)key {

 [_memoryCache setObject:object forKey:key];

[_diskCache setObject:object forKey:key];

}

增加数据,就是分别向_memoryCache 和 _diskCache 增加数据。具体实现到分析具体类再说

- (void)setObject:(id)object forKey:(NSString *)key withBlock:(void (^)(void))block {

    [_memoryCache setObject:object forKey:key];

    [_diskCache setObject:object forKey:key withBlock:block];

}

这个函数带个block 并且block 只在_diskCache 中使用。这应该是增加数据完成的block。因为向磁盘写数据,数据大。需要一定时间。

2 删

- (void)removeObjectForKey:(NSString *)key {

    [_memoryCache removeObjectForKey:key];

    [_diskCache removeObjectForKey:key];

}

_memoryCache 和 _diskCache 删除特定的数据

- (void)removeObjectForKey:(NSString *)key withBlock:(void (^)(NSString *key))block {

    [_memoryCache removeObjectForKey:key];

    [_diskCache removeObjectForKey:key withBlock:block];

}

这里有个block 和上面的一样,磁盘删除需要时间,可能是完成的回调

- (void)removeAllObjects {

    [_memoryCache removeAllObjects];

    [_diskCache removeAllObjects];

}

删除所有对象

- (void)removeAllObjectsWithBlock:(void(^)(void))block {

    [_memoryCache removeAllObjects];

    [_diskCache removeAllObjectsWithBlock:block];

}

这个也是磁盘上的回调有关,磁盘全部删除的回调

- (void)removeAllObjectsWithProgressBlock:(void(^)(int removedCount, int totalCount))progress

                                endBlock:(void(^)(BOOL error))end {

    [_memoryCache removeAllObjects];

    [_diskCache removeAllObjectsWithProgressBlock:progress endBlock:end];


}

这个多了个 ProgressBlock 进度条,暂时不关心,知道删除磁盘上的数据进度

3 改

这里没有可以改的api ,但是也可以实现改,我们就删除再设置就是改了。

4 查

- (id)objectForKey:(NSString *)key { id object = [_memoryCache objectForKey:key];

    if (!object) {

        object = [_diskCache objectForKey:key];

        if (object) {

            [_memoryCache setObject:object forKey:key];

        }

    }

    return object;

}

这里我们查询先从记忆内存中查找,要是查找不到,那就从磁盘上查找,查找到了,就将数据写入记忆内存中 。这个是同步查询

从这里看,应该是和我们上面写数据查询策略一样。

- (void)objectForKey:(NSString *)key withBlock:(void (^)(NSString *key, idobject))block { if (!block) return; idobject = [_memoryCache objectForKey:key]; if (object) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ block(key, object); }); } else { [_diskCache objectForKey:key withBlock:^(NSString *key, id object) {

            if (object && ![_memoryCache objectForKey:key]) {

                [_memoryCache setObject:object forKey:key];

            }

            block(key, object);

        }];

    }

这个是异步的查询。

这个要是在内存中查询,那么我们就在异步中回调这个block,内存中没查询到,就到磁盘上查找,

我们给磁盘上查找的回调进行处理,要是查询到了结果,我们就将其写入到记忆缓存中,再执行回调block。


- (BOOL)containsObjectForKey:(NSString *)key {

    return [_memoryCache containsObjectForKey:key] || [_diskCache containsObjectForKey:key];

}

查询内存中是否有指定的数据

- (void)containsObjectForKey:(NSString *)key withBlock:(void (^)(NSString *key, BOOL contains))block {

    if (!block) return;


    if ([_memoryCache containsObjectForKey:key]) {

        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

            block(key, YES);

        });

    } else  {

        [_diskCache containsObjectForKey:key withBlock:block];

    }

}

异步查询是否包含指定数据。


从YYCache 可以看出,每个api都有带block 和不带block样式的。带block的都是异步执行,不带block 是同步执行。

并且只有_diskCache 磁盘上的block 带有异步,而缓存中是不带有异步的,都是同步执行。



YYMemoryCache

接下来我们看看memory Cache 的数据结构


还有三个变量

   pthread_mutex_t _lock;

    _YYLinkedMap *_lru;

    dispatch_queue_t _queue;


属性比较多,暂时不分析每个的具体作用,我们已具体用法来看每个属性的含义。

这个类的初始化方法比较简单

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

}

我们看看初始化,具体初始化了啥东西。

1 初始化了锁_lock

2生成了_lru 对象

3生成一个队列,这个队列是串行的。

4 初始化 _countLimit _costLimit  ,_ageLimit 都是该类型最大值

5._autoTrimInterval 设置为5

6_shouldRemoveAllObjectsOnMemoryWarning 标记为记忆警告删除对象

7._shouldRemoveAllObjectsWhenEnteringBackground 当进入后台删除所有对象

8 增加内存警告通知

9增加进入后台通知

10调用- (void)_trimRecursively 方法

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

    });

}

#defineNSEC_PER_SEC 1000000000ull

#defineUSEC_PER_SEC 1000000ull

#defineNSEC_PER_USEC 1000ull

NSEC:纳秒。

USEC:微秒。

SEC:秒

这个函数是延迟五秒执行,因为延迟五秒可能导致对象销毁

__strong typeof(_self) self = _self;

        if (!self) return;

这样保证执行下面的函数,一定有self。

- (void)_trimInBackground {

    dispatch_async(_queue, ^{

        [self _trimToCost:self->_costLimit];

        [self _trimToCount:self->_countLimit];

        [self _trimToAge:self->_ageLimit];

    });

}

看看这个类启动,5秒都要执行这个函数。看看五秒都执行啥。

[self _trimToCost:self->_costLimit];

        [self _trimToCount:self->_countLimit];

        [self _trimToAge:self->_ageLimit];


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

        });

    }

}

分步骤分析

1.上锁

2.我们知道传入的是_costLimit ,要是_costLimit 是0 那么_lru 就删除所有对象

3.要是传入_lru 的_totalCost不大于_costLimit ,说明,_lru不满,那么也结束。

4.最后的情况就是_lru 对象超出目前的设置上限了。那么对数据进行处理。

5.试着上锁,要是_lru的_totalCost 价值大于self的,就删除最后一个node。要是上锁失败,那么就延迟10ms

6删除超出范围的node。

if (holder.count) {

        dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();

        dispatch_async(queue, ^{

            [holder count]; // release in queue

        });

    }

这个holder 是数组,要是没有这段代码,那么holder 在当前线程就释放掉了,那holder中的对象也就释放掉了,而用GCD 异步持有holder ,这样就让holder在当前线程不能释放,而在GCD 异步指向的线程中释放holder ,那么holder中的对象也就在异步线程中释放了。

- (void)_trimToCount:(NSUInteger)countLimit {

    BOOL finish = NO;

    pthread_mutex_lock(&_lock);

    if (countLimit == 0) {

        [_lru removeAll];

        finish = YES;

    } else if (_lru->_totalCount <= countLimit) {

        finish = YES;

    }

    pthread_mutex_unlock(&_lock);

    if (finish) return;


    NSMutableArray *holder = [NSMutableArray new];

    while (!finish) {

        if (pthread_mutex_trylock(&_lock) == 0) {

            if (_lru->_totalCount > countLimit) {

                _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

        });

    }

}

这个方法比较的是_totalCount ,和_totalCost 一样的实现方法。不做介绍

- (void)_trimToAge:(NSTimeInterval)ageLimit {

    BOOL finish = NO;

    NSTimeInterval now = CACurrentMediaTime();

    pthread_mutex_lock(&_lock);

    if (ageLimit <= 0) {

        [_lru removeAll];

        finish = YES;

    } else if (!_lru->_tail || (now - _lru->_tail->_time) <= ageLimit) {

        finish = YES;

    }

    pthread_mutex_unlock(&_lock);

    if (finish) return;


    NSMutableArray *holder = [NSMutableArray new];

    while (!finish) {

        if (pthread_mutex_trylock(&_lock) == 0) {

            if (_lru->_tail && (now - _lru->_tail->_time) > ageLimit) {

                _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

        });

    }

}

这样和上面的两个方法实现一样不做介绍。

初始化的时候我们初始化了_YYLinkedMap 这个类。我们看看这个类干嘛的

_YYLinkedMap

这个类赋值给了变量_lru 。什么是lru

1. LRU

1.1. 原理

LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

我只摘了上面博客的一段,有兴趣的朋友可以自己查询。

实现类似链表操作

类结构

这里面的_YYLinkedMapNode 结构


看这个类的变量,我们大概能猜出,这个类类似链表,_prev指向前面的_YYLinkedMapNode,_next指向下一个_YYLinkedMapNode。_value 是值,_key 类似_value 的hash值。


初始化是默认初始化

- (instancetype)init {

    self = [super init];

    _dic = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);

    _releaseOnMainThread = NO;

    _releaseAsynchronously = YES;

    return self;

}

初始化了字典变量,两个变量初始化

_releaseAsynchronously 默认是YES,根据字面意思,是释放对象异步

_releaseOnMainThread 默认是NO,根据字面也是,释放对象是主线程

接下来看看这个类的方法

- (void)insertNodeAtHead:(_YYLinkedMapNode *)node {

    CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node));

    _totalCost += node->_cost;

    _totalCount++;

    if (_head) {

        node->_next = _head;

        _head->_prev = node;

        _head = node;

    } else {

        _head = _tail = node;

    }

}

这个方法

1. 字典保存_YYLinkedMapNode,key 是_YYLinkedMapNode 中的key

2._totalCost 叠加_YYLinkedMapNode 的_cost

3._totalCount 累加一个对象

4.要是有head ,就将_YYLinkedMapNode 的next 指向_head 而 head 的_prev指向_YYLinkedMapNode ,head 指向_YYLinkedMapNode

5 其他的_head =_tail = node

见图解

只有一个node的时候,map的head 和tail指向

新增一个node


经过改函数,数据指向如下



其实就是头插入。而_head 的_prev 是nil _tail 的_next 是nil,这里只能是从_tail 向上查询,或者从_head 向下查询


- (void)bringNodeToHead:(_YYLinkedMapNode *)node {

    if (_head == node) return;


    if (_tail == node) {

        _tail = node->_prev;

        _tail->_next = nil;

    } else {

        node->_next->_prev = node->_prev;

        node->_prev->_next = node->_next;

    }

    node->_next = _head;

    node->_prev = nil;

    _head->_prev = node;

    _head = node;

}

这个分三种情况,

第一种情况,要是node 本身就是_head ,那么不用做任何操作

第二种情况,要是node 是_tail ,那么就将node 从链表上拆下来,让_tail 指向node 的_prev,并且将_tail 的_next 置空。再执行头插法

第三种情况,普通位置位置的node,将node拆下来,头插方法插入到链表头

见表

正常链表


我们拆下来node2


接着将node2 放入head



这个函数就是这样的操作

- (void)removeNode:(_YYLinkedMapNode *)node {

    CFDictionaryRemoveValue(_dic, (__bridge const void *)(node->_key));

    _totalCost -= node->_cost;

    _totalCount--;

    if (node->_next) node->_next->_prev = node->_prev;

    if (node->_prev) node->_prev->_next = node->_next;

    if (_head == node) _head = node->_next;

    if (_tail == node) _tail = node->_prev;

}

这个就是删除一个node

1.就是先将node拆下来

2.检查要是node是_head 那么久将_head指向node 下_next 

3检查要是node是_tail ,那么就将_tail 指向node的_prev

上面的看懂了这个不难,不做图了

不过这里有个问题,就是没有将_head 的_prev 清除,_tail 的_next 清除。

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

}

删除_tail ,

1检测tail 和_head是一个对象,那么说明,只有一个node 那么,删除最后一个,就啥都没了,所以_head和_tail 都是nil

2要是_tail 和_head 不是一个对象,说明node很多,那么,就将_tail 指向_tail_prev的上一个对象,并将_tail的_next 对象清除

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

       }

    }

}

删除所有对象,将所有数据清空。

这里有个变量起作用 了_releaseAsynchronously 异步执行删除

dispatch_queue_t queue = _releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();

            dispatch_async(queue, ^{

                CFRelease(holder); // hold and release in specified queue

            });

猛然一看这不可能产生死锁呢?要是queue是主线程,在主线程调用这个函数这不产生死锁呢?其实并不能,主线程异步调用主线程,会按照主线程的顺序依次执行下去。类似串行队列,将异步加入到主线程的事件放入主线程队列的队末尾依次执行。我测试的结果是这样的,不知道对不对,请大家指点。

单纯看这个类,我认为多少有点问题,在- (void)insertNodeAtHead:(_YYLinkedMapNode *)node方法中,这里非常依赖node的key,要是多次插入相同的key ,这里的结果会导致不准。这就要求所有的node的key需要不一样。不知道作者在具体使用的时候是咋处理的,后面慢慢看。

我们再来看看memory的增删改查

.增

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

}

根据传入的参数我们大概能看出来,内存中的数据是以key value 保存的。每个保存的对象都有一个key。

分步骤分析

1.没有key ,就返回

2.没有value ,就删除key

3.获取_lru->_dic 并且取dic中key值

4.要是获取到node ,那么就_lru->_totalCost 就要删除掉获取到的node的_cost,将获取到的node 重新赋值,将其加入到lru的head处

5.要是没有node ,生成node,赋值,那么就直接加入到lur的head出

6.每次加入新的node ,要检测下_lru中的_totalCost 是否超出限制,超出就调用-trimToCost

7针对_lru 的_totalCount 超出范围,就删除最后一个node就可以了,因为刚才只加入了一个对象


这里我们_YYLinkedMapNode 的的_time 其实就是当前时间

到这里,我们就清楚了YYMemoryCache 中三个属性的作用,

_countLimit 代表改缓存的对象的上限数量

_costLimit 代表该缓存所有对象cost值综合的上限,(想要这个属性起作用,就要给每个对象node增加权重cost,默认是0)

_ageLimit 代表每个对象从创建到销毁可以最长保持时间。

.删

- (void)removeObjectForKey:(id)key {

    if (!key) return;

    pthread_mutex_lock(&_lock);

    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));

    if (node) {

        [_lru removeNode:node];

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

}

这个很简单,就是从_lru->_dic 查找key,找到了。就异步释放掉对象。

- (void)removeAllObjects {

    pthread_mutex_lock(&_lock);

    [_lru removeAll];

    pthread_mutex_unlock(&_lock);

}


删除所有对象

.查

- (BOOL)containsObjectForKey:(id)key {

    if (!key) return NO;

    pthread_mutex_lock(&_lock);

    BOOL contains = CFDictionaryContainsKey(_lru->_dic, (__bridge const void *)(key));

    pthread_mutex_unlock(&_lock);

    return contains;

}

检查是否包含key。

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

}

获取key所对应的node,要是有node ,更新node的_time 并且将其带人到头上

其实现实还是很简单的,我们从这里学到什么了?

1.释放对象的时候,我们是异步里释放对象,而不是在当前线程释放。具体原因看看作者的原话

2.所有的node 对象释放都是在特定指定的线程里面释放。

3.上面说的_YYLinkedMap,- (void)insertNodeAtHead:(_YYLinkedMapNode *)node加入node的时候,在_dic 中一定不能包含node->key,这个是在YYMemoryCache 中保证的。(我疑问,为啥不把这个逻辑加入到这个函数里面,这样保证外界多个地方调用不用考虑_totalCost 或者是_totalCount 的正确性呢)

4 锁的使用,锁的是对象,只要操作_lru 对象就是加入锁。


还有其他的方法我们简单看看

- (NSUInteger)totalCount {

    pthread_mutex_lock(&_lock);

    NSUInteger count = _lru->_totalCount;

    pthread_mutex_unlock(&_lock);

    return count;

}

目前缓存中缓存对象的数量

- (NSUInteger)totalCost {

    pthread_mutex_lock(&_lock);

    NSUInteger totalCost = _lru->_totalCost;

    pthread_mutex_unlock(&_lock);

    return totalCost;

}

缓存中缓存对象的价值

- (BOOL)releaseOnMainThread {

    pthread_mutex_lock(&_lock);

    BOOL releaseOnMainThread = _lru->_releaseOnMainThread;

    pthread_mutex_unlock(&_lock);

    return releaseOnMainThread;

}

获取释放对象的线程是否是主线程

- (void)setReleaseOnMainThread:(BOOL)releaseOnMainThread {

    pthread_mutex_lock(&_lock);

    _lru->_releaseOnMainThread = releaseOnMainThread;

    pthread_mutex_unlock(&_lock);

}

设置释放对象是否再主线程释放

- (BOOL)releaseAsynchronously {

    pthread_mutex_lock(&_lock);

    BOOL releaseAsynchronously = _lru->_releaseAsynchronously;

    pthread_mutex_unlock(&_lock);

    return releaseAsynchronously;

}

释放对象是否是异步

- (void)setReleaseAsynchronously:(BOOL)releaseAsynchronously {

    pthread_mutex_lock(&_lock);

    _lru->_releaseAsynchronously = releaseAsynchronously;

    pthread_mutex_unlock(&_lock);

}

设置是否异步释放对象

- (void)trimToCount:(NSUInteger)count {

    if (count == 0) {

        [self removeAllObjects];

        return;

    }

    [self _trimToCount:count];

}

将缓存中对象数量降至count以下

- (void)trimToCost:(NSUInteger)cost {

    [self _trimToCost:cost];

}


将缓存中对象的cost 降至cost以下

- (void)trimToAge:(NSTimeInterval)age {

    [self _trimToAge:age];

}

将缓存中对象的保存最长时间降低至age以下

两个通知

- (void)_appDidReceiveMemoryWarningNotification {

    if (self.didReceiveMemoryWarningBlock) {

        self.didReceiveMemoryWarningBlock(self);

    }

    if (self.shouldRemoveAllObjectsOnMemoryWarning) {

        [self removeAllObjects];

    }

}

记忆缓存警告就删除所有对象。

- (void)_appDidEnterBackgroundNotification {

    if (self.didEnterBackgroundBlock) {

        self.didEnterBackgroundBlock(self);

    }

    if (self.shouldRemoveAllObjectsWhenEnteringBackground) {

        [self removeAllObjects];

    }

}

当进入后台就就删除所有对象。


YYKVStorage

这个类是YYDiskCache 的底层

先看结构



有三个属性,九个private变量,

接着看看初始化

只有一个指定初始化

- (instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type {

    if (path.length == 0 || path.length > kPathLengthMax) {

        NSLog(@"YYKVStorage init error: invalid path: [%@].", path);

        return nil;

    }

    if (type > YYKVStorageTypeMixed) {

        NSLog(@"YYKVStorage init error: invalid type: %lu.", (unsigned long)type);

        return nil;

    }


    self = [super init];

    _path = path.copy;

    _type = type;

    _dataPath = [path stringByAppendingPathComponent:kDataDirectoryName];

    _trashPath = [path stringByAppendingPathComponent:kTrashDirectoryName];

    _trashQueue = dispatch_queue_create("com.ibireme.cache.disk.trash", DISPATCH_QUEUE_SERIAL);

    _dbPath = [path stringByAppendingPathComponent:kDBFileName];

    _errorLogsEnabled = YES;

    NSError *error = nil;

    if (![[NSFileManager defaultManager] createDirectoryAtPath:path

                                  withIntermediateDirectories:YES

                                                    attributes:nil

                                                        error:&error] ||

        ![[NSFileManager defaultManager] createDirectoryAtPath:[path stringByAppendingPathComponent:kDataDirectoryName]

                                  withIntermediateDirectories:YES

                                                    attributes:nil

                                                        error:&error] ||

        ![[NSFileManager defaultManager] createDirectoryAtPath:[path stringByAppendingPathComponent:kTrashDirectoryName]

                                  withIntermediateDirectories:YES

                                                    attributes:nil

                                                        error:&error]) {

        NSLog(@"YYKVStorage init error:%@", error);

        return nil;

    }


    if (![self _dbOpen] || ![self _dbInitialize]) {

        // db file may broken...

        [self _dbClose];

        [self _reset]; // rebuild

        if (![self _dbOpen] || ![self _dbInitialize]) {

            [self _dbClose];

            NSLog(@"YYKVStorage init error: fail to open sqlite db.");

            return nil;

        }

    }

    [self _fileEmptyTrashInBackground]; // empty the trash if failed at last time

    return self;

}


我们看看初始化都干了啥事情

1.检查path是否合法

2.检查type 是否超出规定范围

3.给变量_path 和_type 赋值

4.给私有变量_dataPath,_trashPath赋值

5.给_trashQueue 变量赋值,是串行队列

6._dbPath 数据库路径

7._errorLogsEnabled 是否打印log 默认打印

8.创建文件夹,文件夹结构


9.初始化数据库

10.调用- (void)_fileEmptyTrashInBackground 方法,清除掉trash

我们看看步骤第九步,数据库初始化。分两步,打开数据库和数据初始化。

先看看打开数据库

- (BOOL)_dbOpen {

    if (_db) return YES;


    int result = sqlite3_open(_dbPath.UTF8String, &_db);

    if (result == SQLITE_OK) {

        CFDictionaryKeyCallBacks keyCallbacks = kCFCopyStringDictionaryKeyCallBacks;

        CFDictionaryValueCallBacks valueCallbacks = {0};

        _dbStmtCache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &keyCallbacks, &valueCallbacks);

        _dbLastOpenErrorTime = 0;

        _dbOpenErrorCount = 0;

        return YES;

    } else {

        _db = NULL;

        if (_dbStmtCache) CFRelease(_dbStmtCache);

        _dbStmtCache = NULL;

        _dbLastOpenErrorTime = CACurrentMediaTime();

        _dbOpenErrorCount++;


        if (_errorLogsEnabled) {

            NSLog(@"%s line:%d sqlite open failed (%d).", __FUNCTION__, __LINE__, result);

        }

        return NO;

    }

}

1.检查变量_db ,有说明数据库已经打开。

2.在路径_dbPath,创建数据库并且打开数据库。

3.创建数据库并且打开数据库成功后,我们创建_dbStmtCache 字典,初始化两个变量_dbLastOpenErrorTime,_dbOpenErrorCount 返回YES

4.要是失败,_db 置空。并且释放_dbStmtCache 变量。_dbLastOpenErrorTime记录当前失败时间,_dbOpenErrorCount 累加,返回NO

为啥这么写呢?这里作者做了一个二次打开的操作

再看看初始化

- (BOOL)_dbInitialize {

    NSString *sql = @"pragma journal_mode = wal; pragma synchronous = normal; create table if not exists manifest (key text, filename text, size integer, inline_data blob, modification_time integer, last_access_time integer, extended_data blob, primary key(key)); create index if not exists last_access_time_idx on manifest(last_access_time);";

    return [self _dbExecute:sql];

}

这是一条sqlite 语句

pragma journal_mode = wal

激活和配置WAL模式

PRAGMA journal_mode=WAL;,如果成功,会返回"wal"。

我不是很懂数据库。这里有个介绍wal模式的

介绍sqlite的wal模式很多,自行上网查询。

摘了网上其他的博客的优点

 优点:

      1.读和写可以完全地并发执行,不会互相阻塞(但是写之间仍然不能并发)。

      2.WAL在大多数情况下,拥有更好的性能(因为无需每次写入时都要写两个文件)。

      3.磁盘I/O行为更容易被预测。

      缺点:

      1.访问数据库的所有程序必须在同一主机上,且支持共享内存技术。

      2.每个数据库现在对应3个文件:.db,-wal,-shm。

      3.当写入数据达到GB级的时候,数据库性能将下降。

      4.3.7.0之前的SQLite无法识别启用了WAL机制的数据库文件。


pragma synchronous = normal

这个是设置模式的sqlite模式的选择。摘自文章

当synchronous设置为FULL (2), SQLite数据库引擎在紧急时刻会暂停以确定数据已经写入磁盘。这使系统崩溃或电源出问题时能确保数据库在重起后不会损坏。FULL synchronous很安全但很慢。

当synchronous设置为NORMAL, SQLite数据库引擎在大部分紧急时刻会暂停,但不像FULL模式下那么频繁。 NORMAL模式下有很小的几率(但不是不存在)发生电源故障导致数据库损坏的情况。但实际上,在这种情况 下很可能你的硬盘已经不能使用,或者发生了其他的不可恢复的硬件错误。

设置为synchronous OFF (0)时,SQLite在传递数据给系统以后直接继续而不暂停。若运行SQLite的应用程序崩溃, 数据不会损伤,但在系统崩溃或写入数据时意外断电的情况下数据库可能会损坏。另一方面,在synchronous OFF时 一些操作可能会快50倍甚至更多。在SQLite 2中,缺省值为NORMAL.而在3中修改为FULL。


剩下一条数据就是简单的sql语句了。

这里把key 设置为primary

主关键字(primary key)是表中的一个或多个字段,它的值用于唯一地标识表中的某一条记录。 

这里把last_access_time_idx  设置为index

索引(Index)是一种特殊的查找表,数据库搜索引擎用来加快数据检索。简单地说,索引是一个指向表中数据的指针。一个数据库中的索引与一本书后边的索引是非常相似的。

例如,如果您想在一本讨论某个话题的书中引用所有页面,您首先需要指向索引,索引按字母顺序列出了所有主题,然后指向一个或多个特定的页码。

索引有助于加快 SELECT 查询和 WHERE 子句,但它会减慢使用 UPDATE 和 INSERT 语句时的数据输入。索引可以创建或删除,但不会影响数据。

使用 CREATE INDEX 语句创建索引,它允许命名索引,指定表及要索引的一列或多列,并指示索引是升序排列还是降序排列。

索引也可以是唯一的,与 UNIQUE 约束类似,在列上或列组合上防止重复条目。

我就是随便摘录下其他人的文章,具体有啥好处,请自行查阅吧。

- (BOOL)_dbExecute:(NSString *)sql {

    if (sql.length == 0) return NO;

    if (![self _dbCheck]) return NO;


    char *error = NULL;

    int result = sqlite3_exec(_db, sql.UTF8String, NULL, NULL, &error);

    if (error) {

        if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite exec error (%d): %s", __FUNCTION__, __LINE__, result, error);

        sqlite3_free(error);

    }


    return result == SQLITE_OK;

}

接下来执行sql 语句,

这里我们每次执行sql 语言。都要就行数据库检查

- (BOOL)_dbCheck {

    if (!_db) {

        if (_dbOpenErrorCount < kMaxErrorRetryCount &&

            CACurrentMediaTime() - _dbLastOpenErrorTime > kMinRetryTimeInterval) {

            return [self _dbOpen] && [self _dbInitialize];

        } else {

            return NO;

        }

    }

    return YES;

}

不做介绍,这里数据库检查

这里看看数据库关闭

- (BOOL)_dbClose {

    if (!_db) return YES;


    int  result = 0;

    BOOL retry = NO;

    BOOL stmtFinalized = NO;


    if (_dbStmtCache) CFRelease(_dbStmtCache);

    _dbStmtCache = NULL;


    do {

        retry = NO;

        result = sqlite3_close(_db);

        if (result == SQLITE_BUSY || result == SQLITE_LOCKED) {

            if (!stmtFinalized) {

                stmtFinalized = YES;

                sqlite3_stmt *stmt;

                while ((stmt = sqlite3_next_stmt(_db, nil)) != 0) {

                    sqlite3_finalize(stmt);

                    retry = YES;

                }

            }

        } else if (result != SQLITE_OK) {

            if (_errorLogsEnabled) {

                NSLog(@"%s line:%d sqlite close failed (%d).", __FUNCTION__, __LINE__, result);

            }

        }

    } while (retry);

    _db = NULL;

    return YES;

}

这里具体怎么关闭。不太懂数据库不想说,就知道这个是关闭数据库就行了。

在初始化的时候,我们知道这里创建了数据库并且创建了表。


我们从作者博客中知道。disk的数据是保存在数据库中的。因此,在这个类里面不免都是好多数据库的操作。数据库操作不过于讲解,这个话题太大。只是简单略过。我们看看看看数据是如何增删改查的

看增删改查前先看看YYKVStorageItem类,增删改查设计到的类

YYKVStorageItem

结构

这个类感觉就是针对数据库表设计的。和表键值一一对应,具体对应。等后面看数据库最后的插入时候对应看。

- (BOOL)saveItem:(YYKVStorageItem *)item {

    return [self saveItemWithKey:item.key value:item.value filename:item.filename extendedData:item.extendedData];

}

- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value {

return [self saveItemWithKey:key value:value filename:nil extendedData:nil];

}

都是调用下面的这个函数

- (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.检测_type 如果是YYKVStorageTypeFile 并且filename的长度是0 。意思是我需要保存文件,但是没有文件名字,所以就返回就行了。

2.要是有filename ,那么就将文件写入到磁盘,接着将filename 写入的数据库。要是写入数据库错误,那么就删除磁盘上的文件。

3.要是没有filename ,那么检查_type 不是sqlite类型,那么我们就从数据库查询下key值有没有记录,有的话,就删除磁盘文件。最后将数据保存到数据库中。

流程图





- (BOOL)removeItemForKey:(NSString *)key {

    if (key.length == 0) return NO;

    switch (_type) {

        case YYKVStorageTypeSQLite: {

            return [self _dbDeleteItemWithKey:key];

        } break;

        case YYKVStorageTypeFile:

        case YYKVStorageTypeMixed: {

            NSString *filename = [self _dbGetFilenameWithKey:key];

            if (filename) {

                [self _fileDeleteWithName:filename];

            }

            return [self _dbDeleteItemWithKey:key];

        } break;

        default: return NO;

    }

}

这里看看也不难

1._type 是YYKVStorageTypeSQLite 那么从数据库删除数据

2其他类型,先删除本地文件再删除数据库




- (BOOL)removeItemForKeys:(NSArray *)keys {

    if (keys.count == 0) return NO;

    switch (_type) {

        case YYKVStorageTypeSQLite: {

            return [self _dbDeleteItemWithKeys:keys];

        } break;

        case YYKVStorageTypeFile:

        case YYKVStorageTypeMixed: {

            NSArray *filenames = [self _dbGetFilenameWithKeys:keys];

            for (NSString *filename in filenames) {

                [self _fileDeleteWithName:filename];

            }

            return [self _dbDeleteItemWithKeys:keys];

        } break;

        default: return NO;

    }

}

逻辑同上面一样,只不过入参是数组而已

- (BOOL)removeItemsLargerThanSize:(int)size {

    if (size == INT_MAX) return YES;

    if (size <= 0) return [self removeAllItems];


    switch (_type) {

        case YYKVStorageTypeSQLite: {

            if ([self _dbDeleteItemsWithSizeLargerThan:size]) {

                [self _dbCheckpoint];

                return YES;

            }

        } break;

        case YYKVStorageTypeFile:

        case YYKVStorageTypeMixed: {

            NSArray *filenames = [self _dbGetFilenamesWithSizeLargerThan:size];

            for (NSString *name in filenames) {

                [self _fileDeleteWithName:name];

            }

            if ([self _dbDeleteItemsWithSizeLargerThan:size]) {

                [self _dbCheckpoint];

                return YES;

            }

        } break;

    }

    return NO;

}

1.检查size 大小,要是小于或者等于0,删除所有对象

2.要是size大于0 ,那么检查_type

3.要是_type 是YYKVStorageTypeSQLite, 那么就删除数据库的大于size的数据,将wal数据写入到数据库中。

4.要是_type 是其他的,那么从数据库查询超过size大小的数据,根据查询的数据,删除磁盘数据,再删除数据库中的数据

- (BOOL)removeItemsEarlierThanTime:(int)time {

    if (time <= 0) return YES;

    if (time == INT_MAX) return [self removeAllItems];


    switch (_type) {

        case YYKVStorageTypeSQLite: {

            if ([self _dbDeleteItemsWithTimeEarlierThan:time]) {

                [self _dbCheckpoint];

                return YES;

            }

        } break;

        case YYKVStorageTypeFile:

        case YYKVStorageTypeMixed: {

            NSArray *filenames = [self _dbGetFilenamesWithTimeEarlierThan:time];

            for (NSString *name in filenames) {

                [self _fileDeleteWithName:name];

            }

            if ([self _dbDeleteItemsWithTimeEarlierThan:time]) {

                [self _dbCheckpoint];

                return YES;

            }

        } break;

    }

    return NO;

}

删除早于某一时刻的数据,这个和删除超过大小的逻辑一致

- (BOOL)removeItemsToFitSize:(int)maxSize {

    if (maxSize == INT_MAX) return YES;

    if (maxSize <= 0) return [self removeAllItems];


    int total = [self _dbGetTotalItemSize];

    if (total < 0) return NO;

    if (total <= maxSize) return YES;


    NSArray *items = nil;

    BOOL suc = NO;

    do {

        int perCount = 16;

        items = [self _dbGetItemSizeInfoOrderByTimeAscWithLimit:perCount];

        for (YYKVStorageItem *item in items) {

            if (total > maxSize) {

                if (item.filename) {

                    [self _fileDeleteWithName:item.filename];

                }

                suc = [self _dbDeleteItemWithKey:item.key];

                total -= item.size;

            } else {

                break;

            }

            if (!suc) break;

        }

    } while (total > maxSize && items.count > 0 && suc);

    if (suc) [self _dbCheckpoint];

    return suc;

}

删除到数据到不超过某个大小。

1检查要是maxSize<=0 ,说明需要删除磁盘上数据

2.获取磁盘上数据的大小

3.循环查询最后16条最后时间访问的数据,依次删除一条数据,查询是否符合最大条数。

- (BOOL)removeItemsToFitCount:(int)maxCount {

    if (maxCount == INT_MAX) return YES;

    if (maxCount <= 0) return [self removeAllItems];


    int total = [self _dbGetTotalItemCount];

    if (total < 0) return NO;

    if (total <= maxCount) return YES;


    NSArray *items = nil;

    BOOL suc = NO;

    do {

        int perCount = 16;

        items = [self _dbGetItemSizeInfoOrderByTimeAscWithLimit:perCount];

        for (YYKVStorageItem *item in items) {

            if (total > maxCount) {

                if (item.filename) {

                    [self _fileDeleteWithName:item.filename];

                }

                suc = [self _dbDeleteItemWithKey:item.key];

                total--;

            } else {

                break;

            }

            if (!suc) break;

        }

    } while (total > maxCount && items.count > 0 && suc);

    if (suc) [self _dbCheckpoint];

    return suc;

}

删除到指定条数,这个和指定大小一样不做介绍

- (BOOL)removeAllItems {

    if (![self _dbClose]) return NO;

    [self _reset];

    if (![self _dbOpen]) return NO;

    if (![self _dbInitialize]) return NO;

    return YES;

}

删除所有对象,作者是将数据库关闭,删除数据库,删除本地磁盘文件夹。再重新打开数据库。

- (void)removeAllItemsWithProgressBlock:(void(^)(int removedCount, int totalCount))progress

                              endBlock:(void(^)(BOOL error))end {


    int total = [self _dbGetTotalItemCount];

    if (total <= 0) {

        if (end) end(total < 0);

    } else {

        int left = total;

        int perCount = 32;

        NSArray *items = nil;

        BOOL suc = NO;

        do {

            items = [self _dbGetItemSizeInfoOrderByTimeAscWithLimit:perCount];

            for (YYKVStorageItem *item in items) {

                if (left > 0) {

                    if (item.filename) {

                        [self _fileDeleteWithName:item.filename];

                    }

                    suc = [self _dbDeleteItemWithKey:item.key];

                    left--;

                } else {

                    break;

                }

                if (!suc) break;

            }

            if (progress) progress(total - left, total);

        } while (left > 0 && items.count > 0 && suc);

        if (suc) [self _dbCheckpoint];

        if (end) end(!suc);

    }

}

这个删除所有对象,不过提供一个删除的进程block





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

}

查询key是否有值

1.查询key是否为nil,key是nil 就返回

2.查询到key所对应的值,数据库更新下访问时间

3.要是数据的fileName 不是nil,就从磁盘查询该值。要是fileName对应的磁盘上值是nil ,就从数据库删除key,返回nil

- (YYKVStorageItem *)getItemInfoForKey:(NSString *)key {

    if (key.length == 0) return nil;

    YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:YES];

    return item;

}

单纯查询下数据。

- (NSData *)getItemValueForKey:(NSString *)key {

    if (key.length == 0) return nil;

    NSData *value = nil;

    switch (_type) {

        case YYKVStorageTypeFile: {

            NSString *filename = [self _dbGetFilenameWithKey:key];

            if (filename) {

                value = [self _fileReadWithName:filename];

                if (!value) {

                    [self _dbDeleteItemWithKey:key];

                    value = nil;

                }

            }

        } break;

        case YYKVStorageTypeSQLite: {

            value = [self _dbGetValueWithKey:key];

        } break;

        case YYKVStorageTypeMixed: {

            NSString *filename = [self _dbGetFilenameWithKey:key];

            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;

}

这个函数是获取key对应的值

这里有个疑惑,为啥不直接调用[self getItemForKey:key].value; 而是分开调用。

1.检查_type类型

2_type 要是YYKVStorageTypeFile类型,那么从数据库中获取fileName,要是有fileName,根据fileName,从磁盘上读取数据。要是没有数据,那么就删除数据库中的key

3_type 要是YYKVStorageTypeSQLite类型,那么就直接数据库读取NSData

4._type 要是YYKVStorageTypeMixed ,那么获取fileName,要是有fileName,根据fileName,从磁盘上读取数据。要是没有数据,那么就删除数据库中的key。没有fileName,那么就从数据库读取nsdata

5.要是value不是nil ,那么就更新下数据库。

- (NSArray *)getItemForKeys:(NSArray *)keys {

    if (keys.count == 0) return nil;

    NSMutableArray *items = [self _dbGetItemWithKeys:keys excludeInlineData:NO];

    if (_type != YYKVStorageTypeSQLite) {

        for (NSInteger i = 0, max = items.count; i < max; i++) {

            YYKVStorageItem *item = items[i];

            if (item.filename) {

                item.value = [self _fileReadWithName:item.filename];

                if (!item.value) {

                    if (item.key) [self _dbDeleteItemWithKey:item.key];

                    [items removeObjectAtIndex:i];

                    i--;

                    max--;

                }

            }

        }

    }

    if (items.count > 0) {

        [self _dbUpdateAccessTimeWithKeys:keys];

    }

    return items.count ? items : nil;

}

获取数组keys 中所对应的YYKVStorageItem。

- (NSArray *)getItemInfoForKeys:(NSArray *)keys {

    if (keys.count == 0) return nil;

    return [self _dbGetItemWithKeys:keys excludeInlineData:YES];

}

获取keys数组每个key对应的value

- (NSDictionary *)getItemValueForKeys:(NSArray *)keys {

    NSMutableArray *items = (NSMutableArray *)[self getItemForKeys:keys];

    NSMutableDictionary *kv = [NSMutableDictionary new];

    for (YYKVStorageItem *item in items) {

        if (item.key && item.value) {

            [kv setObject:item.value forKey:item.key];

        }

    }

    return kv.count ? kv : nil;

}

只是单纯的讲获取的YYKVStorageItem 中提炼出value

- (BOOL)itemExistsForKey:(NSString *)key {

    if (key.length == 0) return NO;

    return [self _dbGetItemCountWithKey:key] > 0;

}

查询是否有key



- (int)getItemsCount {

    return [self _dbGetTotalItemCount];

}

获取数量

- (int)getItemsSize {

    return [self _dbGetTotalItemSize];

}

获取大小

到这里增删改查结束了。

我们知道作者是采用数据库加文件系统来保存数据基本流程


不管数据保存到sqlite 还是file中,所有的数据都是通过sqlite进行管理的。

数据流向搞明白了。再看看作者给我们文件提供的文件操作api

写文件

- (BOOL)_fileWriteWithName:(NSString *)filename data:(NSData *)data {

    NSString *path = [_dataPath stringByAppendingPathComponent:filename];

    return [data writeToFile:path atomically:NO];

}

读文件

- (NSData *)_fileReadWithName:(NSString *)filename {

    NSString *path = [_dataPath stringByAppendingPathComponent:filename];

    NSData *data = [NSData dataWithContentsOfFile:path];

    return data;

}

删除文件

- (BOOL)_fileDeleteWithName:(NSString *)filename {

    NSString *path = [_dataPath stringByAppendingPathComponent:filename];

    return [[NSFileManager defaultManager] removeItemAtPath:path error:NULL];

}

- (BOOL)_fileMoveAllToTrash {

    CFUUIDRef uuidRef = CFUUIDCreate(NULL);

    CFStringRef uuid = CFUUIDCreateString(NULL, uuidRef);

    CFRelease(uuidRef);

    NSString *tmpPath = [_trashPath stringByAppendingPathComponent:(__bridge NSString *)(uuid)];

    BOOL suc = [[NSFileManager defaultManager] moveItemAtPath:_dataPath toPath:tmpPath error:nil];

    if (suc) {

        suc = [[NSFileManager defaultManager] createDirectoryAtPath:_dataPath withIntermediateDirectories:YES attributes:nil error:NULL];

    }

    CFRelease(uuid);

    return suc;

}

这个是将_dataPath路径下的文件移动到_trashPath 的一个子目录下,再重新创建文件下_dataPath

这个方法干嘛的。当要删除_dataPath 中的所有文件的时候,我们不是直接删除,而是将文件移动到_trashPath文件中,然后再删除_trashPath下的文件。

- (void)_fileEmptyTrashInBackground {

    NSString *trashPath = _trashPath;

    dispatch_queue_t queue = _trashQueue;

    dispatch_async(queue, ^{

        NSFileManager *manager = [NSFileManager new];

        NSArray *directoryContents = [manager contentsOfDirectoryAtPath:trashPath error:NULL];

        for (NSString *path in directoryContents) {

            NSString *fullPath = [trashPath stringByAppendingPathComponent:path];

            [manager removeItemAtPath:fullPath error:NULL];

        }

    });

}

这个是文件删除,在异步线程中删除_trashPath 路径下的文件。

从以上api 和搜索下工程使用的地方,我们看出来文件操作要领。

单文件操作,都是对文件读写删除

所有文件删除操作,将所有文件移动到_trashPath路径下,然后异步删除。在这里我有个疑惑,删除所有文件,为啥不是直接删除,而是先移动文件再删除呢?

还是效率问题,当我们在一个线程里面要删除所有文件的时候,要是文件多的话,那么线程会删除一段时间,而移动文件的所需要的时间比删除文件要快很多,然我异步删除。

这只是猜测,因此要验证。

测试代码

-(void)test{

    NSString * url=[[NSBundle mainBundle]pathForResource:@"namecard" ofType:@"jpg"];///188k

    NSData * datajpg=[NSData dataWithContentsOfFile:url];

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);

    NSString *path = [paths objectAtIndex:0];

    NSString * testPath = [path stringByAppendingPathComponent:@"test"];


    NSError * error=nil;

    if (![[NSFileManager defaultManager] createDirectoryAtPath:path

                                  withIntermediateDirectories:YES

                                                    attributes:nil

                                                        error:&error] ||

        ![[NSFileManager defaultManager] createDirectoryAtPath:[path stringByAppendingPathComponent:@"test"]

                                  withIntermediateDirectories:YES

                                                    attributes:nil

                                                        error:&error] ||

        ![[NSFileManager defaultManager] createDirectoryAtPath:[path stringByAppendingPathComponent:@"trash"]

                                  withIntermediateDirectories:YES

                                                    attributes:nil

                                                        error:&error]) {


        }


    for (int i=0; i<1000; i++) {

        CFUUIDRef uuidRef = CFUUIDCreate(NULL);

        CFStringRef uuid = CFUUIDCreateString(NULL, uuidRef);

        CFRelease(uuidRef);

        NSString * struuid=(__bridge NSString *)(uuid);

        NSString *path = [testPath stringByAppendingPathComponent:struuid];

        [datajpg writeToFile:path atomically:NO];

    }


    [self _fileEmptyTrashInBackground];

}

- (BOOL)_fileMoveAllToTrash {

    CFUUIDRef uuidRef = CFUUIDCreate(NULL);

    CFStringRef uuid = CFUUIDCreateString(NULL, uuidRef);

    CFRelease(uuidRef);

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);

    NSString *path = [paths objectAtIndex:0];

    NSString * testPath = [path stringByAppendingPathComponent:@"test"];

    NSString * _trashPath=[path stringByAppendingPathComponent:@"trash"];

    NSString *tmpPath = [_trashPath stringByAppendingPathComponent:(__bridge NSString *)(uuid)];


    NSError * error=nil;

    BOOL suc = [[NSFileManager defaultManager] moveItemAtPath:testPath toPath:tmpPath error:&error];

    if (suc) {

        suc = [[NSFileManager defaultManager] createDirectoryAtPath:testPath withIntermediateDirectories:YES attributes:nil error:NULL];

    }


    CFRelease(uuid);

    return suc;

}

- (void)_fileEmptyTrashInBackground {

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);

    NSString *path = [paths objectAtIndex:0];

    NSString * _trashPath=[path stringByAppendingPathComponent:@"trash"];

    NSString *trashPath = _trashPath;

    dispatch_queue_t queue =dispatch_queue_create("com.ibireme.cache.disk.trash", DISPATCH_QUEUE_SERIAL) ;

    dispatch_async(queue, ^{

        NSTimeInterval begin, end;

        begin = CACurrentMediaTime();

        [self _fileMoveAllToTrash];

        end = CACurrentMediaTime();

        printf("moveTime      %8.2f  \n", (end - begin) * 1000);       

        begin = CACurrentMediaTime();

        NSFileManager *manager = [NSFileManager new];

        NSArray *directoryContents = [manager contentsOfDirectoryAtPath:trashPath error:NULL];

        for (NSString *path in directoryContents) {

            NSString *fullPath = [trashPath stringByAppendingPathComponent:path];

            [manager removeItemAtPath:fullPath error:NULL];

        }

        end = CACurrentMediaTime();

        printf("deleteFile      %8.2f  \n", (end - begin) * 1000);

    });

}


结果是

moveTime 2.75

deleteFile        435.20 


我们看出来,移动文件比删除文件,消耗的时间小很多。

这就是为啥删除所有文件的时候不是直接删除而是先移动到垃圾文件夹里。之后再删除。











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

推荐阅读更多精彩内容