iOS系统中缓存的使用

原文发布于:wenghengcong.com

NSCache

NSCache是系统提供的缓存类,用法类似于NSMutableDictionary,它与集合的不同如下:

  1. NSCache具有自动移除对象的功能,以减少系统占用的内存;

  2. NSCache是线程安全的;

  3. 键对象不会像 NSMutableDictionary 中那样被复制。(键不需要实现 NSCopying 协议)。

NSCache胜过NSMutableDictionary之处在于,当系统资源将要耗尽时,它可以自动删减缓存。如果采用普通字典对象,那么就要自己编写挂钩,在系统发出“低内存”通知时,手工删减缓存。而NSCache会自动删减“最久未使用的”(lease recently used)对象。

NSCache并不会“拷贝”键,而是“保留”它。

NSCache是线程安全的,而NSDictionary不是。对缓存来说,线程安全通常很重要,因为开发者要在某个线程中读数据,此时如果发现缓存中找不到指定的键,那么就要下载该键对应的数据了。而下载完数据之后所要执行的回调函数,有可能会放在背景线程中运行,这样的话,就等于是用另外一个线程来写入缓存了。

NSCache类

下面是该类的头文件:

    @interface NSCache  : NSObject {
    @private
    id delegate;
    void private[5];
    void reserved;
    }

    @property (copy) NSString name;

    @property (nullable, assign) id delegate;

    -(nullable ObjectType)objectForKey:(KeyType)key;
    -(void)setObject:(ObjectType)obj forKey:(KeyType)key; // 0 cost
    -(void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;
    -(void)removeObjectForKey:(KeyType)key;

    (void)removeAllObjects;

    @property NSUInteger totalCostLimit;    // limits are imprecise/not strict
    @property NSUInteger countLimit;    // limits are imprecise/not strict
    @property BOOL evictsObjectsWithDiscardedContent;

    @end

    @protocol NSCacheDelegate 
    @optional
    -(void)cache:(NSCache )cache willEvictObject:(id)obj;
    @end

需要说明的是,关于删减对象的时机,可以通过totalCostLimitcountLimit来控制,即“总开销”和“缓存对象总数”。当超过指定的上限后,那么久“可能”删减某对象。之所以,是“可能”,是因为缓存系统并不严格精确,在上限的数值上有小范围的浮动。

在设置缓存对象的下面方法中,还需要关注对象的“开销值”(cost):

-(void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;

那么,如何决定这个开销值呢?

只有在很快能计算出“开销值”的情况下,才应该考虑采用这个尺度。否则直接调用:

-(void)setObject:(ObjectType)obj forKey:(KeyType)key; // 0 cost

因为加入计算开销值的过程复杂,就违背了使用缓存的初衷——快速定位对象,比如,要从磁盘中读取文件大小,那么这个开销就过大,不应该计算器开销值。但是加入缓存对象是NSData对象,可以直接获取其数据大小,将其数据大小当做开销值也是可以的。

下面是个实例:

#import "JSNetworkFetcher.h"
@interface JSNetworkFetcher()
{
    struct {
        unsigned int didReceiveData                 :1;
        unsigned int didFailedWithError             :1;
        unsigned int didUpdateProgressTo            :1;
    } _delegateFlags;
    NSURL *_url;
    NSCache *_cache;
    
}

@end

@implementation JSNetworkFetcher

/**
 *  在设置代理的时候检查方法可达性,并缓存起来
 */
- (void)setDelegate:(id)delegate
{
    _delegate = delegate;
    _delegateFlags.didReceiveData = [delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)];
    _delegateFlags.didFailedWithError = [delegate respondsToSelector:@selector(networkFetcher:didFailerWithError:)];
    _delegateFlags.didUpdateProgressTo = [delegate respondsToSelector:@selector(networkFetcher:didUpdateProgerssTo:)];
}

- (instancetype)init {
    
    if (self == [super init]) {
        _cache = [NSCache new];
        
        _cache.countLimit = 100;    //限制缓存对象在100个
        _cache.totalCostLimit = 5*1024*1024;    //限制所有缓存对象大小不超过5MB
    }
    return self;
}

- (instancetype)initWithUrl:(NSURL*)url {
    
    self = [self init];
    _url = url;
    return self;
}

- (void)downloadDataFromUrl:(NSURL *)url {
    NSData *cacheData = [_cache objectForKey:url];
    if (cacheData) {
        //命中缓存
        [self userData:cacheData];
    } else {
        JSNetworkFetcher *fetcher = [[JSNetworkFetcher alloc]initWithUrl:url];
        [fetcher startWithCompletionHandle:^(NSData *data) {
            [_cache setObject:data forKey:url cost:data.length];
            [self userData:data];
        }];
    }
    
}

//使用数据
- (void)userData:(NSData *)data {
    
}

- (void)startWithCompletionHandle:( void (^)(NSData* data) )completion {
    
}

@end

NSCache类声明中还有一个evictsObjectsWithDiscardedContent的属性。该属性为了搭配NSPurgeableData对象而添加的。

NSPurgeableDataNSMutableData的子类,实现了NSDiscardableContent协议。该协议声明如下:

@protocol NSDiscardableContent
@required
- (BOOL)beginContentAccess;
- (void)endContentAccess;
- (void)discardContentIfPossible;
- (BOOL)isContentDiscarded;
@end

如果某个对象实现了该协议所定义的接口,该对象所占的内存就可以根据需要随时丢弃。而NSPurgeableData实现了该协议就意味着,当系统资源紧张时,可以把保存NSPurgeableData对象那块内存释放掉。

如果需要访问NSPurgeableData对象,调用其beginContentAccess方法,告诉它现在还不应该丢弃自己所占有的内存。用完之后,调用endContentAccess方法,告诉它在必要时可以丢弃自己所占据的内存。这就像retainrelease类似来管理,只有当NSPurgeableData中purge引用计数为0时才可以丢弃。purge引用计数,会在创建时加1,非常像真正的引用计数管理规则。

isContentDiscarded是查询对象内存是否被释放。

那么,NSPurgeableData对象假如加入到NSCache中,那么当该对象被系统丢弃时,也会自动从缓存中移除。而上面提到的NSCache中的evictsObjectsWithDiscardedContent属性,就是是否要开启这个功能的开关。

- (void)downloadDataFromUrl:(NSURL *)url {
    NSPurgeableData *cacheData = [_cache objectForKey:url];
    if (cacheData) {
        
        //开始使用
        [cacheData beginContentAccess];     //purge count +1
        
        //cacheData处理
        [self userData:cacheData];
        
        //使用完毕
        [cacheData endContentAccess];       //purge count -1
        
    } else {
        JSNetworkFetcher *fetcher = [[JSNetworkFetcher alloc]initWithUrl:url];
        [fetcher startWithCompletionHandle:^(NSData *data) {
            
            NSPurgeableData *purgeData = [NSPurgeableData dataWithData:data];       //purge count = 1
            
            [_cache setObject:purgeData forKey:url cost:purgeData.length];
            
            //cacheData处理,此处不需要beginContentAccess,因为创建会+1
            [self userData:data];
            
            //使用完毕
            [purgeData endContentAccess];
        }];
    }
    
}

经验总结

  • 实现自动缓存是应选用NSCache而非NSDictionary对象,因为NSCache可以提供优雅的自动删减功能,而且是“线程安全的”,此外,它与字典不同,并不会拷贝键;

  • 可以给NSCache设置上限,用以限制缓存中的对象总个数及“总成本”,而这些尺度定义了缓存删减其中对象的时机,但是绝对不要把这些尺度当初可靠的“硬限制”。它们仅仅对NSCache起指导作用;

  • 将NSPureableData与NSCache搭配使用,可实现自动清除数据的功能,也就是说,当NSPureableData对象所占内存为系统所丢弃是,该对象那自身也会从缓存中移除;

  • 如果缓存使用得当,那么应用程序的响应就能提升。只有那种“重新计算起来很费事”的数据,才值得放入缓存,比如那些需要从网络获取或从磁盘读取的数据;

实例

  • SDWebImage
  • AFNetWorking
    ...

参考

  1. NSCache
  2. NSCache from nshipster
  3. 第50条:构建缓存时选用NSCache而非NSDictionary

你可能感兴趣的:(iOS系统中缓存的使用)