//
|
// YYDiskCache.m
|
// YYCache <https://github.com/ibireme/YYCache>
|
//
|
// Created by ibireme on 15/2/11.
|
// Copyright (c) 2015 ibireme.
|
//
|
// This source code is licensed under the MIT-style license found in the
|
// LICENSE file in the root directory of this source tree.
|
//
|
|
#import "YYDiskCache.h"
|
#import "YYKVStorage.h"
|
#import <UIKit/UIKit.h>
|
#import <CommonCrypto/CommonCrypto.h>
|
#import <objc/runtime.h>
|
#import <time.h>
|
|
#define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER)
|
#define Unlock() dispatch_semaphore_signal(self->_lock)
|
|
static const int extended_data_key;
|
|
/// Free disk space in bytes.
|
static int64_t _YYDiskSpaceFree() {
|
NSError *error = nil;
|
NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfFileSystemForPath:NSHomeDirectory() error:&error];
|
if (error) return -1;
|
int64_t space = [[attrs objectForKey:NSFileSystemFreeSize] longLongValue];
|
if (space < 0) space = -1;
|
return space;
|
}
|
|
/// 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]
|
];
|
}
|
|
/// weak reference for all instances
|
static NSMapTable *_globalInstances;
|
static dispatch_semaphore_t _globalInstancesLock;
|
|
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];
|
});
|
}
|
|
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;
|
}
|
|
static void _YYDiskCacheSetGlobal(YYDiskCache *cache) {
|
if (cache.path.length == 0) return;
|
_YYDiskCacheInitGlobal();
|
dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER);
|
[_globalInstances setObject:cache forKey:cache.path];
|
dispatch_semaphore_signal(_globalInstancesLock);
|
}
|
|
|
|
@implementation YYDiskCache {
|
YYKVStorage *_kv;
|
dispatch_semaphore_t _lock;
|
dispatch_queue_t _queue;
|
}
|
|
- (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 {
|
__weak typeof(self) _self = self;
|
dispatch_async(_queue, ^{
|
__strong typeof(_self) self = _self;
|
if (!self) return;
|
Lock();
|
[self _trimToCost:self.costLimit];
|
[self _trimToCount:self.countLimit];
|
[self _trimToAge:self.ageLimit];
|
[self _trimToFreeDiskSpace:self.freeDiskSpaceLimit];
|
Unlock();
|
});
|
}
|
|
- (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];
|
}
|
|
- (NSString *)_filenameForKey:(NSString *)key {
|
NSString *filename = nil;
|
if (_customFileNameBlock) filename = _customFileNameBlock(key);
|
if (!filename) filename = _YYNSStringMD5(key);
|
return filename;
|
}
|
|
- (void)_appWillBeTerminated {
|
Lock();
|
_kv = nil;
|
Unlock();
|
}
|
|
#pragma mark - public
|
|
- (void)dealloc {
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillTerminateNotification object:nil];
|
}
|
|
- (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;
|
}
|
|
- (BOOL)containsObjectForKey:(NSString *)key {
|
if (!key) return NO;
|
Lock();
|
BOOL contains = [_kv itemExistsForKey:key];
|
Unlock();
|
return contains;
|
}
|
|
- (void)containsObjectForKey:(NSString *)key withBlock:(void(^)(NSString *key, BOOL contains))block {
|
if (!block) return;
|
__weak typeof(self) _self = self;
|
dispatch_async(_queue, ^{
|
__strong typeof(_self) self = _self;
|
BOOL contains = [self containsObjectForKey:key];
|
block(key, contains);
|
});
|
}
|
|
- (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;
|
}
|
|
- (void)objectForKey:(NSString *)key withBlock:(void(^)(NSString *key, id<NSCoding> object))block {
|
if (!block) return;
|
__weak typeof(self) _self = self;
|
dispatch_async(_queue, ^{
|
__strong typeof(_self) self = _self;
|
id<NSCoding> object = [self objectForKey:key];
|
block(key, object);
|
});
|
}
|
|
- (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();
|
}
|
|
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key withBlock:(void(^)(void))block {
|
__weak typeof(self) _self = self;
|
dispatch_async(_queue, ^{
|
__strong typeof(_self) self = _self;
|
[self setObject:object forKey:key];
|
if (block) block();
|
});
|
}
|
|
- (void)removeObjectForKey:(NSString *)key {
|
if (!key) return;
|
Lock();
|
[_kv removeItemForKey:key];
|
Unlock();
|
}
|
|
- (void)removeObjectForKey:(NSString *)key withBlock:(void(^)(NSString *key))block {
|
__weak typeof(self) _self = self;
|
dispatch_async(_queue, ^{
|
__strong typeof(_self) self = _self;
|
[self removeObjectForKey:key];
|
if (block) block(key);
|
});
|
}
|
|
- (void)removeAllObjects {
|
Lock();
|
[_kv removeAllItems];
|
Unlock();
|
}
|
|
- (void)removeAllObjectsWithBlock:(void(^)(void))block {
|
__weak typeof(self) _self = self;
|
dispatch_async(_queue, ^{
|
__strong typeof(_self) self = _self;
|
[self removeAllObjects];
|
if (block) block();
|
});
|
}
|
|
- (void)removeAllObjectsWithProgressBlock:(void(^)(int removedCount, int totalCount))progress
|
endBlock:(void(^)(BOOL error))end {
|
__weak typeof(self) _self = self;
|
dispatch_async(_queue, ^{
|
__strong typeof(_self) self = _self;
|
if (!self) {
|
if (end) end(YES);
|
return;
|
}
|
Lock();
|
[_kv removeAllItemsWithProgressBlock:progress endBlock:end];
|
Unlock();
|
});
|
}
|
|
- (NSInteger)totalCount {
|
Lock();
|
int count = [_kv getItemsCount];
|
Unlock();
|
return count;
|
}
|
|
- (void)totalCountWithBlock:(void(^)(NSInteger totalCount))block {
|
if (!block) return;
|
__weak typeof(self) _self = self;
|
dispatch_async(_queue, ^{
|
__strong typeof(_self) self = _self;
|
NSInteger totalCount = [self totalCount];
|
block(totalCount);
|
});
|
}
|
|
- (NSInteger)totalCost {
|
Lock();
|
int count = [_kv getItemsSize];
|
Unlock();
|
return count;
|
}
|
|
- (void)totalCostWithBlock:(void(^)(NSInteger totalCost))block {
|
if (!block) return;
|
__weak typeof(self) _self = self;
|
dispatch_async(_queue, ^{
|
__strong typeof(_self) self = _self;
|
NSInteger totalCost = [self totalCost];
|
block(totalCost);
|
});
|
}
|
|
- (void)trimToCount:(NSUInteger)count {
|
Lock();
|
[self _trimToCount:count];
|
Unlock();
|
}
|
|
- (void)trimToCount:(NSUInteger)count withBlock:(void(^)(void))block {
|
__weak typeof(self) _self = self;
|
dispatch_async(_queue, ^{
|
__strong typeof(_self) self = _self;
|
[self trimToCount:count];
|
if (block) block();
|
});
|
}
|
|
- (void)trimToCost:(NSUInteger)cost {
|
Lock();
|
[self _trimToCost:cost];
|
Unlock();
|
}
|
|
- (void)trimToCost:(NSUInteger)cost withBlock:(void(^)(void))block {
|
__weak typeof(self) _self = self;
|
dispatch_async(_queue, ^{
|
__strong typeof(_self) self = _self;
|
[self trimToCost:cost];
|
if (block) block();
|
});
|
}
|
|
- (void)trimToAge:(NSTimeInterval)age {
|
Lock();
|
[self _trimToAge:age];
|
Unlock();
|
}
|
|
- (void)trimToAge:(NSTimeInterval)age withBlock:(void(^)(void))block {
|
__weak typeof(self) _self = self;
|
dispatch_async(_queue, ^{
|
__strong typeof(_self) self = _self;
|
[self trimToAge:age];
|
if (block) block();
|
});
|
}
|
|
+ (NSData *)getExtendedDataFromObject:(id)object {
|
if (!object) return nil;
|
return (NSData *)objc_getAssociatedObject(object, &extended_data_key);
|
}
|
|
+ (void)setExtendedData:(NSData *)extendedData toObject:(id)object {
|
if (!object) return;
|
objc_setAssociatedObject(object, &extended_data_key, extendedData, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
}
|
|
- (NSString *)description {
|
if (_name) return [NSString stringWithFormat:@"<%@: %p> (%@:%@)", self.class, self, _name, _path];
|
else return [NSString stringWithFormat:@"<%@: %p> (%@)", self.class, self, _path];
|
}
|
|
- (BOOL)errorLogsEnabled {
|
Lock();
|
BOOL enabled = _kv.errorLogsEnabled;
|
Unlock();
|
return enabled;
|
}
|
|
- (void)setErrorLogsEnabled:(BOOL)errorLogsEnabled {
|
Lock();
|
_kv.errorLogsEnabled = errorLogsEnabled;
|
Unlock();
|
}
|
|
@end
|