前言

本篇文章将带来YYCache的解读,YYCache支持内存和本地两种方式的数据存储。我们先抛出两个问题:

  • YYCache是如何把数据写入内存之中的?又是如何实现的高效读取?
  • YYCache采用了何种方式把数据写入磁盘?

这次的解读跟之前的源码解读不同,我只会展示重要部分的代码,因为我们学习YYCache的目的是学习作者的思路,顺便学习一下实现这些功能所用到的技术。

YYMemoryCache

我们使用YYMemoryCache可以把数据缓存进内存之中,它内部会创建了一个YYMemoryCache对象,然后把数据保存进这个对象之中。

但凡涉及到类似这样的操作,代码都需要设计成线程安全的。所谓的线程安全就是指充分考虑多线程条件下的增删改查操作。

我们应该养成这样的习惯:在写任何类的时候都把该类当做框架来写,因此需要设计好暴露出来的接口,这也正符合代码封装的思想。

YYMemoryCache暴露出来的接口我们在此就略过了,我们都知道要想高效的查询数据,使用字典是一个很好的方法。字典的原理跟哈希有关,总之就是把key直接映射成内存地址,然后处理冲突和和扩容的问题。对这方面有兴趣的可以自行搜索资料。

YYMemoryCache内部封装了一个对象_YYLinkedMap,包含了下边这些属性:

@interface _YYLinkedMap : NSObject {@packageCFMutableDictionaryRef _dic; // do not set object directlyNSUInteger _totalCost;NSUInteger _totalCount;_YYLinkedMapNode *_head; // MRU, do not change it directly_YYLinkedMapNode *_tail; // LRU, do not change it directlyBOOL _releaseOnMainThread;BOOL _releaseAsynchronously;
}

可以看出来,CFMutableDictionaryRef _dic将被用来保存数据。这里使用了CoreFoundation的字典,性能更好。字典里边保存着的是_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 dicid _key;id _value;NSUInteger _cost;NSTimeInterval _time;
}
@end

但看上边的代码,就能知道使用了链表的知识。但是有一个疑问,单用字典我们就能很快的查询出数据,为什么还要实现链表这一数据结构呢?

答案就是淘汰算法,YYMemoryCache使用了LRU淘汰算法,也就是当数据超过某个限制条件后,我们会从链表的尾部开始删除数据,直到达到要求为止。

通过这种方式,就实现了类似数组的功能,是原本无序的字典成了有序的集合。

我们简单看一段把一个节点插入到最开始位置的代码:

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

如果有一列数据已经按顺序排好了,我使用了中间的某个数据,那么就要把这个数据插入到最开始的位置,这就是一条规则,越是最近使用的越靠前。

在设计上,YYMemoryCache还提供了是否异步释放数据这一选项,在这里就不提了,我们在来看看在YYMemoryCache中用到的锁的知识。

pthread_mutex_lock是一种互斥所:

pthread_mutex_init(&_lock, NULL); // 初始化
pthread_mutex_lock(&_lock); // 加锁
pthread_mutex_unlock(&_lock); // 解锁
pthread_mutex_trylock(&_lock) == 0 // 是否加锁,0:未锁住,其他值:锁住

在OC中有很多种锁可以用,pthread_mutex_lock就是其中的一种。YYMemoryCache有这样一种设置,每隔一个固定的时间就要处理数据,代码如下:

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

上边的代码中,每隔_autoTrimInterval时间就会在后台尝试处理数据,然后再次调用自身,这样就实现了一个类似定时器的功能。这一个小技巧可以学习一下。

- (void)_trimInBackground {dispatch_async(_queue, ^{[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});}
}

这段代码很经典,可以直接拿来用,我们在某个处理数据的类中,可以直接使用类似这样的代码。如果锁正在使用,那么可以使用usleep(10 * 1000); //10 ms等待一小段时间。上边的代码把需要删除的数据,首先添加到一个数组中,然后使用[holder count]; // release in queue释放了资源。

当某个变量在出了自己的作用域之后,正常情况下就会被自动释放。

YYKVStorage

我发现随着编码经验的不断增加,会不经意间学会模仿这一技能。但有一点,我们必须发现那些出彩的地方,因此,我认为深入理解的本质就是学习该框架的核心思想。

上一小节中,我们已经明白了YYMemoryCache实际上就是创建了一个对象实例,该对象内部使用字典和双向链表实现。YYKVStorage最核心的思想是KV这两个字母,表示key-value的意思,目的是让使用者像使用字典一样操作数据。

我们应该明白,封装具有层次性,不建议用一层封装来封装复杂的功能。

YYKVStorage让我们只关心3件事:

  1. 数据保存的路径
  2. 保存数据,并为该数据关联一个key
  3. 根据key取出数据或删除数据

同理,YYKVStorage在设计接口的时候,也从这3个方面进行了考虑。这数据功能设计层面的思想。

在真实的编程中,往往需要把数据封装成一个对象:

/**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

上边的代码就是对每条数据的一个封装,在我封装的MCDownloader(iOS下载器)说明书中,也是用了类似的技术。当然,在YYKVStorage中,我们并不需要是用上边的对象。

我们看一些借口设计方面的内容:

#pragma mark - Attribute
///=============================================================================
/// @name Attribute
///=============================================================================@property (nonatomic, readonly) NSString *path;        ///< The path of this storage.
@property (nonatomic, readonly) YYKVStorageType type;  ///< The type of this storage.
@property (nonatomic) BOOL errorLogsEnabled;           ///< Set `YES` to enable error logs for debug.#pragma mark - Initializer
///=============================================================================
/// @name Initializer
///=============================================================================
- (instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (instancetype)new UNAVAILABLE_ATTRIBUTE;/**The designated initializer. @param path  Full path of a directory in which the storage will write data. Ifthe directory is not exists, it will try to create one, otherwise it will read the data in this directory.@param type  The storage type. After first initialized you should not change the type of the specified path.@return  A new storage object, or nil if an error occurs.@warning Multiple instances with the same path will make the storage unstable.*/
- (nullable instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type NS_DESIGNATED_INITIALIZER;

接口中的属性都是很重要的信息,我们应该尽量利用好它的读写属性,尽量设计成只读属性。默认情况下,不是只读的,都很容易让其他开发者认为,该属性是可以设置的。

对于初始化方法而言,如果某个类需要提供一个指定的初始化方法,那么就要使用NS_DESIGNATED_INITIALIZER给予提示。同时使用UNAVAILABLE_ATTRIBUTE禁用掉默认的方法。接下来要重写禁用的初始化方法,在其内部抛出异常:

- (instancetype)init {@throw [NSException exceptionWithName:@"YYKVStorage init error" reason:@"Please use the designated initializer and pass the 'path' and 'type'." userInfo:nil];return [self initWithPath:@"" type:YYKVStorageTypeFile];
}

上边的代码大家可以直接拿来用,千万不要怕程序抛出异常,在发布之前,能够发现潜在的问题是一件好事。使用了上边的一个小技巧后呢,编码水平是不是有所提升?

再给大家简单分析分析下边一样代码:

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

上边我们关心的是nullable关键字,表示可能为空,与之对应的是nonnull,表示不为空。可以说,他们都跟swift有关系,swift中属性或参数是否为空都有严格的要求。因此我们在设计属性,参数,返回值等等的时候,要考虑这些可能为空的情况。

// 设置中间的内容默认都是nonnull
NS_ASSUME_NONNULL_BEGIN
NS_ASSUME_NONNULL_END

我们现在来分析YYKVStorage.m的代码:

static const NSUInteger kMaxErrorRetryCount = 8;
static const NSTimeInterval kMinRetryTimeInterval = 2.0;
static const int kPathLengthMax = PATH_MAX - 64;
static NSString *const kDBFileName = @"manifest.sqlite";
static NSString *const kDBShmFileName = @"manifest.sqlite-shm";
static NSString *const kDBWalFileName = @"manifest.sqlite-wal";
static NSString *const kDataDirectoryName = @"data";
static NSString *const kTrashDirectoryName = @"trash";

代码的这种写法,应该不用我说了吧,如果你平时开发没用到过,那么就要认真去查资料了。

/*File:/path//manifest.sqlite/manifest.sqlite-shm/manifest.sqlite-wal/data//e10adc3949ba59abbe56e057f20f883e/e10adc3949ba59abbe56e057f20f883e/trash//unused_file_or_folderSQL: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);*/

在我看来这是超级赞的注释了。在我个人角度来说,我认为大多数人的注释都写不好,也包括我自己。从上边的注释的内容,我们能够很容易明白YYKVStorage的数据保存结构,和数据库的设计细节。

上图中这些函数都是跟数据库有关的函数,我们在这里也不会把代码弄上来。我个人对这些函数的总结是:

  • 每个函数只实现先单一功能,函数组合使用形成新的功能
  • 对于类内部的私有方法,前边添加_
  • 使用预处理stmt对数据库进行了优化,避免不必要的开销
  • 健壮的错误处理机制
  • 可以说是使用iOS自带sqlite3的经典代码,在项目中可以直接拿来用

这也许就是函数的魅力,有了这些函数,那么在给接口中的函数写逻辑的时候就会变得很简单。

有一个很重要的前提,这些函数都是线程不安全的。因此在使用中需要考虑多线程的问题,这也正是我们下一小节YYDiskCache的内容。

数据库增删改查的思想基本上都差不多,我以后会写一篇介绍数据库的文章。

建议大家一定要读读YYKVStorage这个类的源码,这是一个类的典型设计。它内部使用了两种方式保存数据:一种是保存到数据库中,另一种是直接写入文件。当数据较大时,使用文件写入性能更好,反之数据库更好。

YYDiskCache

上一小节我们已经明白了YYKVStorage实现了所有的数据存储的功能,但缺点是它不是线程安全的,因此在YYKVStorage的基础之上,YYDiskCache保证了线程的安全。

一个类提供什么样的功能,这属于程序设计的范畴,YYDiskCache的接口设计在YYKVStorage的基础上添加了一些新的特性。比如:

/**If this block is not nil, then the block will be used to archive object insteadof NSKeyedArchiver. You can use this block to support the objects which do notconform to the `NSCoding` protocol.The default value is nil.*/
@property (nullable, copy) NSData *(^customArchiveBlock)(id object);/**If this block is not nil, then the block will be used to unarchive object insteadof NSKeyedUnarchiver. You can use this block to support the objects which do notconform to the `NSCoding` protocol.The default value is nil.*/
@property (nullable, copy) id (^customUnarchiveBlock)(NSData *data);

使用上边的属性可以设置对象与NSData之间转化的规则,这和很多框架一样,目的是给该类增加一些额外的特性。

还是那句话,设计一个存储类,需要考虑下边几个特性:

  • 标识,在YYDiskCache中使用path作为存储位置的标识,使用key作为value的标识
  • 操作方法 包含增删改查
  • 限制条件 包括count,cost,age
  • 其他

我们来看看YYDiskCache.m的核心内容。我们来分析分析下边这段代码:

static YYDiskCache *_YYDiskCacheGetGlobal(NSString *path) {if (path.length == 0) return nil;_YYDiskCacheInitGlobal();dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER);id cache = [_globalInstances objectForKey:path];dispatch_semaphore_signal(_globalInstancesLock);return cache;
}

YYDiskCache内部实现了一种这样的机制,他会把开发者创建的每一个YYDiskCache对象保存到一个全局的集合中,YYDiskCache根据path创建,如果开发者创建了相同path的YYDiskCache,那么就会返回全局集合中的YYDiskCache。

这里就产生了一个很重要的概念,在全局对象中的YYDiskCache是可以释放的。为什么会发生这种事呢?按理说全局对象引用了YYDiskCache,它就不应该被释放的。这个问题我们马上就会给出答案。

继续分析上边的代码:

static YYDiskCache *_YYDiskCacheGetGlobal(NSString *path)这种风格的代码是值得学习的第一点,如果在一个文件中,有一些方法是不依赖某个对象的,那么我们就可以写成这种形式,它可以跨对象调用,因此这算是私有函数的一种写法吧。

if (path.length == 0) return nil;这个不用多说,健壮的函数内部都要有检验参数的代码。

_YYDiskCacheInitGlobal();从函数的名字,我们可以猜测出它是一个初始化全局对象的方法,它内部引出了一个很重要的对象:

static void _YYDiskCacheInitGlobal() {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{_globalInstancesLock = dispatch_semaphore_create(1);_globalInstances = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];});
}

大家对NSMapTable可能不太熟悉,他其实和NSMutableDictionary非常相似,我们都知道字典的key值copy的,他必须实现NSCopying协议,如果key的值改变了,就无法获取value了。而NSMapTable使用起来更加自由,我们可以操纵key,value的weak和strong特性,关于NSMapTable的详细使用方法,大家可以自行去搜索相关的内容。在上边的代码中,_globalInstances的中value被设置为NSPointerFunctionsWeakMemory,也就是说,当_globalInstances添加了一个对象后,该对象的引用计数器不会加1.当该对象没有被任何其他对象引用的时候就会释放。

在网上看着这样一个例子:

Person *p1 = [[Person alloc] initWithName:@"jack"];
Favourite *f1 = [[Favourite alloc] initWithName:@"ObjC"];Person *p2 = [[Person alloc] initWithName:@"rose"];
Favourite *f2 = [[Favourite alloc] initWithName:@"Swift"];NSMapTable *MapTable = [NSMapTable mapTableWithKeyOptions:NSMapTableWeakMemory valueOptions:NSMapTableWeakMemory];
// 设置对应关系表
// p1 => f1;
// p2 => f2
[MapTable setObject:f1 forKey:p1];
[MapTable setObject:f2 forKey:p2];NSLog(@"%@ %@", p1, [MapTable objectForKey:p1]);
NSLog(@"%@ %@", p2, [MapTable objectForKey:p2]);

上边的代码中,使用NSMapTable让不同类型的对象一一对应起来,这种方式的最大好处是我们可以把一个View或者Controller当做key都没问题,怎么使用全凭想象啊。

在网上看到一个这样的例子,他把一些控制器保存到了MapTable之中,然后在想要使用的时候直接读取出来就行了。不会对控制器造成任何影响。

我们继续分析代码:

dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER);
id cache = [_globalInstances objectForKey:path];
dispatch_semaphore_signal(_globalInstancesLock);

dispatch_semaphore_wait配合dispatch_semaphore_signal实现加锁解锁的功能,这个没什么好说的,可以大胆使用。

没有读过源码的同学,一定要读一读YYDiskCache的源码,和YYKVStorage一样有很多代码可以直接拿来用。

YYCache

当我们读到YYCache的时候,感觉一下子就轻松了很多,YYCache就是对YYMemoryCache和YYDiskCache的综合运用,创建YYCache对象后,就创建了一个YYMemoryCache对象和一个YYDiskCache对象。唯一新增的特性就是可以根据name来创建YYCache,内部会根据那么来创建一个path,本质上还是使用path定位的。

Summary

第一次以这样的方式写博客,我发现好处很多,把很大一部分不是学习重点的代码过滤掉为我节省了大量时间。我们不可能记住所有的代码,当要用某些知识的时候,知道去哪找就可以了。

写代码就是一个不断模仿,不断进步的过程。

感谢YYCache的作者开源了这么好的东西

转载于:https://www.cnblogs.com/machao/p/7086675.html

深入理解YYCache相关推荐

  1. 深入理解YYCache缓存策略

    文章目录 前言 几个主要成员类 1 YYCache 2 YYMemoryCache 3 YYDiskCache 实例化 1 实例方法 2 构造器方法 查 1 检查是否有缓存 2 读缓存 增 1 写内存 ...

  2. YYCache 源码解析(一):使用方法,架构与内存缓存的设计

    YYCache是国内开发者ibireme开源的一个线程安全的高性能缓存组件,代码风格简洁清晰,阅读它的源码有助于建立比较完整的缓存设计的思路,同时也能巩固一下双向链表,线程锁,数据库操作相关的知识. ...

  3. iOS 缓存框架YYCache学习

    文章目录 前言 一.YYCache的来源 二.YYCache的结构 1. YYMemoryCache 1.1 最近最少使用-LRU(Least Frequently Used) 1.2 基于LRU的增 ...

  4. YYCache 源码解析

    YYCache 源码解析 YYCache是国内开发者ibireme开源的一个线程安全的高性能缓存组件,代码风格简洁清晰,在GitHub上已经有了1600+颗星. 阅读它的源码有助于建立比较完整的缓存设 ...

  5. 看动画轻松理解「链表」实现「LRU缓存淘汰算法」

    作者 | 程序员小吴,哈工大学渣,目前正在学算法,开源项目 「 LeetCodeAnimation 」5500star,GitHub Trending 榜连续一月第一. 本文为 AI科技大本营投稿文章 ...

  6. YYCache深入学习

    深知,源码还是一点点读,加点读书笔记,才可以深入挖掘,因此还是觉得每次读源码都记录一番,无论好坏,如有写错,请斧正 简介 YYCahce 是作为 ibireme 大神开源的一个YYkit组件库中的一部 ...

  7. YYCache 源码学习(二):YYDiskCache

    整体思路 从作者的<YYCache 设计思路>一文中可以看出,作者在设计YYDiskCache之前做了充分的测试:iPhone 6 64G 下,SQLite 写入性能比直接写文件要高,但读 ...

  8. 看动画轻松理解「链表」实现「 LRU 缓存淘汰算法」

    作者 | 吴至波 责编 | 胡巍巍 快速挑战Python全栈工程师: https://edu.csdn.net/topic/python115?utm_source=csdn_bw 前几节学习了「链表 ...

  9. 看动画理解「链表」实现LRU缓存淘汰算法

    前几节学习了「链表」.「时间与空间复杂度」的概念,本节将结合「循环链表」.「双向链表」与 「用空间换时间的设计思想」来设计一个很有意思的缓存淘汰策略:LRU缓存淘汰算法. 循环链表的概念 如上图所示: ...

最新文章

  1. Unity 游戏框架搭建 (七) 减少加班利器-QApp类
  2. 洛谷P3273 [SCOI2011] 棘手的操作 [左偏树]
  3. 高级Linux程序设计第五章:进程间通信
  4. 低版本Eclipse如何快速设置黑色主题
  5. html 打开页面光标自动选中输入框_Python自动部署码云:
  6. C语言-库文件与头文件
  7. 全面开放运营3个月,百度揭秘Apollo最新技术创新
  8. aspx页面中文汉字显示为乱码
  9. python第五章自己的笔记总结(6)
  10. 数组索引越界异常和空指针异常
  11. 高一下册计算机教案,高一信息技术教案
  12. 【2020GAN】对抗生成网络论文收录(1月-6月)
  13. playwright 启动已经打开的浏览器,或者远程浏览器
  14. 第一代商用计算机是由,计算机基础辅导资料
  15. 通用局部搜索算法之WALKSAT
  16. auto 和 auto
  17. 羊毛党大揭秘:上亿黑卡在手,撸垮上市公司
  18. 最优化方法期末考试复习
  19. 巡检报告实例-Python脚本生成
  20. 在WPS 中使用LaTeX

热门文章

  1. 你的代码是如何被炫技毁掉的
  2. ZOJ3635 Cinema in Akiba(线段树)
  3. 数学建模常用模型方法
  4. 网络编程中Nagle算法和Delayed ACK的测试(转)
  5. python pyinstall 打包EXE
  6. 开始学CPU啦,任务艰巨
  7. 在PLC控制器CPU多核的基本概念
  8. 亚马逊官方选品工具——“入驻卖家产品指南”使用方法-跨境知道
  9. 找到一个数组中每一个元素第一个比它大的元素
  10. 如何使用 flv.js 做直播