野火🔥

生命如野火,骄傲而顽强

iOS数据的磁盘缓存实现

虽然第三方框架AFNetworking SDWebImage都有自己的内存缓存和数据缓存,但有时候app内部有一些数据(json、image等)可能有必要单独缓存进行管理,本文就根据最近的需求设计了一个比较简单磁盘缓存工具。

代码前根据需求进行一些分析设计

  • 缓存管理的全局性,使得该类应由单例管理
  • 因为磁盘缓存,所以有必要在子线程处理,避免卡IO
  • 缓存应该有过期时间管理,有默认值、也可以自定义
  • 能够具备 增、删、查的操作
  • 因为要根据url和params缓存返回的json数据,所以要有对url为key的处理

根据设计,定义h文件的public方法和public的成员变量

@interface QDiskCacheService : QService<QService>
- (void)clearCache;
- (void)removeCacheForKey:(NSString* __nonnull)key;
- (BOOL)hasCacheForKey:(NSString* __nonnull)key;

- (NSDictionary * __nullable)jsonForUrl:(NSString * __nonnull)url withParams:(NSDictionary* __nullable)params;
- (void) setJson:(NSDictionary * __nullable)item forURL:(NSString * __nonnull)url withParams:(NSDictionary* __nullable)params;
- (void) setJson:(NSDictionary * __nullable)item forURL:(NSString * __nonnull)url withParams:(NSDictionary* __nullable)params withTimeoutInterval:(NSTimeInterval)timeoutInterval;

- (NSData* __nullable)dataForKey:(NSString* __nonnull)key;
- (void)setData:(NSData* __nonnull)data forKey:(NSString* __nonnull)key;
- (void)setData:(NSData* __nonnull)data forKey:(NSString* __nonnull)key withTimeoutInterval:(NSTimeInterval)timeoutInterval;
@property(nonatomic) NSTimeInterval defaultTimeoutInterval; //默认缓存时间 
@end

代码中QService为个人定义的一个协议,用于统一管理单例,在工程中,凡是实现该协议的类,均可以通过一个固定的宏实现单例,以后会单独一片文章介绍这个用于单例管理的QServiceCenter

由于需要设定保存时间,所以设计一个plist用于保存缓存的key和过期时间,该plist的处理需要一个队列处理,文件保存本地和本地读取的操作在另一个队列完成。

m文件中需要定义一个方法来将params和url合并成一个key的方法。下面是实现细节:

  1. init方法需要初始化几个线程队列,初始化目录并把过期缓存清理

    _cacheInfoQueue = dispatch_queue_create("com.qdaily.qcache.info", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t priority = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    dispatch_set_target_queue(priority, _cacheInfoQueue);
    _frozenCacheInfoQueue = dispatch_queue_create("com.qdaily.qcache.info.frozen", DISPATCH_QUEUE_SERIAL);
    priority = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    dispatch_set_target_queue(priority, _frozenCacheInfoQueue);
    _diskQueue = dispatch_queue_create("com.qdaily.qcache.disk", DISPATCH_QUEUE_CONCURRENT);
    priority = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
    dispatch_set_target_queue(priority, _diskQueue);
    _directory = cacheDirectory;
    _cacheInfo = [[NSDictionary dictionaryWithContentsOfFile:cachePathForKey(_directory, @"QCache.plist")] mutableCopy];
    if(!_cacheInfo) {
    _cacheInfo = [[NSMutableDictionary alloc] init];
    }
    [[NSFileManager defaultManager] createDirectoryAtPath:_directory withIntermediateDirectories:YES attributes:nil error:NULL];
    NSTimeInterval now = [[NSDate date] timeIntervalSinceReferenceDate];
    NSMutableArray* removedKeys = [[NSMutableArray alloc] init];
    for(NSString* key in _cacheInfo) {
    if([_cacheInfo[key] timeIntervalSinceReferenceDate] <= now) {
    [[NSFileManager defaultManager] removeItemAtPath:cachePathForKey(_directory, key) error:NULL];
    [removedKeys addObject:key];
    }
    }
    [_cacheInfo removeObjectsForKeys:removedKeys];
    self.frozenCacheInfo = _cacheInfo;
    [self setDefaultTimeoutInterval:86400*7];
  2. 缓存的一些必须有的基本操作,清空,删除,多是对缓存列表(plist)的处理

    - (void)clearCache {
        dispatch_sync(_cacheInfoQueue, ^{
    for(NSString* key in _cacheInfo) {
    [[NSFileManager defaultManager] removeItemAtPath:cachePathForKey(_directory, key) error:NULL];
    }
    [_cacheInfo removeAllObjects];
    dispatch_sync(_frozenCacheInfoQueue, ^{
    self.frozenCacheInfo = [_cacheInfo copy];
    });
    [self setNeedsSave];
    });
    }

    - (void)removeCacheForKey:(NSString*)key {
        CHECK_FOR_QCACHE_PLIST();

        dispatch_async(_diskQueue, ^{
            [[NSFileManager defaultManager] removeItemAtPath:cachePathForKey(_directory, key) error:NULL];
        });

        [self setCacheTimeoutInterval:0 forKey:key];
    }

    - (BOOL)hasCacheForKey:(NSString*)key {
        NSDate* date = [self dateForKey:key];
        if(date == nil) return NO;
        if([date timeIntervalSinceReferenceDate] < CFAbsoluteTimeGetCurrent()) return NO;

        return [[NSFileManager defaultManager] fileExistsAtPath:cachePathForKey(_directory, key)];
    }

    - (NSDate*)dateForKey:(NSString*)key {
        __block NSDate* date = nil;

        dispatch_sync(_frozenCacheInfoQueue, ^{
            date = (self.frozenCacheInfo)[key];
        });

        return date;
    }

    - (NSArray*)allKeys {
        __block NSArray* keys = nil;

        dispatch_sync(_frozenCacheInfoQueue, ^{
            keys = [self.frozenCacheInfo allKeys];
        });

        return keys;
    }

    - (void)setCacheTimeoutInterval:(NSTimeInterval)timeoutInterval forKey:(NSString*)key {
        NSDate* date = timeoutInterval > 0 ? [NSDate dateWithTimeIntervalSinceNow:timeoutInterval] : nil;

        // Temporarily store in the frozen state for quick reads
        dispatch_sync(_frozenCacheInfoQueue, ^{
            NSMutableDictionary* info = [self.frozenCacheInfo mutableCopy];

            if(date) {
                info[key] = date;
            } else {
                [info removeObjectForKey:key];
            }

            self.frozenCacheInfo = info;
        });

        // Save the final copy (this may be blocked by other operations)
        dispatch_async(_cacheInfoQueue, ^{
            if(date) {
                _cacheInfo[key] = date;
            } else {
                [_cacheInfo removeObjectForKey:key];
            }

            dispatch_sync(_frozenCacheInfoQueue, ^{
                self.frozenCacheInfo = [_cacheInfo copy];
            });

            [self setNeedsSave];
        });
    }

    - (void)setNeedsSave {
        dispatch_async(_cacheInfoQueue, ^{
            if(_needsSave) return;
            _needsSave = YES;

            double delayInSeconds = 0.5;
            dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
            dispatch_after(popTime, _cacheInfoQueue, ^(void){
                if(!_needsSave) return;
                [_cacheInfo writeToFile:cachePathForKey(_directory, @"QCache.plist") atomically:YES];
                _needsSave = NO;
            });
        });
    }
  1. 具体的实现,对于data的实现不列出来了,只标记url的处理,这种保存在debug下按分级目录保存,易于查看和调试,在release下直接采用md5为key

    - (NSDictionary *)jsonForUrl:(NSString *)url withParams:(NSDictionary*)params {
    #ifdef DEBUG
    NSString* filePath = [self keyForUrl:url params:params]; //debug下其实是一个相对路径
    NSDate* date = [self dateForKey:filePath];
    if(date == nil || [date timeIntervalSinceReferenceDate] < CFAbsoluteTimeGetCurrent()) return nil;
    if ([[NSFileManager defaultManager] fileExistsAtPath:[_directory stringByAppendingPathComponent:filePath]]) {
    NSData *data = [NSData dataWithContentsOfFile:[_directory stringByAppendingPathComponent:filePath]];
    NSDictionary *item = [NSKeyedUnarchiver unarchiveObjectWithData:data];
    return item;
    }
    return nil;
    #else
    NSString* key = [self keyForUrl:url params:params];
    return [NSKeyedUnarchiver unarchiveObjectWithData:[self dataForKey:key]];
    #endif
    }

    - (void) setJson:(NSDictionary *)item forURL:(NSString *)url withParams:(NSDictionary*)params {
        [self setJson:item forURL:url withParams:params withTimeoutInterval:self.defaultTimeoutInterval];
    }

    - (void) setJson:(NSDictionary *)item forURL:(NSString *)url withParams:(NSDictionary*)params withTimeoutInterval:(NSTimeInterval)timeoutInterval{
        if (item == nil || item.count == 0) {
            return;
        }
    #ifdef DEBUG
        NSString* filePath = [self keyForUrl:url params:params]; //debug下其实是一个相对路径
        NSData *data = [NSKeyedArchiver archivedDataWithRootObject:item];
        dispatch_async(_diskQueue, ^{
            [data writeToFile:[_directory stringByAppendingPathComponent:filePath] atomically:YES];
        });
        [self setCacheTimeoutInterval:timeoutInterval forKey:filePath];
    #else
        NSString* key = [self keyForUrl:url params:params];
        [self setData:[NSKeyedArchiver archivedDataWithRootObject:item] forKey:key withTimeoutInterval:timeoutInterval];
    #endif
    }

    - (NSString*) keyForUrl:(NSString *)url params:(NSDictionary*)params {
    #ifdef DEBUG
        NSRange range = [url rangeOfString:@"/" options:NSBackwardsSearch];
        NSString *cacheName = [NSString stringWithFormat:@"%@%@", [url substringFromIndex:range.location + range.length], [NSURL queryStringFromParameters:params]];

        NSFileManager *fileManager = [NSFileManager defaultManager];

        NSString* relativePath = [NSString stringWithFormat:@"%@/%@", @"QUrlCaches", [url substringToIndex:range.location]];
        NSString *filePath = [NSString stringWithFormat:@"%@/%@", _directory, relativePath];
        BOOL ui;
        if (![fileManager fileExistsAtPath:filePath isDirectory:&ui])
        {
            [fileManager createDirectoryAtPath:filePath withIntermediateDirectories:YES attributes:nil error:nil];
        }
        return [NSString stringWithFormat:@"%@/%@", relativePath, cacheName];
    #else 
        NSString *cacheName = [NSString stringWithFormat:@"%@%@", url, [NSURL queryStringFromParameters:params]]; //非debug直接对url做md5
        return [cacheName lf_md5Hash];
    #endif
    }

Categories:  技术 

« iOS hook实践(2)---入门插件实现 iOS hook实践(1) »