iOS文件缓存

iOS文件缓存

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


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

1、读写文件效率问题

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

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

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

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

1
- (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进行控制

逻辑示意图入下:

看看dispatch_barrier_async函数原型:

1
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是相同的。

下面看些详细代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
@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/
转载请注明出处!

坚持原创技术分享,您的支持将鼓励我继续创作!