前言:
最近面试时,问到了限定并发数的视频下载,当时回答的时通过GCD_barrier 处理,回来想想也可以通过NSCache处理,所以顺便复习一下,这个知识点。
一,关于NSCache说明
说明文档:
@interface NSCache : NSObject
|
|
Description |
A mutable collection you use to temporarily store transient key-value pairs that are subject to eviction when resources are low. Cache objects differ from other mutable collections in a few ways:
You typically use Objects that have subcomponents that can be discarded when not being used can adopt the NSDiscardableContent protocol to improve cache eviction behavior. By default, NSDiscardableContent objects in a cache are automatically removed if their content is discarded, although this automatic removal policy can be changed. If an NSDiscardableContentobject is put into the cache, the cache calls discardContentIfPossible on it upon its removal. |
---|---|
SDKs | iOS 4.0+, macOS 10.6+, tvOS 9.0+, watchOS 2.0+ |
Declared In | Foundation |
More | Class Reference |
大概翻译:
一个可变集合,用于临时存储在资源不足时可能被收回的临时键-值对。
缓存对象与其他可变集合的不同之处在于:
* NSCache类集成了各种自动回收策略,这些策略确保缓存不会占用太多系统内存。如果其他应用程序需要内存,这些策略将从缓存中删除一些项,从而最小化内存占用。
* 您可以从不同的线程在缓存中添加、删除和查询条目,而不必自己锁定缓存。
* 与NSMutableDictionary对象不同,缓存不会复制放入其中的关键对象。
您通常使用NSCache对象来临时存储具有临时数据的对象,这些数据创建起来非常昂贵。重用这些对象可以提供性能优势,因为它们的值不必重新计算。但是,这些对象对应用程序并不重要,如果内存紧张,可以丢弃它们。如果丢弃,则需要重新计算它们的值。
如果对象的子组件在不使用时可以被丢弃,则可以采用NSDiscardableContent协议来改进缓存回收行为。默认情况下,尽管可以更改此自动删除策略, 如果缓存中的NSDiscardableContent对象的内容被丢弃,则会自动删除它们。如果NSDiscardableContentobject被放入缓存,缓存将在它被移除时对其调用discardContentIfPossible。
重点信息:
NSCache
是一个类似NSDictionary
一个可变的集合。- 提供了可设置缓存的数目与内存大小限制的方式。
- 保证了处理的数据的线程安全性。在多线程操作中,不需要对Cache加锁
- 缓存使用的key不需要是实现
NSCopying
的类。 - 当内存警告时内部自动清理部分缓存数据。
二,NSCacheAPI
#import <Foundation/NSObject.h> @class NSString; @protocol NSCacheDelegate; NS_ASSUME_NONNULL_BEGIN NS_CLASS_AVAILABLE(10_6, 4_0) @interface NSCache <KeyType, ObjectType> : NSObject { @private //私有变量 id _delegate; void *_private[5]; void *_reserved; }
@property (copy) NSString *name; //缓存的名字 @property (nullable, assign) id<NSCacheDelegate> delegate; //delegate代理属性 - (nullable ObjectType)objectForKey:(KeyType)key; //返回与键值关联的对象 - (void)setObject:(ObjectType)obj forKey:(KeyType)key; // 0 cost,在缓存中设置指定键名对应的值,与可变字典不同,缓存对象不会对键名做 copy 操作0成本。 - (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g; //在缓存中设置指定键名对应的值,并且指定该键值对的成本,成本 (cost) 用于计算记录在缓冲中的所有对象的总成本,成本可以自行指定。 - (void)removeObjectForKey:(KeyType)key; //删除缓存中,指定键名的对象。 - (void)removeAllObjects; //删除缓存中所有对象。 @property NSUInteger totalCostLimit; //缓存空间的最大总成本,超出上限会自动回收对象 默认值是 0,表示没有限制。
@property NSUInteger countLimit; //能够缓存对象的最大数量,默认值是 0,表示没有限制。;
@end
@protocol NSCacheDelegate <NSObject>
@optional
- (void)cache:(NSCache *)cache willEvictObject:(id)obj; //缓存将要删除对象时调用,不能在此方法中修改缓存。
@end NS_ASSUME_NONNULL_END
三,举例验证
#import "ViewController.h" @interface ViewController () <NSCacheDelegate> @property (nonatomic, strong)NSCache *cache; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.cache = [[NSCache alloc]init]; /** 创建对象.*/ self.cache.delegate = self; /** 设置代理方法.*/ self.cache.countLimit = 5; /** 设置能够缓存对象的最大数.*/ [self save:@1 key:@"data1"]; [self save:@2 key:@"data2"]; [self save:@3 key:@"data3"]; [self save:@4 key:@"data4"]; [self save:@5 key:@"data5"]; [self save:@6 key:@"data6"]; } - (void)save:(id)objc key:(NSString *)key { [self.cache setObject:objc forKey:key]; NSLog(@"save:%@",objc); } /** * @brief 缓存将要删除对象时调用 * @param cache 缓存对象 * @param obj 删除的对象 */ - (void)cache:(NSCache *)cache willEvictObject:(id)obj { NSLog(@"evict:%@",obj); }
@end
打印结果:
2018-11-29 14:47:27.032630+0800 数组内容为空[6655:467226] save:1 2018-11-29 14:47:27.033117+0800 数组内容为空[6655:467226] save:2 2018-11-29 14:47:27.033661+0800 数组内容为空[6655:467226] save:3 2018-11-29 14:47:27.034004+0800 数组内容为空[6655:467226] save:4 2018-11-29 14:47:27.034173+0800 数组内容为空[6655:467226] save:5 2018-11-29 14:47:27.034555+0800 数组内容为空[6655:467226] evict:1 2018-11-29 14:47:27.035314+0800 数组内容为空[6655:467226] save:6
示例1.缓存池的使用
CYWImageCache.h
#import <Foundation/Foundation.h> @interface CYWImageCache : NSObject @property (nonatomic, copy)NSString *name; /** 图片名称.*/ @property (nonatomic, copy)NSString *icon; /** 图标.*/ @property (nonatomic, copy)NSString *download; /** 下载地址.*/ + (instancetype) appWithDict:(NSDictionary *)dict; @end
CYWImageCache.m
#import "CYWImageCache.h" @implementation CYWImageCache + (instancetype)appWithDict:(NSDictionary *)dict { CYWImageCache *image = [[CYWImageCache alloc]init]; [image setValuesForKeysWithDictionary:dict]; return image; } @end
ViewController.h
#import <UIKit/UIKit.h> @interface ViewController : UIViewController @end
ViewController.m
#import "ViewController.h" #import "CYWImageCache.h" @interface ViewController () <UITableViewDelegate,UITableViewDataSource> /** plist文件数据容器.*/ @property (nonatomic, strong) NSArray *imageList; /** 管理下载的全局队列.*/ @property (nonatomic, strong) NSOperationQueue *opqueue; /** 所有下载的缓存池.*/ @property (nonatomic, strong) NSMutableDictionary *operationCache; /** 保存所有图像的缓存.*/ @property (nonatomic, strong) NSCache *imageCache; @property (nonatomic, strong) UITableView *tableView; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; } #pragma mark -- Layout Methods - (NSArray *)imageList { if (!_imageList) { NSArray *dictArray = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle]pathForResource:@"image" ofType:@".plist" ]]; NSMutableArray *arrayM = @[].mutableCopy; for (NSDictionary *dict in dictArray) { CYWImageCache *image = [[CYWImageCache alloc]init]; [image setValuesForKeysWithDictionary:dict]; [arrayM addObject:image]; } _imageList = arrayM; } return _imageList; } - (NSOperationQueue *)opqueue { if (!_opqueue) { _opqueue = [[NSOperationQueue alloc]init]; } return _opqueue; } - (NSMutableDictionary *)operationCache { if (!_operationCache) { _operationCache = [[NSMutableDictionary alloc]init]; } return _operationCache; } - (NSCache *)imageCache { if (!_imageCache) { _imageCache = [[NSCache alloc]init]; } return _imageCache; } - (UITableView *)tableView { if (!_tableView) { _tableView = [[UITableView alloc]initWithFrame:self.view.bounds style:UITableViewStylePlain]; _tableView.delegate = self; _tableView.dataSource = self; [_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:NSStringFromClass(UITableViewCell.class)]; } return _tableView; } #pragma mark --Delegate - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.imageList.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass(UITableViewCell.class)]; CYWImageCache *image = [[CYWImageCache alloc]init]; cell.textLabel.text = image.name; cell.detailTextLabel.text = image.description; if ([self.imageCache objectForKey:image.name]) { NSLog(@"图片已经下载。。。。。。"); cell.imageView.image = [self.imageCache objectForKey:image.icon]; } else { //内存无图片 //如果沙盒里有图片,直接从沙盒里加载 UIImage *images = [UIImage imageWithContentsOfFile:[self cachePathWithUrl:image.name]]; if (images) { //沙盒有图片 NSLog(@"直接从沙盒加载图片"); //1.设置图片缓存到内存, 方便下次直接从内存加载 [self.imageCache setObject:images forKey:image.name]; //2.显示图片 cell.imageView.image = [self.imageCache objectForKey:image.icon]; } else { //显示占位图 cell.imageView.image = [UIImage imageNamed:@"user_default"]; //下载图片 [self downloadImage:indexPath]; } } return cell; } -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { // 点击后查看操作缓冲池内的操作详情 NSLog(@"%@", self.operationCache); } #pragma mark --Private Methods - (NSString *)cachePathWithUrl:(NSString *)urlStr { //将图片网址名称作为最后一项 //1.获取缓存数据 NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; //2.把路径根urlStr拼接起来 return [cachePath stringByAppendingString:urlStr.lastPathComponent]; } - (void)downloadImage:(NSIndexPath *)indexpath { CYWImageCache *imageCache = self.imageList[indexpath.row]; /** 如果下载缓冲池里面有当前图片的下载操作,就不用创建下载操作,没有才创建 缓冲池字典中 key:存放当前图片的url,字符串类型。 Value:保存下载操作 */ if (self.operationCache[imageCache.icon]) { NSLog(@"正在玩命下载中......"); return; } // 缓冲池没有下载操作 // 异步下载图片 __weak typeof(self) weakSelf = self; NSBlockOperation *downLoadOp = [NSBlockOperation blockOperationWithBlock:^{ // 模拟延时 [NSThread sleepForTimeInterval:2]; NSLog(@"正在下载中......"); // 1. 下载图片(二进制数据) NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageCache.icon]]; UIImage *image = [UIImage imageWithData:data]; // 2. 将下载的数据保存到沙盒 // 字典的赋值不能为nil,赋值为nil会崩溃 if (image) { // 先保存到图片缓存 [weakSelf.imageCache setObject:image forKey:imageCache.icon]; // 将图片写入到沙盒 [data writeToFile:[self cachePathWithUrl:imageCache.icon] atomically:YES]; } // 3 将操作从缓冲池删除——将下载好的图片操作从缓冲池中移除 [weakSelf.operationCache removeObjectForKey:imageCache.icon]; // 4. 在主线程更新UI [[NSOperationQueue mainQueue] addOperationWithBlock:^{ [weakSelf.tableView reloadRowsAtIndexPaths:@[indexpath] withRowAnimation:UITableViewRowAnimationNone]; /** reload 会重新调用cell的初始化方法, 会重新判断模型里面是否有图像 有的话就直接显示 */ }]; }]; // 将操作添加到队列 [weakSelf.opqueue addOperation:downLoadOp]; NSLog(@"操作的数量------------->%ld", self.opqueue.operationCount); // 将操作添加到缓冲池中(使用图片的url作为key) [weakSelf.operationCache setObject:downLoadOp forKey:imageCache.icon]; } /** 在真实开发中,一定要注意这个方法 */ -(void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // 需要在这里做一些内存清理的工作,如果不处理会被系统强制闪退 // 清理内存 [self.imageCache removeAllObjects]; // 清理操作的缓存 [self.operationCache removeAllObjects]; // 取消下载队列内的任务 [self.opqueue cancelAllOperations]; } @end
SDWebimage.h 缓存池使用--图片缓存
// 继承NSCache,实现自定义的cache类 @interface AutoPurgeCache : NSCache
@end @implementation AutoPurgeCache - (id)init{ self = [super init]; if (self) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; } return self; } - (void)dealloc{ [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; } @end
AutoPurgeCache的使用 初始化 // Init the memory cache _memCache = [[AutoPurgeCache alloc] init]; _memCache.name = fullNamespace; 缓存图片与取缓存图片 - (UIImage *)imageFromMemoryCacheForKey:(NSString *)key { return [self.memCache objectForKey:key]; } - (UIImage *)imageFromDiskCacheForKey:(NSString *)key { // First check the in-memory cache... UIImage *image = [self imageFromMemoryCacheForKey:key]; if (image) { return image; } // Second check the disk cache... UIImage *diskImage = [self diskImageForKey:key]; if (diskImage && self.shouldCacheImagesInMemory) { // 计算需要缓存的内存空间 NSUInteger cost = SDCacheCostForImage(diskImage); [self.memCache setObject:diskImage forKey:key cost:cost]; } return diskImage; }
四,NSCache总结
* 为什么使用NSCache?
NSCache的好处在于当系统资源耗尽时,它可以自动删减缓存;如果采用NSDictionary,那么就要自己编写程序在收到系统发出的“低内存”通知时手动删减缓存。那么都能实现要求为什么还要使用NSCache呢?
(1) 首先NSCache是资源耗尽时自动删减缓存,比起NSDictionary来说简单;
(2)它是Foundation框架的一部分,所以它能在更深层次上处理效率会更好;
(3)NSCache会先行删减“最久未使用”的对象,若自己为字典添加此功能会很复杂。
(4) NSCache是线程安全的,再不编写锁代码时,多个线程可以同时访问NSCache
(5)NSCache不会拷贝键 ,而是强引用键,因为键大部分是由不支持拷贝的对象来充当。
* NSCache在系统资源耗尽时会自动删减内存,那我可以控制什么时候删减内存吗?
我们可以使用NSCache的两个与系统资源相关的尺寸来操控缓存删减起内容的时机。
(1)缓存可就收的对象总数即countLimit属性,
(2)所有对象的“总开销”即totalCostLimit属性,我们在将对象加入缓存时,可以指定“”开销值“。当对象或总开销超过上限时,缓存可能会删减其中的对象,也可能在系统资源紧缺时删减
注意:可能删减,意味着不一定会删除,所以想通过调整开销值迫使缓存删减对象的情况下,不应使用NSCache;
* 我们在任何时候都使用NSCache就好了?
使用任何方案都要了解它的利弊,对于“开销值”这个控制删减内容的尺寸的使用有一些注意事项,在向内存添加对象时,应该很容易计算出它的“开销值”(也就是大小),才应该是用这个尺度,例如加入缓存的事NSData对象,可以将它的大小当作“开销值”,因为不必计算,读取NSData的大小就可以了。例如必须访问磁盘才能确定文件的大小,或是必须访问数据库才能决定具体取值就不应使用“开销值”。
stackoverflow上的说明:如果可以在运行时重新创建的值(通过从Internet下载,通过计算,无论如何),NSCache可以满足您的需要。 如果无法重新创建数据(例如用户输入,时间敏感等),则不应将其存储在NSCache中,因为它将在那里被销毁。
* 怎样使用它?
(1)基本了解和使用
我们先介绍一个与它配合使用的NSPurgeableData,NSPurgeableData是NSMutableData类的子类,实现了NSDiscardableContent协议。当系统资源紧张时,可以把保存为NSPurgeableData对象的内存释放掉。
需要访问NSPurgeable对象,可调用beginContentAccess方法,表示不应丢弃自己占的内存。用完之后,调用endContentAccess方法,表示在必要时可以丢弃自己占的内存。记住只有对象引用计数为0时才可以丢弃。
因为缓存中NSPurgeableData对象在被系统丢弃时,会自动从缓存中移除,NSCache的evictsObjectsWithDiscardedContent属性用于标志是否开启此功能。
* 我想知道是否最好使用一个NSCache的全局实例,或者是每个需要它的组件的几个实例。
例如,我有几个视图子类将内容绘制到图像中并缓存它们,以避免一直重新生成它们。 每个类都有一个NSCache实例,还是整个应用程序只有一个集中的NSCache?
请注意,我不是每个实例的一个缓存一个缓存。 我在说每个类的NSCache的一个实例。
回答1:我通常会为每种类型的缓存对象投一个缓存。 这样,您可以为每个例如不同的countLimit值。 要指定“保持最近呈现的50个缩略图”,“保留最近下载的5个最大的图像”,“保留最近下载的10个PDF”等。
对于真正计算上昂贵的任务,我还采用两层缓存,NSCache来实现最佳性能,并将其保存到本地文件系统中的临时/缓存目录中,以避免昂贵的下载周期,同时不消耗RAM。
回答2:如果缓存的内容在组件之间共享,那么可以使用统一的缓存 - 查找不会显着增加,至少可以减少缓存对象的冗余副本。
但是,如果缓存的内容对于每个组件是唯一的,则将它们混合在高速缓存中是无意义的,并且从代码可读性的角度来看可能会令人困惑。 在这种情况下保持缓存分开。 这也让您更精确地控制它们 - 例如 您可以更积极地从不被立即使用的组件中缓存。
总结:如果缓存的内容在组件间共享可以创建一个统一的缓存,如果不是组件间共享建议为每种类型的内容单独创建缓存,这样我们可以细粒度的控制他们,分别设置他们的countLimit等;而且我们可以设计两级缓存