iOS开发多图下载程序浅析
iOS开发多图下载程序浅析。
效果图如下:
打印效果:
上图打印效果,展现了滚动tableView重复从网络中下载数据的现象,在后面会对上面打印做介绍.
涉及到的知识点:
01 字典转模型 02 存储数据到沙盒,从沙盒中加载数据 03 占位图片的设置(cell的刷新问题) 04 如何进行内存缓存(使用NSDictionary) 05 在程序开发过程中的一些容错处理 06 如何刷新tableView的指定行(解决数据错乱问题) 07 NSOperation以及线程间通信相关知识
看效果图,感觉很简单,创建一个UITableView,在cell上面设置数据. 以前在都是一些现成的数据,这次试用的数据(图片)是通过URL从网络中下载来的,因此会出现很多问题!
比如:
1. UI很不流畅 --------> 开子线程下载图片
2. 图片重复从网络中下载--------> 把下载过的图片保存起来
3. 图片不会自动刷新
4. 当网络延迟时,图片又会重复下载
5. 数据错乱现象.
首先不考虑上面出现的问题,先把上面的效果图做好.然后再根据上面问题逐一解决.
storyboard
ZYTableViewController文件
这个tableViewController和storyboard中的控制器是绑定好的.
// // ZYTableViewController.h // 00-掌握-多图下载综合案例-数据展示 // // Created by 朝阳 on 2017/11/22. // Copyright © 2017年 sunny. All rights reserved. // #import @interface ZYTableViewController : UITableViewController @end #import "ZYTableViewController.h" #import "ZYApps.h" @interface ZYTableViewController () @property (nonatomic, strong) NSArray *apps; @end @implementation ZYTableViewController #pragma -mark lazy loading - (NSArray *)apps { if (!_apps) { // 加载plist文件 NSArray *arrayM = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"apps.plist" ofType:nil]]; // 创建一个临时可变数组 NSMutableArray *tempArray = [NSMutableArray array]; for (NSDictionary *dict in arrayM) { [tempArray addObject:[ZYApps appWithDict:dict]]; } _apps = tempArray; } return _apps; } - (void)viewDidLoad { [super viewDidLoad]; } #pragma mark - Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.apps.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *ID = @"app"; // 创建cell UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID forIndexPath:indexPath]; // 设置数据给cell ZYApps *app = self.apps[indexPath.row]; cell.textLabel.text = app.name; cell.detailTextLabel.text = app.download; NSURL *url = [NSURL URLWithString:app.icon]; NSData *iconData = [NSData dataWithContentsOfURL:url]; UIImage *image = [UIImage imageWithData:iconData]; cell.imageView.image = image; // NSLog(@"%ld----",indexPath.row); /* 存在两个严重问题: 1. UI很不流畅 ----> 开子线程下载图片 2. 图片重复下载 ----> 先把之前已经下载的图片保存起来 */ // 返回cell return cell; } @end
模型数据ZYApps文件
// // ZYApps.h // 00-掌握-多图下载综合案例-数据展示 // // Created by 朝阳 on 2017/11/22. // Copyright © 2017年 sunny. All rights reserved. // #import @interface ZYApps : NSObject /** app名称 */ @property(nonatomic, strong) NSString * name; /** app下载量 */ @property(nonatomic, strong) NSString * download; /** app图标 */ @property(nonatomic, strong) NSString * icon; + (instancetype)appWithDict:(NSDictionary *)dict; @end @implementation ZYApps + (instancetype)appWithDict:(NSDictionary *)dict { ZYApps *apps = [[ZYApps alloc] init]; // 利用KVC [apps setValuesForKeysWithDictionary:dict]; return apps; } @end
以上代码就可以实现效果图.但是存在两个严重的问题:
1. 图片被重复下载
2. UI很不流畅
问题1. 图片被重复下载
因为当滚动tableView的时候,会重复下载网络中的图片.----解决---> 先把下载好的图片保存起来
具体解决:
当应用程序第一次下载下来的时候,tableView中的图片,需要从网络中下载下来.然后把图片保存到内存缓存一份,把图片也写入到沙盒中一份.
当来回滚动tableView的时候,下载过的图片已经在内存中缓存过了,因此获取内存中的图片就可以了.由此防止了重复下载图片的现象.把图片的二进制写入到沙盒中,原因是
因为当应用程序重新启动的时候,在应用程序内存中缓存的图片都清空了,因此还需要重新从网络上下载图片,保存到沙盒中就是为了当重新启动应用程序的时候,数据可以从沙盒中读取,防止重复下载.
此时 tableView:cellForRowAtIndexPath:方法中代码.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *ID = @"app"; // 创建cell UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID forIndexPath:indexPath]; // 设置数据给cell ZYApps *app = self.apps[indexPath.row]; cell.textLabel.text = app.name; cell.detailTextLabel.text = app.download; // 设置图标 // 查看内存缓存中该图片是否存在,若存在直接用,否则去磁盘缓存中查看是否有缓存\ 如果有磁盘缓存,就保存一份到内存.设置图片,否则下载 // 从内存缓存中读取 UIImage *image = [self.images objectForKey:app.icon]; // 是否内存中存在已下载的图片 if (image) { cell.imageView.image = image; NSLog(@"使用内存缓存中的图片---%ld",indexPath.row); }else{ // 保存图片到沙盒缓存 /* arg1: 沙盒的哪个目录 arg2: 去主目录下去搜索,默认就是NSUserDomainMask arg3: 是否展开路径 */ NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; // 获得图片名称,不能包含/ NSString *fileName = [app.icon lastPathComponent]; // 拼接图片的全路径 NSString *fullPath = [caches stringByAppendingPathComponent:fileName]; // 检查磁盘缓存 NSData *imageData = [NSData dataWithContentsOfFile:fullPath]; if (imageData) { UIImage *image = [UIImage imageWithData:imageData]; // 设置图标 cell.imageView.image = image; NSLog(@"%ld--使用了磁盘缓存的图片--",indexPath.row); // 把图片保存到内存中一份 [self.images setObject:image forKey:app.icon]; // NSLog(@"%@",fullPath); }else{ NSURL *url = [NSURL URLWithString:app.icon]; NSData *iconData = [NSData dataWithContentsOfURL:url]; UIImage *image = [UIImage imageWithData:iconData]; cell.imageView.image = image; // 把图片保存到内存缓存 [self.images setObject:image forKey:app.icon]; // 写数据到沙盒 [iconData writeToFile:fullPath atomically:YES]; // NSLog(@"%@",fullPath); NSLog(@"%ld--下载--",indexPath.row); } } /* 存在两个严重问题: 1. UI很不流畅 ----> 开子线程下载图片 2. 图片重复下载 ----> 先把之前已经下载的图片保存起来 */ // 返回cell return cell; }
这样就解决了重复下载图片.
解决上面的5个问题 和 ZYTableViewController文件
问题:
1. UI很不流畅 --->开子线程下载图片
2. 图片重复下载 ---> 先把之前已经下载的图片保存起来(字典)
内存缓存 ---> 磁盘缓存
3. 图片不会自动刷新:
原因: 因为cell是subTitle类型的,subTitle类型中的image默认frame是(0,0,0,0)的,当显示cell的时候,image的frame还是(0,0,0,0),此时有图片已经下载完了.因为是开子线程下载图片的,程序是异步的,因此先return cell,此时的cell的Image的frame为0,图片设置上去也是不显示的.
解决: 手动刷新每一行cell. reloadRowsAtIndexPaths:withRowAnimation:,这个方法会调用cellForRow方法,因此会重新创建cell,cell的Image此时已经在内存缓存了.
4.(当网络延迟时)图片重复下载:
因为当下载一个cell的图片时候需要2s,当这个cell下载到1s的时候,用户滚动速度较快,此时整个cell被存放到缓存池中了(此时cell的图片还没有下载完),当下一个cell显示的时候,会从缓存池中取,此时缓存池中没有下载好图片的cell. 因此会出现重复下载现象
5. 数据错乱
原因: cell的复用问题造成的,当从缓存池中复用cell的同时,把复用的cell的图片也复用过来了.因此出现数据紊乱现象
解决: 当cell需要下载新的图片之前,清空cell原来的图片(设置占位图片)
#import "ZYTableViewController.h" #import "ZYApps.h" @interface ZYTableViewController () /** 模型数组 */ @property (nonatomic, strong) NSArray *apps; /** 存放下载过的图片 */ @property (nonatomic,strong) NSMutableDictionary *images; /** 队列 */ @property (nonatomic,strong) NSOperationQueue *queue; /** 操作缓存 */ @property (nonatomic,strong) NSMutableDictionary *operations; @end @implementation ZYTableViewController #pragma -mark lazy loading - (NSArray *)apps { if (!_apps) { // 加载plist文件 NSArray *arrayM = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"apps.plist" ofType:nil]]; // 创建一个临时可变数组 NSMutableArray *tempArray = [NSMutableArray array]; for (NSDictionary *dict in arrayM) { [tempArray addObject:[ZYApps appWithDict:dict]]; } _apps = tempArray; } return _apps; } - (NSMutableDictionary *)images { if (!_images) { _images = [NSMutableDictionary dictionary]; } return _images; } - (NSOperationQueue *)queue { if(!_queue){ _queue = [[NSOperationQueue alloc] init]; // 设置最大并发数:并行执行的任务数 _queue.maxConcurrentOperationCount = 5; } return _queue; } - (NSMutableDictionary *)operations { if (!_operations) { _operations = [NSMutableDictionary dictionary]; } return _operations; } - (void)viewDidLoad { [super viewDidLoad]; } - (void)didReceiveMemoryWarning { // 当发生内存警告的时候 [self.images removeAllObjects]; // 取消队列中所有的操作 [self.queue cancelAllOperations]; } #pragma mark - Table view data source - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.apps.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *ID = @"app"; // 创建cell UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID forIndexPath:indexPath]; // 设置数据给cell ZYApps *app = self.apps[indexPath.row]; cell.textLabel.text = app.name; cell.detailTextLabel.text = app.download; // 设置图标 // 查看内存缓存中该图片是否存在,若存在直接用,否则去磁盘缓存中查看是否有缓存\ 如果有磁盘缓存,就保存一份到内存.设置图片,否则下载 // 从内存缓存中读取 UIImage *image = [self.images objectForKey:app.icon]; // 是否内存中存在已下载的图片 if (image) { cell.imageView.image = image; NSLog(@"使用内存缓存中的图片---%ld",indexPath.row); }else{ // 保存图片到沙盒缓存 /* arg1: 沙盒的哪个目录 arg2: 去主目录下去搜索,默认就是NSUserDomainMask arg3: 是否展开路径 */ NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; // 获得图片名称,不能包含/ NSString *fileName = [app.icon lastPathComponent]; // 拼接图片的全路径 NSString *fullPath = [caches stringByAppendingPathComponent:fileName]; // 检查磁盘缓存 NSData *imageData = [NSData dataWithContentsOfFile:fullPath]; // 废除 // imageData = nil; if (imageData) { UIImage *image = [UIImage imageWithData:imageData]; // 设置图标 cell.imageView.image = image; NSLog(@"%ld--使用了磁盘缓存的图片--",indexPath.row); // 把图片保存到内存中一份 [self.images setObject:image forKey:app.icon]; // NSLog(@"%@",fullPath); }else{ // 创建队列(注意:在这里会创建很多个队列); // NSOperationQueue *queue = [[NSOperationQueue alloc] init]; //## 检查该图片是否正在下载,如果是那么就什么都不做,否则再添加下载任务 NSBlockOperation *downloadImage = [self.operations objectForKey:app.icon]; if (downloadImage) { // 什么都不做 }else{ // 清空cell之前的图片 // cell.imageView.image = nil; // 占位图片 // cell.imageView.image = [UIImage imageNamed:@"qq"]; // 创建操作 downloadImage = [NSBlockOperation blockOperationWithBlock:^{ NSURL *url = [NSURL URLWithString:app.icon]; NSData *iconData = [NSData dataWithContentsOfURL:url]; UIImage *image = [UIImage imageWithData:iconData]; //NSLog(@"%ld--下载--",indexPath.row); // 容错处理 if (image == nil) { [self.operations removeObjectForKey:app.icon]; return; } // 演示网络延迟 //[NSThread sleepForTimeInterval:2.0]; // 线程间通信 [[NSOperationQueue mainQueue] addOperationWithBlock:^{ // cell.imageView.image = image; // 刷新一行(会重新调用cellForRow方法) [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationRight]; }]; // 把图片保存到内存缓存 [self.images setObject:image forKey:app.icon]; // 写数据到沙盒 [iconData writeToFile:fullPath atomically:YES]; // NSLog(@"%@",fullPath); NSLog(@"%ld--下载--",indexPath.row); // 移除图片的下载操作 [self.operations removeObjectForKey:app.icon]; }]; // 添加操作到操作缓存中 [self.operations setObject:downloadImage forKey:app.icon]; // 把操作添加到队列中 [self.queue addOperation:downloadImage]; } } } // 返回cell return cell; } @end
沙盒
Documents: 会备份,不允许
tmp: 临时路径(随时会被删除)
Libray
Preferences: 偏好设置,保存账号密码
caches: 缓存文件
上一篇: 你跟相亲的姑娘来电么
下一篇: ios开发Runtime的简单使用方法