YYCache,作为一个非常优秀的开源iOS缓存框架,其代码非常值得学习。网上已经有大量的源码分析文章,再加上原作者也有一篇非常优秀的博文,因此我也不再复述,在这里推荐给大家几篇我觉的写的不错的源码分析的博客:
- 首先当然是YYCache作者ibireme写的YYCache 设计思路
- 然后是爱生活的小悦悦的博客写的YYCache源码分析系列:
- YYCache源码分析(一) - YYMemoryCache
- YYCache源码分析(二) - YYDiskCache
- YYCache源码分析(三) - YYKVStorage
- 最后一篇是来自马在路上的# 深入理解YYCache,因为视角和上一篇稍微有点不一样,所以在这里也推荐给大家。
那么这篇文章说什么,拾遗!就是那些我在读代码时,觉得有意思的或者我们值得借鉴的,又或者有所拓展的内容。他们不一定是YYCache的核心内容,但却体现了作者的严谨和对iOS/C的理解和运用的能力。
「__has_include」宏
#if __has_include()
FOUNDATION_EXPORT double YYCacheVersionNumber;
FOUNDATION_EXPORT const unsigned char YYCacheVersionString[];
#import
#import
#import
#elif __has_include()
#import
#import
#import
#else
#import "YYMemoryCache.h"
#import "YYDiskCache.h"
#import "YYKVStorage.h"
#endif
先来看这段代码, 在#if/#elif/#else/#endif宏中出现了__has_include()这个宏,此宏传入一个你想引入文件的名称作为参数,如果该文件能够被引入则返回1,否则返回0。所以上面这段的意思就是
- 首先检查是否存在YYCache框架,如果存在,则引入YYCache框架下的三个头文件
- 否则检查是否存在YYWebImage框架,如果存在,则引入YYWebImage框架下的三个头文件。
- 否则直接引入三个头文件。
可以看出,引入框架下的头文件,使用了左右尖括号< >
并添加了框架目录,而非框架下的引入则使用了双引号" "
FOUNDATION_EXPORT
FOUNDATION_EXPORT具体是什么,来看看这个宏的定义
#if defined(__cplusplus)
#define FOUNDATION_EXTERN extern "C"
#else
#define FOUNDATION_EXTERN extern
#endif
#if TARGET_OS_WIN32
#if defined(NSBUILDINGFOUNDATION)
#define FOUNDATION_EXPORT FOUNDATION_EXTERN __declspec(dllexport)
#else
#define FOUNDATION_EXPORT FOUNDATION_EXTERN __declspec(dllimport)
#endif
#define FOUNDATION_IMPORT FOUNDATION_EXTERN __declspec(dllimport)
#else
#define FOUNDATION_EXPORT FOUNDATION_EXTERN
#define FOUNDATION_IMPORT FOUNDATION_EXTERN
#endif
可以看到,在通常的iOS开发中,FOUNDATION_EXPORT或者FOUNDATION_IMPORT等同于extern。使用FOUNDATION_EXPORT或者FOUNDATION_IMPORT更具有平台或者语言兼容性,你可以看到在C++环境中,又或者Windows环境中,他们的定义会发生改变。
NS_ASSUME_NONNULL_BEGIN/NS_ASSUME_NONNULL_END
在Swift中存在Option类型,也就是使用?和!声明的变量。但是OC里面没有这个特征 ,因此在XCODE6.3之后出现新的关键词(__nullable && ___nonnull)定义用于OC转SWIFT时候可以区分到底是什么类型
- __nullable指代对象可以为NULL或者为NIL
- __nonnull指代对象不能为null
当我们不遵循这一规则时,编译器就会给出警告。
但如果需要每个属性或每个方法都去指定nonnull和nullable,是一件非常繁琐的事。苹果为了减轻我们的工作量,专门提供了两个宏:NS_ASSUME_NONNULL_BEGIN和NS_ASSUME_NONNULL_END。在这两个宏之间的代码,所有简单指针对象都被假定为nonnull,因此我们只需要去指定那些nullable的指针。
atomic
@interface YYCache : NSObject
/** The name of the cache, readonly. */
@property (copy, readonly) NSString *name;
/** The underlying memory cache. see `YYMemoryCache` for more information.*/
@property (strong, readonly) YYMemoryCache *memoryCache;
/** The underlying disk cache. see `YYDiskCache` for more information.*/
@property (strong, readonly) YYDiskCache *diskCache;
可以看到,YYCache类的属性定义,并没有使用我们常用的nonatomic属性,而是使用了默认的atomic。同样,你可以看到在YYMemoryCache,YYDiskCache这些需要线程安全的类中,属性都使用了atomic。而YYKVStorage类中,属性使用的是nonatomic,也就是说YYKVStorage是线程不安全的。
当然atomic属性只能保证属性的原子性,也就是说在属性访问/设置时保证属性被唯一访问。但并不能保证类是线程安全的。
UNAVAILABLE_ATTRIBUTE
- (instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (instancetype)new UNAVAILABLE_ATTRIBUTE;
C++中我们可以声明构造函数为私有,从而来禁止用通用的构造函数来生成一个对象。那么在OC中,如果我们需要来屏蔽init或者new这些方法,从而告知使用者必须用我们指定的构造方法来生成一个对象,这该怎么做呢?我们来看看UNAVAILABLE_ATTRIBUTE的定义
#if defined(__GNUC__) && ((__GNUC__ >= 4) || ((__GNUC__ == 3) && (__GNUC_MINOR__ >= 1)))
#define UNAVAILABLE_ATTRIBUTE __attribute__((unavailable))
#else
#define UNAVAILABLE_ATTRIBUTE
#endif
由于attribute是GNU C特色之一,可以看到如果不是GNU C,UNAVAILABLE_ATTRIBUTE相当于空,而在GNU C环境下,为attribute((unavailable))。告诉编译器该方法不可用,如果强行调用编译器会提示错误。
关于attribute,这里就不详细展开了,有兴趣的可以参考下面这两篇文章,包括
- 来自nshipster的attribute
- 以及来自的attribute 总结
@package
@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
我们在_YYLinkedMapNode的定义中看到了@package关键字,简单来说, @package变量,对于framework内部,相当于@protected, 对于framework外部,相当于@private。
这个特性,很适合用于开发第三方框架,因为我们并不希望让别人知道自己属性的值。
dispatch_after实现重复的延时触发器
- (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];
});
}
上面代码可以用作一个重复的延时触发器。当然我们可以用NSTimer来实现一个重复的延时触发器,但NSTimer基于Runloop,而在自线程中默认的Runloop并没有开启。而dispatch_after并没有这种问题。
同时可以看到,这边的执行的队列使用了系统定义的DISPATCH_QUEUE_PRIORITY_LOW并发队列。
pthread_mutex
由于OSSpinLock不再线程安全的缘故 (不再安全的 OSSpinLock),因此在内存加锁上使用了pthread_mutex。
pthread_mutex 互斥锁是一种超级易用的互斥锁,使用的时候,只需要初始化一个 pthread_mutex_t 用 pthread_mutex_lock 来锁定 pthread_mutex_unlock 来解锁,当使用完成后,记得调用 pthread_mutex_destroy 来销毁锁。
pthread_mutex_init(&lock,NULL);
pthread_mutex_lock(&lock);
//do your stuff
pthread_mutex_unlock(&lock);
pthread_mutex_destroy(&lock);
在阅读YYMemoryCache.m源码的时候,你可以看到,为了保证线程安全,所有的缓存相关的操作,都进行了加锁/解锁操作。
dispatch_semaphore_t
在YYDiskCache.m文件中我们可以看到,在初始化函数中,信号总量被定义为1,所以dispatch_semaphore_wait和dispatch_semaphore_signal函数可以类似的用作加锁/解锁操作,可以看到该文件头部定义的两个宏。
_lock = dispatch_semaphore_create(1);
#define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER)
#define Unlock() dispatch_semaphore_signal(self->_lock)
作者认为在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。相对于 pthread_mutex 来说,它的优势在于等待时不会消耗 CPU 资源。因此对磁盘缓存来说,它比较合适。
beginBackgroundTaskWithExpirationHandler
- (void)dealloc {
UIBackgroundTaskIdentifier taskID = [_YYSharedApplication() beginBackgroundTaskWithExpirationHandler:^{}];
[self _dbClose];
if (taskID != UIBackgroundTaskInvalid) {
[_YYSharedApplication() endBackgroundTask:taskID];
}
}
在YYKVStorage.m的dealloc代码中,我们看到了beginBackgroundTaskWithExpirationHandler/endBackgroundTask的调用。beginBackgroundTaskWithExpirationHandler方法允许你的APP在退至后台后继续运行一段时间。在这里我们看到处理就是需要将DB关闭。
获取UIApplication实例
/// Returns nil in App Extension.
static UIApplication *_YYSharedApplication() {
static BOOL isAppExtension = NO;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = NSClassFromString(@"UIApplication");
if(!cls || ![cls respondsToSelector:@selector(sharedApplication)]) isAppExtension = YES;
if ([[[NSBundle mainBundle] bundlePath] hasSuffix:@".appex"]) isAppExtension = YES;
});
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
return isAppExtension ? nil : [UIApplication performSelector:@selector(sharedApplication)];
#pragma clang diagnostic pop
}
在这里,我们可以看到作者的考虑的全面。在获取SharedApplication的时候,考虑了是否为App Extension。
同时注意的细节,clang diagnostic ignored配合clang diagnostic push/pop,让编译器在这一行忽略undeclared-selector的警告。
FUNCTION/LINE
我们在YYKVStorage.m的源码中,可以看到下面这种NSLog。
NSLog(@"%s line:%d sqlite open failed (%d).", __FUNCTION__, __LINE__, result);
这是GCC预定义的宏,方便调试,除此之外,还有TIME、FILE、DATE等。
MD5
/// String's md5 hash.
static NSString *_YYNSStringMD5(NSString *string) {
if (!string) return nil;
NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
unsigned char result[CC_MD5_DIGEST_LENGTH];
CC_MD5(data.bytes, (CC_LONG)data.length, result);
return [NSString stringWithFormat:
@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
result[0], result[1], result[2], result[3],
result[4], result[5], result[6], result[7],
result[8], result[9], result[10], result[11],
result[12], result[13], result[14], result[15]
];
}
MD5消息摘要算法5,苹果自带的CC_MD5就支持这种算法。在这里,作者用MD5算法来生成文件名,来用于文件类型的缓存存储。
具体的MD5的详细解释,可以参考:iOS MD5 (消息摘要算法5)
良好的注释/完美的代码结构
#pragma mark - Save Items
///=============================================================================
/// @name Save Items
///=============================================================================
/**
Save an item or update the item with 'key' if it already exists.
@discussion This method will save the item.key, item.value, item.filename and
item.extendedData to disk or sqlite, other properties will be ignored. item.key
and item.value should not be empty (nil or zero length).
If the `type` is YYKVStorageTypeFile, then the item.filename should not be empty.
If the `type` is YYKVStorageTypeSQLite, then the item.filename will be ignored.
It the `type` is YYKVStorageTypeMixed, then the item.value will be saved to file
system if the item.filename is not empty, otherwise it will be saved to sqlite.
@param item An item.
@return Whether succeed.
*/
- (BOOL)saveItem:(YYKVStorageItem *)item;
#pragma mark - db
- (BOOL)_dbCheck {
if (!_db) {
if (_dbOpenErrorCount < kMaxErrorRetryCount &&
CACurrentMediaTime() - _dbLastOpenErrorTime > kMinRetryTimeInterval) {
return [self _dbOpen] && [self _dbInitialize];
} else {
return NO;
}
}
return YES;
}
上面仅仅是两个例子,大家可以看到作者对函数的注释写的非常漂亮。此外,私有函数命名以_开始,并用#pragma mark分割功能块。这都是非常值得借鉴学习的。
SQLite的运用
如果你想使用SQLite,那么这也是一个非常好的范本,很多API可以拿来直接使用。