iOS文件缓存

Plist文件可以直接映射为NSDictionary和NSArray,是使用非常广泛了一种文件格式。 iOS项目开发过程中我们要用Plist文件保存一些界面的开启次数、判断用户是否是第一次进入界面、保存用户的一些配置信息等等。


接下来我们先聊聊Plist文件读写可能遇到的一些问题。

1、读写文件效率问题

iOS对于文件的读写速度优化是非常好的,在iPhone7上测试的一些几百Kb的文件读写速度达都在十几毫秒左右,这样的效率在大多数情况下,基本不会对界面流程度造成影响。但是如果同时出现了较多次数的文件读写,特别是在主线程进行大量的文件读写操作,就可能会造成一些界面卡顿的情况。例如:

一个界面Push的时候,需要读写十个状态,这时界面的卡顿时间就可能有100多毫秒,按照1秒60帧的最优帧率计算(也就是16.7毫秒左右一帧),这时可能要丢失10帧左右。界面流程度就严重下降了。

2、多线程读写的问题误解

我们来看看NSDictionary自带的writeToFile方法:

- (BOOL)writeToFile:(NSString *)path atomically:(BOOL)useAuxiliaryFile;

path:指定了写入的文件路径
useAuxiliaryFile:则用来控制是否要原子写入。
下面是官方文档对useAuxiliaryFile的解释:

A flag that specifies whether the file should be written atomically.
If flag is YES, the dictionary is written to an auxiliary file, and then the auxiliary file is renamed to path. If flag is NO, the dictionary is written directly to path. The YES option guarantees that path, if it exists at all, won’t be corrupted even if the system should crash during writing.

大意:如果标志为YES,则将字典写入辅助文件,然后将辅助文件重命名到path 。如果标志为NO,则字典直接写入path。 YES选项保证路径(如果存在)将不会被破坏,即使系统在写入时应该崩溃。
简单的说就是,旧内容不会改变,除非新内容写入结束。

逻辑示意图如下:

但是这里的useAuxiliaryFile和属性的atomic是完全不同的两个概念。属性的atomic实际上是给属性加锁,同时只能有一个线程在写入,而useAuxiliaryFile只是用来控制文件的完整性,多个线程是可以同时进行写操作的,只不过都是写入临时文件,谁写的快谁就先写入到真正文件path路径,出现的问题显而易见,很可能最终保存的是一份过期的脏数据。
逻辑示意图如下:

3、解决方案

为了处理好上面两个问题,这里采用的文件缓存逻辑。大致步骤:

1、使用了两个队列控制:写文件队列、读写缓存队列。
2、为了保证文件的完整性和先进先出,写文件队列设计为同步队列,保证同时只有一个缓存在写文件。
3、读写缓存队列因为涉及多线程的读写使用了异步队列,同时为了保证写缓存的同时不能有读文件的操作(因为可能读到的是脏数据,并不是最新的),使用了GCD的barrier进行控制

逻辑示意图入下:

image3.jpg

看看dispatch_barrier_async函数原型:

void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);

看看官方对barrier类型函数的解释

/*!

  • @functiongroup Dispatch Barrier API
  • The dispatch barrier API is a mechanism for submitting barrier blocks to a
  • dispatch queue, analogous to the dispatch_async()/dispatch_sync() API.
  • It enables the implementation of efficient reader/writer schemes.
  • Barrier blocks only behave specially when submitted to queues created with
  • the DISPATCH_QUEUE_CONCURRENT attribute; on such a queue, a barrier block
  • will not run until all blocks submitted to the queue earlier have completed,
  • and any blocks submitted to the queue after a barrier block will not run
  • until the barrier block has completed.
  • When submitted to a a global queue or to a queue not created with the
  • DISPATCH_QUEUE_CONCURRENT attribute, barrier blocks behave identically to
  • blocks submitted with the dispatch_async()/dispatch_sync() API.
    */

barrier能够实现高效的读/写方案,barrier中的bolck代码块需要等在barrier之前添加到队列中的代码块执行完才开始执行,同时只有barrier中的bolck代码块在执行完之前,其他代码块不会执行。

如果将barrier提交到一个全局队列或不是使用 DISPATCH_QUEUE_CONCURRENT创建的队列,barrier块的行为与dispatch_async() / dispatch_sync()API是相同的。

下面看些详细代码

@interface FSFileCache : NSObject

@end

@implementation FSFileCache
{
    NSMutableDictionary *cacheDict;
    dispatch_queue_t synQueue;
    dispatch_queue_t writeQueue;
}

+(instancetype) shareInstance
{
    static FSFileCache *fileCache = nil;
    if (fileCache == nil)
    {
        fileCache = [[FSFileCache alloc] init];
    }
    return fileCache;
}

-(instancetype)init
{
    self = [super init];
    if (self) {
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceivememoryWarning) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
        
        cacheDict = [NSMutableDictionary dictionaryWithCapacity:1.0];
        synQueue = dispatch_queue_create("com.FSFile.synQueue", DISPATCH_QUEUE_CONCURRENT);
        writeQueue = dispatch_queue_create("com.FSFile.write", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

-(void)didReceivememoryWarning
{
    dispatch_barrier_async(synQueue, ^{
        [cacheDict removeAllObjects];
    });
}

-(void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

-(id) objForKey:(NSString *)key withPath:(NSString *)path
{
    CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
    NSAssert(key!=nil, nil);
    NSAssert(path!=nil, nil);
    
    if (key == nil || path == nil)
    {
        return nil;
    }
    
    __block id obj = nil;
    dispatch_sync(synQueue, ^{
        NSMutableDictionary *fileDic = [self getAndCacheFileWithPath:path autoCreateFile:NO];
        
        if ([fileDic isKindOfClass:[NSDictionary class]])
        {
            obj = fileDic[key];
        }
    });
    CFAbsoluteTime end = CFAbsoluteTimeGetCurrent();
    
    NSLog(@"get obj forKey:%@ time : %fms", key,(end-start)*1000);
    return obj;
}

-(BOOL) setObj:(id)obj forKey:(NSString *)key withPath:(NSString *)path
{
    CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();

    NSAssert(key!=nil, nil);
    NSAssert(path!=nil, nil);
    
    if (key == nil || path == nil)
    {
        return NO;
    }

    dispatch_barrier_async(synQueue, ^{
        NSMutableDictionary *fileDic = [self getAndCacheFileWithPath:path autoCreateFile:YES];
        if ([fileDic isKindOfClass:[NSDictionary class]]) {
            fileDic[key] = obj;
            [self saveFile:path withDict:fileDic];
        }
    });
    CFAbsoluteTime end = CFAbsoluteTimeGetCurrent();
    
    NSLog(@"set obj forKey %@ time : %fms", key,(end-start)*1000);
    return YES;
}

-(NSMutableDictionary *)getAndCacheFileWithPath:(NSString *)path autoCreateFile:(BOOL)autoCreateFile
{
    NSAssert(path!=nil, nil);
    if (path == nil)
    {
        return nil;
    }
    
    NSMutableDictionary *fileDict = nil;
    
    NSFileManager *fileManager = [NSFileManager defaultManager];
    
    if (cacheDict[path] && [fileManager fileExistsAtPath:path]){
        fileDict = cacheDict[path];
        return fileDict;
    }
    
    if ([fileManager fileExistsAtPath:path]) {
        fileDict = [[NSDictionary dictionaryWithContentsOfFile:path] mutableCopy];
    }else if (autoCreateFile){
        NSError *error = [EntrysOperateHelper touchDirForFilePath:path];
        if(error) {
            NSLogToFile(@"Warn: ************ create global stroage directory fail");
        }
#if(!TARGET_IPHONE_SIMULATOR)
        NSDictionary *attr = @{NSFileProtectionKey: NSFileProtectionCompleteUntilFirstUserAuthentication};
#else
        NSDictionary *attr = nil;
#endif
        if (error == nil) {
            if([fileManager createFileAtPath:path contents:nil attributes:attr]) {
                fileDict = [NSMutableDictionary dictionary];
            }
        }
    }
    else
    {
        //nothing
    }
    
    if (fileDict){
        cacheDict[path] = fileDict;
    }
    
    return fileDict;
}

-(void) saveFile:(NSString *)path withDict:(NSDictionary *)dict
{
    NSDictionary *writeDict = [dict mutableCopy];
    dispatch_async(writeQueue, ^{
        BOOL sucess = [writeDict writeToFile:path atomically:YES];
        if (!sucess) {
            NSLogToFile(@"FSFileOperation saveObject %@ fail",writeDict);
        }
    });
}

@end

关于清空缓存策略

缓存的存储空间有限制,当缓存空间被用满时,如何保证有效提升命中率?这就由缓存清空策略来处理,设计适合自身数据特征的清空策略能有效提升命中率。常见的一般策略有:

• FIFO(first in first out)

先进先出策略,最先进入缓存的数据在缓存空间不够的情况下(超出最大元素限制)会被优先被清除掉,以腾出新的空间接受新的数据。策略算法主要比较缓存元素的创建时间。在数据实效性要求场景下可选择该类策略,优先保障最新数据可用。

• LFU(less frequently used)

最少使用策略,无论是否过期,根据元素的被使用次数判断,清除使用次数较少的元素释放空间。策略算法主要比较元素的hitCount(命中次数)。在保证高频数据有效性场景下,可选择这类策略。

• LRU(least recently used)

最近最少使用策略,无论是否过期,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素释放空间。策略算法主要比较元素最近一次被get使用时间。在热点数据场景下较适用,优先保证热点数据的有效性。
除此之外,还有一些简单策略比如:
• 根据过期时间判断,清理过期时间最长的元素;
• 根据过期时间判断,清理最近要过期的元素;
• 随机清理;
• 根据关键字(或元素内容)长短清理等。

这些策略不分好坏,主要看缓存的应用场景。

本文作者: ctinusdev
原文链接: https://ctinusdev.github.io/2017/07/29/FileCache/
转载请注明出处!

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

推荐阅读更多精彩内容

  • 史上最全的iOS面试题及答案 iOS面试小贴士———————————————回答好下面的足够了----------...
    Style_伟阅读 2,356评论 0 35
  • 多线程、特别是NSOperation 和 GCD 的内部原理。运行时机制的原理和运用场景。SDWebImage的原...
    LZM轮回阅读 2,008评论 0 12
  • iOS面试小贴士 ———————————————回答好下面的足够了------------------------...
    不言不爱阅读 1,984评论 0 7
  • __block和__weak修饰符的区别其实是挺明显的:1.__block不管是ARC还是MRC模式下都可以使用,...
    LZM轮回阅读 3,315评论 0 6
  • 简介 在iOS中,我们需要将非UI且耗时的任务放在主线程当中执行,同时确保在任务完成时进行回调。常用的三种实现多线...
    adduct阅读 382评论 0 1