效果图如下:
打印效果:
上图打印效果,展现了滚动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: 缓存文件