美文网首页
YYCache源码学习

YYCache源码学习

作者: 崔又淇 | 来源:发表于2022-07-15 14:59 被阅读0次

    通常缓存是由内存缓存和磁盘缓存组成的,内存缓存容量小但是存取速度快,磁盘容量大但是存取速度慢。

    内存缓存

    YYMemoryCache

    • 包括数量限制、容量限制、时间限制、内存警告、退出后台清空缓存;
    • 采用了双向链表和字典实现了LRU 淘汰算法;
    • 优化同步访问的性能,使用了pthread_mutex_lock锁来保证线程安全;

    磁盘缓存

    YYDiskCache

    • 采用了SQLite配合文件的存储方式;
    • 采用了LRU淘汰算法更快统计数据;

    iPhone 6 64G 下,SQLite 写入性能比直接写文件要高,但读取性能取决于数据大小:当单条数据小于 20K 时,数据越小 SQLite 读取性能越高;单条数据大于 20K 时,直接写为文件速度会更快一些。

    LRU算法

    LRU (英文:Least Recently Used), 意为最近最少使用,这个算法的精髓在于如果一块数据最近被访问,那么它将来被访问的几率也很高,根据数据的历史访问来淘汰长时间未使用的数据。

    实现方法:

    • 新数据放在链表头部;
    • 缓存数据被访问时,将数据移到链表头部;
    • 链表满的时候,链表尾部数据被丢弃;

    YYMemoryCache

    @implementation YYMemoryCache {
        pthread_mutex_t _lock;
        _YYLinkedMap *_lru;
        dispatch_queue_t _queue;
    }
    
    • 使用了pthread_mutex_lock锁来保证线程安全
      原来作者使用了OSSpinLock,但是OSSpinLock不能再保证线程安全

    除非开发者能保证访问锁的线程全部都处于同一优先级,否则 iOS 系统中所有类型的自旋锁都不能再使用了。
    作者做了测试,除了 OSSpinLock 外,dispatch_semaphore 和 pthread_mutex 性能是最高的。

    • 使用_YYLinkedMap来实现LRU算法, _YYLinkedMap中包括一个字典、双向链表的头、尾节点。_YYLinkedMap中还包括对链表的操作,添加节点、移动节点、删除节点。
    /*
     A linked map used by YYMemoryCache.
     It's not thread-safe and does not validate the parameters.
     
     Typically, you should not use this class directly.
     */
    @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;// 默认为NO
        BOOL _releaseAsynchronously; // 默认为YES
    }
    
    /// Insert a node at head and update the total cost.
    /// Node and node.key should not be nil.
    - (void)insertNodeAtHead:(_YYLinkedMapNode *)node;
    
    /// Bring a inner node to header.
    /// Node should already inside the dic.
    - (void)bringNodeToHead:(_YYLinkedMapNode *)node;
    
    /// Remove a inner node and update the total cost.
    /// Node should already inside the dic.
    - (void)removeNode:(_YYLinkedMapNode *)node;
    
    /// Remove tail node if exist.
    - (_YYLinkedMapNode *)removeTailNode;
    
    /// Remove all node in background queue.
    - (void)removeAll;
    
    @end
    

    链表中的节点的是_YYLinkedMapNode

    /**
     A node in linked map.
     Typically, you should not use this class directly.
     */
    @interface _YYLinkedMapNode : NSObject {
        @package
        __unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
        __unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
        id _key;
        id _value;
        NSUInteger _cost;
        NSTimeInterval _time;
    }
    @end
    
    @implementation _YYLinkedMapNode
    @end
    

    实现LRU算法,对双向链表的操作如下:

    1. 新增节点并将节点添加到链表头部
    - (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. 移动访问到的节点到链表头部
    - (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;
    }
    
    1. 移除节点
    - (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;
    }
    
    1. 移除尾节点
    - (_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. 删除所有节点
      _releaseAsynchronously默认为YES,_releaseOnMainThread默认值为NO,默认为在YYMemoryCacheGetReleaseQueue队列中清空字典,该队列的执行优先级最低。
    static inline dispatch_queue_t YYMemoryCacheGetReleaseQueue() {
        return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
    }
    
    - (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);
            }
        }
    }
    

    内存警告或退出后台清空缓存

    YYMemoryCache中有个autoTrimInterval属性,默认是5秒,在初始化方法中会调用_trimRecursively方法,会在5秒中自动检查内存的大小、数量、还有时间是否超出限制,超出会清理内存。

    /**
     The auto trim check time interval in seconds. Default is 5.0.
     
     @discussion The cache holds an internal timer to check whether the cache reaches 
     its limits, and if the limit is reached, it begins to evict objects.
     */
    @property NSTimeInterval 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];
        });
    }
    

    如果内存超过了costLimit、countLimit、ageLimit需要遍历链表,移除尾部节点,将节点加到数组中,然后将对象捕获block中,在子线程或主线程异步释放对象。_trimToCount, _trimToAge和下面代码类似。

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

    MemoryCache中的使用

    初始化方法:

    - (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];
       // 每5秒钟检查内存
       [self _trimRecursively];
       return self;
    }
    
    - (void)dealloc {
       [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
       [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil];
       [_lru removeAll];
       pthread_mutex_destroy(&_lock);
    }
    

    根据key值获取缓存对象:在字典中根据key值获取node,然后更新node的时间,将该node移动到头节点。

    - (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,然后放到头节点。新增时需要判断是否超出内存,超出了则移除尾部节点。

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

    移除节点:

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

    YYDiskCache

    inlineThreshold属性默认是20KB,如果超过20KB的文件用file方式保存更加快速。小于20KB使用sqlite方式保存。

    /**
     If the object's data size (in bytes) is larger than this value, then object will
     be stored as a file, otherwise the object will be stored in sqlite.
     
     0 means all objects will be stored as separated files, NSUIntegerMax means all
     objects will be stored in sqlite. 
     
     The default value is 20480 (20KB).
     */
    @property (readonly) NSUInteger inlineThreshold;
    

    YYKVStorage是实现Disk保存的类,这里使用的锁是dispatch_semaphore_t。

    @implementation YYDiskCache {
        YYKVStorage *_kv;
        dispatch_semaphore_t _lock;
        dispatch_queue_t _queue;
    }
    

    YYKVStorage的元数据是YYKVStorageItem

    /**
     YYKVStorageItem is used by `YYKVStorage` to store key-value pair and meta data.
     Typically, you should not use this class directly.
     */
    @interface YYKVStorageItem : NSObject
    @property (nonatomic, strong) NSString *key;                ///< key
    @property (nonatomic, strong) NSData *value;                ///< value
    @property (nullable, nonatomic, strong) NSString *filename; ///< filename (nil if inline)
    @property (nonatomic) int size;                             ///< value's size in bytes
    @property (nonatomic) int modTime;                          ///< modification unix timestamp
    @property (nonatomic) int accessTime;                       ///< last access unix timestamp
    @property (nullable, nonatomic, strong) NSData *extendedData; ///< extended data (nil if no extended data)
    @end
    

    YYKVStorage的存储方式有3种,文件形式、数据库、混合方式。

    typedef NS_ENUM(NSUInteger, YYKVStorageType) {
        
        /// The `value` is stored as a file in file system.
        YYKVStorageTypeFile = 0,
        
        /// The `value` is stored in sqlite with blob type.
        YYKVStorageTypeSQLite = 1,
        
        /// The `value` is stored in file system or sqlite based on your choice.
        YYKVStorageTypeMixed = 2,
    };
    

    DiskCache的初始化方法:默认开启混合存储的方法

    // 直接调用init方法会抛出异常
    - (instancetype)init {
        @throw [NSException exceptionWithName:@"YYDiskCache init error" reason:@"YYDiskCache must be initialized with a path. Use 'initWithPath:' or 'initWithPath:inlineThreshold:' instead." userInfo:nil];
        return [self initWithPath:@"" inlineThreshold:0];
    }
    
    - (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;
    }
    

    _trimRecursively的方法主要每隔5秒调用一下方法,检查内存大小、数量、时间、硬盘的空闲大小,采用LRU删除最近不用的数据。

    - (void)_trimToCost:(NSUInteger)costLimit {
        if (costLimit >= INT_MAX) return;
        [_kv removeItemsToFitSize:(int)costLimit];
        
    }
    
    - (void)_trimToCount:(NSUInteger)countLimit {
        if (countLimit >= INT_MAX) return;
        [_kv removeItemsToFitCount:(int)countLimit];
    }
    
    - (void)_trimToAge:(NSTimeInterval)ageLimit {
        if (ageLimit <= 0) {
            [_kv removeAllItems];
            return;
        }
        long timestamp = time(NULL);
        if (timestamp <= ageLimit) return;
        long age = timestamp - ageLimit;
        if (age >= INT_MAX) return;
        [_kv removeItemsEarlierThanTime:(int)age];
    }
    
    - (void)_trimToFreeDiskSpace:(NSUInteger)targetFreeDiskSpace {
        if (targetFreeDiskSpace == 0) return;
        int64_t totalBytes = [_kv getItemsSize];
        if (totalBytes <= 0) return;
        int64_t diskFreeBytes = _YYDiskSpaceFree();
        if (diskFreeBytes < 0) return;
        int64_t needTrimBytes = targetFreeDiskSpace - diskFreeBytes;
        if (needTrimBytes <= 0) return;
        int64_t costLimit = totalBytes - needTrimBytes;
        if (costLimit < 0) costLimit = 0;
        [self _trimToCost:(int)costLimit];
    }
    

    kv中removeItemsToFitSize和removeItemsToFitCount方法类似,根据last_access_time时间排序,每16个一组,如果数量大于max值,如果有file name则删除file中文件,然后删除数据库中的数据。直到数量小于maxcount值。

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

    删除更早时间缓存的数据:如果是YYKVStorageTypeFile的存储类型,需要根据时间获取file,然后删除file。

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

    删除操作,这里都调用了一个_dbCheckpoint

    - (void)_dbCheckpoint {
        if (![self _dbCheck]) return;
        // Cause a checkpoint to occur, merge `sqlite-wal` file to `sqlite` file.
        sqlite3_wal_checkpoint(_db, NULL);
    }
    

    想要搞懂这个方法是在做什么,先看下数据库初始化方法

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

    这里声明了

    pragma journal_mode = wal;
    

    设置数据库访问模式,从iOS4.3开始,sqlite提供了Write-Ahead Logging模式,在大部分情况下这种模式读写速度更快,且两者互不堵塞。使用这种模式时,改写操作不改动数据库文件,而是修改到WAL文件中。
    WAL的文件会在执行checkpoint操作时写回数据库文件,或者当文件大小达到某个阙值时(默认为1KB)会自动执行checkpoint操作

    pragma synchronous = normal;
    

    磁盘同步等级
    PRAGMA synchronous = FULL; (2)
    PRAGMA synchronous = NORMAL; (1)
    PRAGMA synchronous = OFF; (0)
    当synchronous为FULL时,数据库引擎会在紧急时刻暂停以确定数据写入磁盘,这样能保证在系统崩溃或者计算机死机的环境下数据库在重启后不会被损坏,代价是插入数据的速度会降低。
    如果synchronous为OFF则不会暂停。除非计算机死机或者意外关闭的情况,否则即便是sqlite程序崩溃了,数据也不会损伤,这种等级的写入速度最高。这里采用了第二种,速度不那么慢又相对安全的同步等级:

    保存或更新对象:这里我们可以看到,如果kv的type不是YYKVStorageTypeSQLite,判断value的大小是否大于_inlineThreshold,如果比这个值大,则将key进行MD5加密,获取filename,然后调用kv的保存方法。

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

    来看一下kv的保存方法都做了什么:

    • 如果filename是存在的,则把value保存在file中
    • 然后保存到数据库中,这时数据库中之保存filename
    • 如果保存数据库失败,则把刚开始写入的文件也删除
    - (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];
        }
    }
    

    获取保存对象:调用kv的获取get对象的方法

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

    来看下kv主要做了什么:

    • 查询数据库查找到了YYKVStorageItem
    • 更新数据库中的时间,为了实现LRU
    • 如果item是有filename,说明缓存是用file形式存储的,从file中取出value
    • 如果没有取到value也将数据库该条数据删除
    - (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. 打开数据库
     sqlite3_open(_dbPath.UTF8String, &_db);
    
    - (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. 关闭数据库
    sqlite3_close(_db);
    
    - (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;
    }
    
    1. 校验数据库,如果打开数据库错误的次数大于8的话,并且现在时间和上次错误打开的时间差大于2s,说明打开数据库失败
    - (BOOL)_dbCheck {
        if (!_db) {
            if (_dbOpenErrorCount < kMaxErrorRetryCount &&
                CACurrentMediaTime() - _dbLastOpenErrorTime > kMinRetryTimeInterval) {
                return [self _dbOpen] && [self _dbInitialize];
            } else {
                return NO;
            }
        }
        return YES;
    }
    

    4.保存数据

    // 绑定、更新数据
      sqlite3_bind
    // 执行语句
      sqlite3_step
    
    - (BOOL)_dbSaveWithKey:(NSString *)key value:(NSData *)value fileName:(NSString *)fileName extendedData:(NSData *)extendedData {
        NSString *sql = @"insert or replace into manifest (key, filename, size, inline_data, modification_time, last_access_time, extended_data) values (?1, ?2, ?3, ?4, ?5, ?6, ?7);";
        sqlite3_stmt *stmt = [self _dbPrepareStmt:sql];
        if (!stmt) return NO;
        
        int timestamp = (int)time(NULL);
        sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL);
        sqlite3_bind_text(stmt, 2, fileName.UTF8String, -1, NULL);
        sqlite3_bind_int(stmt, 3, (int)value.length);
        if (fileName.length == 0) {
            sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0);
        } else {
            sqlite3_bind_blob(stmt, 4, NULL, 0, 0);
        }
        sqlite3_bind_int(stmt, 5, timestamp);
        sqlite3_bind_int(stmt, 6, timestamp);
        sqlite3_bind_blob(stmt, 7, extendedData.bytes, (int)extendedData.length, 0);
        
        int result = sqlite3_step(stmt);
        if (result != SQLITE_DONE) {
            if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite insert error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
            return NO;
        }
        return YES;
    }
    

    在获取 sqlite3_stmt时,都会调用_dbPrepareStmt方法,用这个方法将stmt缓存起来。

    - (sqlite3_stmt *)_dbPrepareStmt:(NSString *)sql {
        if (![self _dbCheck] || sql.length == 0 || !_dbStmtCache) return NULL;
        sqlite3_stmt *stmt = (sqlite3_stmt *)CFDictionaryGetValue(_dbStmtCache, (__bridge const void *)(sql));
        if (!stmt) {
            int result = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &stmt, NULL);
            if (result != SQLITE_OK) {
                if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite stmt prepare error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
                return NULL;
            }
            CFDictionarySetValue(_dbStmtCache, (__bridge const void *)(sql), stmt);
        } else {
            sqlite3_reset(stmt);
        }
        return stmt;
    }
    

    sqlite3_stmt是操作数据库数据的辅助数据类型,每一个sql语句可以解析成对应的辅助数据结构,大量的sql语句解析同样会带来性能上的损耗,因此YYCache采用CFMutableDictionaryRef结构将解析后的辅助数据类型存储起来,每次执行sql语句前查询是否存在已缓存的数据

    总结:
    YYCache的源码体量很小,代码也很容易看懂在讲什么,也没有很深的嵌套,菜鸟也能知道在表达什么。看了源码后对缓存的设计思路有了很强的理解,也对数据库的操作有了一些理解。

    学习链接:
    YYCache 设计思路
    不再安全的 OSSpinLock
    深入理解 YYCache
    iOS Sqlite3 的总结

    相关文章

      网友评论

          本文标题:YYCache源码学习

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