YYCache 源码分析 拾遗

YYCache,作为一个非常优秀的开源iOS缓存框架,其代码非常值得学习。网上已经有大量的源码分析文章,再加上原作者也有一篇非常优秀的博文,因此我也不再复述,在这里推荐给大家几篇我觉的写的不错的源码分析的博客:

那么这篇文章说什么,拾遗!就是那些我在读代码时,觉得有意思的或者我们值得借鉴的,又或者有所拓展的内容。他们不一定是YYCache的核心内容,但却体现了作者的严谨和对iOS/C的理解和运用的能力。

「__has_include」宏

#if __has_include(<YYCache/YYCache.h>)
FOUNDATION_EXPORT double YYCacheVersionNumber;
FOUNDATION_EXPORT const unsigned char YYCacheVersionString[];
#import <YYCache/YYMemoryCache.h>
#import <YYCache/YYDiskCache.h>
#import <YYCache/YYKVStorage.h>
#elif __has_include(<YYWebImage/YYCache.h>)
#import <YYWebImage/YYMemoryCache.h>
#import <YYWebImage/YYDiskCache.h>
#import <YYWebImage/YYKVStorage.h>
#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,这里就不详细展开了,有兴趣的可以参考下面这两篇文章,包括

@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预定义的宏,方便调试,除此之外,还有TIMEFILEDATE等。

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的运用

屏幕快照 2018-06-14 上午10.46.31.png

如果你想使用SQLite,那么这也是一个非常好的范本,很多API可以拿来直接使用。

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

推荐阅读更多精彩内容