有沙盒缓存的思路:(重点)
原理图如下所示:
前提(准备工作):开始有一个存放图片的缓存池全局属性即一个可变字典images,和一个专门存放下载操作的操作缓存池operations,开始都为空。
启动程序,会初始化(新建)当前页的Cell或当滑动时也会初始化从Cell缓冲池拿出的Cell的数据,这时都会根据图片的url去images图片缓存池进行寻找图片,如果内存里即图片缓存池内不存在,就会从沙盒进行读取。判断读取结果如果沙盒文件存在则从沙盒加载对应的图片,并将从沙盒加载的图片保存到图片缓存池中,然后从图片缓存池中取出对应图片显示到cell上。如果沙盒文件不存在则先让Cell显示占位图,然后根据图片的url去查看存放操作的缓冲池即operations中是否有当前图片的下载操作,如果有的话,说明正在下载则直接返回,否则则创建当前图片的下载操作并添加到operations操作缓存池中,在下载操作中有一个下载完毕后的判断处理就是在图片下载完毕后将图片对象保存到图片缓存images,并将下载操作(就是当前的NSBlockOPeration对象中的块操作,也包含了其他的一些操作)从操作缓存池operations中移除,其次把下载后的图片按照url存进沙盒,最后刷新表格,重新为Cell初始化一次数据,把前面的操作又重新执行了一次。
之所以先从内存的图片缓存池中进行读取,内存中不存在时才把从沙盒读取数据先加载到图片缓存池(内存)中,然后从图片缓存池images中取出对应图片为cell的imageView赋值,主要是因为内存比沙盒(硬盘)的读取效率高。
新建工程:
CZApp.h
<span style="font-size:18px;">// // CZApp.h // NSoperation之网络图片下载 // // Created by apple on 15/10/23. // Copyright (c) 2015年 LiuXun. All rights reserved. // #import <UIKit/UIKit.h> @interface CZApp : NSObject @property(nonatomic, copy) NSString *name; @property (nonatomic, copy) NSString *icon; @property (nonatomic, strong) NSString *download; /** 保存网络下载的图像 */ // @property(nonatomic, strong) UIImage *image; +(instancetype) appWithDict:(NSDictionary *) dict; @end </span>CZApp.m
<span style="font-size:18px;">// // CZApp.m // NSoperation之网络图片下载 // // Created by apple on 15/10/23. // Copyright (c) 2015年 LiuXun. All rights reserved. // #import "CZApp.h" @implementation CZApp +(instancetype) appWithDict:(NSDictionary *) dict { CZApp *app = [[self alloc] init]; [app setValuesForKeysWithDictionary:dict]; return app; } @end</span>viewController.m
<span style="font-size:18px;">// // ViewController.m // NSoperation之网络图片下载 // // Created by apple on 15/10/23. // Copyright (c) 2015年 LiuXun. All rights reserved. // #import "ViewController.h" #import "CZApp.h" @interface ViewController () // plist文件数据的容器 @property (nonatomic, strong) NSArray *appList; // 管理下载的全局队列 @property (nonatomic, strong) NSOperationQueue *opQueue; /**所有下载的缓冲池*/ @property (nonatomic, strong) NSMutableDictionary *operationCache; /**保存所有图像的缓存*/ @property (nonatomic, strong) NSMutableDictionary *imageCache; @end @implementation ViewController // 懒加载 -(NSArray *)appList { if (_appList == nil) { NSArray *dicArray = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"apps.plist" ofType:nil]]; // 字典转模型 NSMutableArray *arryM = [NSMutableArray array]; for(NSDictionary *dict in dicArray){ CZApp *app = [CZApp appWithDict:dict]; [arryM addObject:app]; } _appList = arryM; } return _appList; } -(NSOperationQueue *)opQueue { if (_opQueue == nil) { _opQueue = [[NSOperationQueue alloc] init]; } return _opQueue; } -(NSMutableDictionary *)operationCache { if (_operationCache == nil) { _operationCache = [[NSMutableDictionary alloc] init]; } return _operationCache; } -(NSMutableDictionary *)imageCache { if (_imageCache == nil) { _imageCache = [[NSMutableDictionary alloc] init]; } return _imageCache; } #pragma mark - 实现数据源方法 -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.appList.count; } /** 问题1:如果网速比较慢,会很卡 解决方法:使用异步下载 问题2:图片没有Frame,所有cell初始化的时候,给imageView的frame是0。异步下载完之后不显示 解决办法:使用占位图(如果展位图比较大, 自定义cell可以解决) 问题3:如果图片下载速度不一致,同时用户快速滚动的时候,会因为Cell的重用导致图片混乱 解决方法:MVC,使用Model(模型)保存下载的图像,再次刷新表格。 问题4:在用户快速滚动的时候,会重复添加下载任务到下载队列。 解决方法:建立下载操作的缓冲池。首先检查缓冲池里是否有当前图片的下载操作。有的话就不创建下载操作。从而保证一张图片只添加一个下载操作。其实就是建立一个全局的字典属性。 问题5: 将图片保存到模型里的优缺点 优点:不用重复下载,利用MVC刷新表格,不会造成数据混乱,加载速度比较快 缺点:内存,所有下载好的图像都会记录在模型里。如果数据比较多(2000)个就会造成内存警告。 -***图像根模型耦合性太强。导致清理内存非常困难 解决办法:模型跟图像分开。在控制器里做缓存。 问题6:下载操作缓冲池会越来越大。需要及时清理。 */ /** 代码重构:1.如果代码太长。 目的: - 写的时候,如果思路清楚,能够一次性写完,但是也要注意同构。 - 时间长了,不好阅读 - 重构代码,便于维护 重构方法: 如果有一部分代码专门解决某一问题,就封装起来。 1. 新建一个方法—> 剪切代码。 2. 传参数。 3. 在原来剪切代码的地方,调用抽取的方法。 4. 注意,测试。 5. 注意if嵌套,在实际的开发,非常忌讳很深的嵌套。 */ -(void)viewDidLoad { NSLog(@"%@", [self cachePathWithUrl:@""]); } // cell里面的imageView子控件是懒加载 -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *ID = @"AppCell"; UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:ID]; // 给Cell设置数据 CZApp *app = self.appList[indexPath.row]; cell.textLabel.text = app.name; cell.detailTextLabel.text = app.download; // 判断模型里面是否有图像 if ([self.imageCache objectForKey:app.icon]) { // 内存有图片 NSLog(@" 图片已经下载......"); cell.imageView.image = self.imageCache[app.icon]; }else{ // 内存无图片 // 显示图片 // 如果沙盒里面有图片,直接从沙盒加载 UIImage *image = [UIImage imageWithContentsOfFile:[self cachePathWithUrl:app.icon]]; if (image) { // 沙盒有图片 NSLog(@"直接从沙盒加载图片"); // 1. 设置图片缓存到内存,方便下次从内存直接加载 [self.imageCache setObject:image forKey:app.icon]; // 2. 显示图片到cell cell.imageView.image = self.imageCache[app.icon]; }else{ // 沙盒没有图片 // 显示图片—占位图 cell.imageView.image = [UIImage imageNamed:@"user_default"]; #warning 从这里开始剪切的代码 // 下载图片 [self downloadImage:indexPath]; } } return cell; } -(void)downloadImage:(NSIndexPath *)indexPath { CZApp *app = self.appList[indexPath.row]; /** 如果下载缓冲池里面有当前图片的下载操作,就不用创建下载操作,没有才创建 缓冲池字典中 key:存放当前图片的url,字符串类型。 Value:保存下载操作 */ if (self.operationCache[app.icon]) { NSLog(@"正在玩命下载中......"); return; } // 缓冲池没有下载操作 // 异步下载图片 __weak typeof(self) weakSelf = self; NSBlockOperation *downLoadOp = [NSBlockOperation blockOperationWithBlock:^{ // 模拟延时 [NSThread sleepForTimeInterval:2]; NSLog(@"正在下载中......"); // 1. 下载图片(二进制数据) NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:app.icon]]; UIImage *image = [UIImage imageWithData:data]; // 2. 将下载的数据保存到沙盒 // 字典的赋值不能为nil,赋值为nil会崩溃 if (image) { // 先保存到图片缓存 [weakSelf.imageCache setObject:image forKey:app.icon]; // 将图片写入到沙盒 [data writeToFile:[self cachePathWithUrl:app.icon] atomically:YES]; } // 3 将操作从缓冲池删除——将下载好的图片操作从缓冲池中移除 [weakSelf.operationCache removeObjectForKey:app.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:app.icon]; } -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { // 点击后查看操作缓冲池内的操作详情 NSLog(@"%@", self.operationCache); } /** 在真实开发中,一定要注意这个方法 */ -(void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // 需要在这里做一些内存清理的工作,如果不处理会被系统强制闪退 // 清理内存 [self.imageCache removeAllObjects]; // 清理操作的缓存 [self.operationCache removeAllObjects]; // 取消下载队列内的任务 [self.opQueue cancelAllOperations]; } /** 拼接一个文件在沙盒的全路径 */ -(NSString *)cachePathWithUrl:(NSString *)urlStr { // 将图片网址名作为作为最后一项 // 1. 获得缓存的路径 NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; // 2. 把路径根urlStr拼接起来 return [cachePath stringByAppendingPathComponent:urlStr.lastPathComponent]; } -(void)dealloc { NSLog(@"销毁控制器-------------"); } @end</span>首次运行,没有缓存,也没有沙盒缓存,需要下载图片,结果如下所示:
再次运行,由于沙盒已经有了缓存,直接会被读取到图片缓存池中,然后被加载显示到Cell。不会再重新下载,如下所示: