前言

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

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

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

YYMemoryCache

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

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

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

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

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

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

可以看出来,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 dic
id _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. If
the 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_folder SQL:
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 instead
of NSKeyedArchiver. You can use this block to support the objects which do not
conform 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 instead
of NSKeyedUnarchiver. You can use this block to support the objects which do not
conform 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的作者开源了这么好的东西

深入理解YYCache的更多相关文章

  1. YYCache设计思路及源码学习

    设计思路 利用YYCache来进行操作,实质操作分为了内存缓存操作(YYMemoryCache)和硬盘缓存操作(YYDiskCache).内存缓存设计一般是在内存中开辟一个空间用以保存请求的数据(一般 ...

  2. iOS数据缓存及YYCache的实现分析

    1. 什么是cache cache就是缓存的意思. 计算机上的cache就是高速缓存,计算机组成课程里的定义是,存在于主存和CPU之间,主要用于解决CPU处理数据的速度远远大于读取主存数据的速度. 手 ...

  3. 浅析YYCache

    一.前言 读优秀的源码,对自己的提升还是很快的,无论是考虑问题的角度,还是编码能力. 带着问题读源码的,学习效率更高,可以暂时先定几个小问题,带着问题,去思考为什么作者这样弄,是否有替换方案? 1). ...

  4. 理解CSS视觉格式化

    前面的话   CSS视觉格式化这个词可能比较陌生,但说起盒模型可能就恍然大悟了.实际上,盒模型只是CSS视觉格式化的一部分.视觉格式化分为块级和行内两种处理方式.理解视觉格式化,可以确定得到的效果是应 ...

  5. 彻底理解AC多模式匹配算法

    (本文尤其适合遍览网上的讲解而仍百思不得姐的同学) 一.原理 AC自动机首先将模式组记录为Trie字典树的形式,以节点表示不同状态,边上标以字母表中的字符,表示状态的转移.根节点状态记为0状态,表示起 ...

  6. 理解加密算法(三)——创建CA机构,签发证书并开始TLS通信

    接理解加密算法(一)--加密算法分类.理解加密算法(二)--TLS/SSL 1 不安全的TCP通信 普通的TCP通信数据是明文传输的,所以存在数据泄露和被篡改的风险,我们可以写一段测试代码试验一下. ...

  7. node.js学习(三)简单的node程序&&模块简单使用&&commonJS规范&&深入理解模块原理

    一.一个简单的node程序 1.新建一个txt文件 2.修改后缀 修改之后会弹出这个,点击"是" 3.运行test.js 源文件 使用node.js运行之后的. 如果该路径下没有该 ...

  8. 如何一步一步用DDD设计一个电商网站(一)—— 先理解核心概念

    一.前言     DDD(领域驱动设计)的一些介绍网上资料很多,这里就不继续描述了.自己使用领域驱动设计摸滚打爬也有2年多的时间,出于对知识的总结和分享,也是对自我理解的一个公开检验,介于博客园这个平 ...

  9. 学习AOP之透过Spring的Ioc理解Advisor

    花了几天时间来学习Spring,突然明白一个问题,就是看书不能让人理解Spring,一方面要结合使用场景,另一方面要阅读源代码,这种方式理解起来事半功倍.那看书有什么用呢?主要还是扩展视野,毕竟书是别 ...

随机推荐

  1. 关于WordPress搬家方法步骤的整理

    最近准备更换自己的博客服务器,所以需要将原来服务器上的所有东西都搬到新的服务器.为了数据的安全,在网上找了很多的资料.现在整理一下整个搬家过程的操作步骤.下面进入正题: 1.测试环境这次我使用的示例服 ...

  2. poj 1001 分析

    1) n = 0; return 1: 2) n = 1; bool standardizeNumNoDot(string &s){标准化是一定要得} _将‘.’前后的〇全部去除,正常retu ...

  3. 基于三层交换机和基于路由子接口的vlan间路由

    1:通过三层交换机实现vlan间的通信:为三层交换机创建vlan,设置交换机的两个SVI,并配置IP地址. (在二层交换机上只能配置一个SVI端口,用来实现交换机交换机远程管理,在三层交换机上可以配置 ...

  4. 读《Java并发编程的艺术》(一)

    离开博客园很久了,自从找到工作,到现在基本没有再写过博客了.在大学培养起来的写博客的习惯在慢慢的消失殆尽,感觉汗颜.所以现在要开始重新培养起这个习惯,定期写博客不仅是对自己学习知识的一种沉淀,更是在督 ...

  5. 简单实现服务器/客户端的c代码

    #include<stdio.h> #include<stdlib.h> #include<string.h> #include<sys/types.h> ...

  6. IE haslayout

    我们都知道浏览器有bug,而IE的bug似乎比大多数浏览器都多.IE的表现与其他浏览器不同的原因之一就是,显示引擎使用一个称为布局(layout)的内部概念.   因为布局是专门针对显示引擎内部工作方 ...

  7. 开涛spring3(7.4) - 对JDBC的支持 之 7.4 Spring提供的其它帮助

    7.4  Spring提供的其它帮助 7.4.1  SimpleJdbc方式 Spring JDBC抽象框架提供SimpleJdbcInsert和SimpleJdbcCall类,这两个类通过利用JDB ...

  8. 2.solr学习速成之安装

    1.下载解压solr-5.3.1.tgz [root@205 opt]# tar -zxf solr-5.3.1.tgz -C /opt/module/ 2.将solr-5.3.1/server/so ...

  9. 一个gif远程crash你的微信!

    测试了一下iPhone6,iPhone7不同版本的 iOS上,收到某个天线宝宝的gif,最新版本的微信都会挂. demo视频:演示视频 通过 crash log可以看到微信最新的6.5.8版本在打开 ...

  10. Tomcat 连接池详解

    (转) JDBC 连接池 org.apache.tomcat.jdbc.pool 是Apache-Commons DBCP连接池的一种替换或备选方案. 那究竟为何需要一个新的连接池? 原因如下: Co ...