@WilliamAlex大叔
前言
SDWebImage框架是我们最常用的框架,我们下载图片,清除缓存等都可以使用该框架.下面我们模仿SDWebImage来实现多图片的下载
在写代码之前,我们先整理整理思路,下载图片我们分为两种情况,有缓存和没缓存,下面我们来看看我做的两幅图
-
无沙盒缓存
有沙盒缓存
- 纠正一点: 图片中的下载操作是是保存到一个可变的字典中的,不是数组,画图的时候一整天都没有吃饭,肚子太饿了,出了错,还望大家见谅
代码实现
在正式写代码之前,需要说明几步操作
- 定义模型类
- 将storyboard中的控制器移除,拖入一个新的UITableViewController控制器,绑定ID:"apps"以及绑定控制器,将类型设置为subtitle,最后给控制器设置启动箭头.
- 将ViewController的父类换成UITableViewController
-
代码存在很大问题(需要优化)
- 问题 1,滑动界面时有严重的卡顿现象,原因是下载操作是在主线程上执行的.
#import "ViewController.h"
#import "WGApps.h"
@interface ViewController ()
/** 图片缓存(plist文件中是字典存储的) */
@property (nonatomic, weak) NSMutableDictionary *images;
/** apps数据源*/
@property(nonatomic, strong) NSArray *apps;
@end
@implementation ViewController
#pragma mark - 生命周期方法
- (void)viewDidLoad
{
[super viewDidLoad];
self.tableView.rowHeight = 44;
}
#pragma mark ----------------
#pragma mark - lazyLoading
- (NSMutableDictionary *)images {
if (_images == nil) {
_images = [NSMutableDictionary dictionary];
}
return _images;
}
- (NSArray *)apps {
if (_apps == nil) {
// 加载plist文件
NSString *path = [[NSBundle mainBundle] pathForResource:@"apps.plist" ofType:nil];
// 加载数据
NSArray *dictArray = [NSArray arrayWithContentsOfFile:path];
NSMutableArray *tempArray = [NSMutableArray arrayWithCapacity:dictArray.count];
// 字典数组转模型数组
for (NSDictionary *dict in dictArray) {
[tempArray addObject:[WGApps appsWithDict:dict]];
}
_apps = tempArray;
}
return _apps;
}
#pragma mark - 数据源方法
// 一共有多少个cell
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.apps.count;
}
// 每一个cell显示什么内容
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// 定义ID(最好和storyboard中定义的ID一致)
static NSString *ID = @"apps";
// 创建cell
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
// 通过模型拿到对应的资源
WGApps *app = self.apps[indexPath.row];
// 设置数据
cell.textLabel.text = app.name;
cell.detailTextLabel.text = app.download;
// 下载头像
// 1, 首先去内存缓存中取,如果没有再去沙盒中去
UIImage *image = [self.images objectForKey:app.icon];
if (image) {
// 来到这里,说明图片缓存中已经有需要的图片,直接显示到对应的cell即可
cell.imageView.image = image;
} else
{
// 来到这里表示:图片缓存中没有所需要的图片,那么这时候就要到对应的沙盒中找有没有下载的图片
// 1, 获取沙盒路径
NSString *cachesPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
// 注意:拿到沙盒路径还不够,我们需要的是下载图片的路径,所以需要拼接,拿到全路径
// 2, 获取模型中图片资源最后一个目录名
NSString *fileName = [app.icon lastPathComponent];
// 3, 拼接路径
NSString *fullPath = [cachesPath stringByAppendingPathComponent:fileName];
// 注意 : 在沙盒中的保存的资源是以二进制的形式存在的.我们还需要判断沙盒中是否有下载过的图片
// 4, 通过图片路径,拿到下载的图片
NSData *imageData = [NSData dataWithContentsOfFile:fullPath];
// 5, 判断沙盒中是否有值
if (imageData) {
// 来到这里说明沙盒中有下载好的图片,直接将二进制转为图片显示即可,但是最后还需要讲图片报讯到图片缓存中,方便下次直接获取.
UIImage *image = [UIImage imageWithData:imageData];
// 显示图片
cell.imageView.image = image;
// 将图片保存到图片缓存中
[self.images setObject:image forKey:app.icon];
} else
{
// 来到这里说明沙盒中没有值,这时候我们就需要下载图片资源啦.
// 下载图片
NSURL *url = [NSURL URLWithString:app.icon];
// 将url转为data保存到本地
NSData *data = [NSData dataWithContentsOfURL:url];
// 再将二进制转为图片显示到cell上
UIImage *image = [UIImage imageWithData:data];
// 显示图片
cell.imageView.image = image;
// 将下载的图片保存到图片缓存和沙盒中
[self.images setObject:image forKey:app.icon];
[data writeToFile:fullPath atomically:YES];
}
}
return cell;
}
多图片下载(优化后的代码)
- 解决的问题 : 解决了卡顿现象,避免了重复下载
#import "ViewController.h"
#import "WGApps.h"
@interface ViewController ()
/** 图片缓存(plist文件中是字典存储的) */
@property (nonatomic, weak) NSMutableDictionary *images;
/** apps数据源*/
@property(nonatomic, strong) NSArray *apps;
/** 下载操作 */
@property(nonatomic, strong) NSMutableDictionary *operations;
/** 队列 */
@property(nonatomic, strong) NSOperationQueue *queue;
@end
@implementation ViewController
#pragma mark - 生命周期方法
- (void)viewDidLoad
{
[super viewDidLoad];
self.tableView.rowHeight = 44;
}
#pragma mark ----------------
#pragma mark - lazyLoading
- (NSMutableDictionary *)operations
{
if (_operations == nil) {
_operations = [NSMutableDictionary dictionary];
}
return _operations;
}
- (NSOperationQueue *)queue {
if (_queue == nil) {
// 创建队列
_queue = [[NSOperationQueue alloc] init];
// 设置最大并发数
_queue.maxConcurrentOperationCount = 3;
}
return _queue;
}
- (NSMutableDictionary *)images {
if (_images == nil) {
_images = [NSMutableDictionary dictionary];
}
return _images;
}
- (NSArray *)apps {
if (_apps == nil) {
// 加载plist文件
NSString *path = [[NSBundle mainBundle] pathForResource:@"apps.plist" ofType:nil];
// 加载数据
NSArray *dictArray = [NSArray arrayWithContentsOfFile:path];
NSMutableArray *tempArray = [NSMutableArray arrayWithCapacity:dictArray.count];
// 字典数组转模型数组
for (NSDictionary *dict in dictArray) {
[tempArray addObject:[WGApps appsWithDict:dict]];
}
_apps = tempArray;
}
return _apps;
}
#pragma mark - 数据源方法
// 一共有多少个cell
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.apps.count;
}
// 每一个cell显示什么内容
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// 定义ID(最好和storyboard中定义的ID一致)
static NSString *ID = @"apps";
// 创建cell
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
// 通过模型拿到对应的资源
WGApps *app = self.apps[indexPath.row];
// 设置数据
cell.textLabel.text = app.name;
cell.detailTextLabel.text = app.download;
// 下载头像
// 1, 首先去内存缓存中取,如果没有再去沙盒中去
UIImage *image = [self.images objectForKey:app.icon];
if (image) {
// 来到这里,说明图片缓存中已经有需要的图片,直接显示到对应的cell即可
cell.imageView.image = image;
} else
{
// 来到这里表示:图片缓存中没有所需要的图片,那么这时候就要到对应的沙盒中找有没有下载的图片
// 1, 获取沙盒路径
NSString *cachesPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
// 注意:拿到沙盒路径还不够,我们需要的是下载图片的路径,所以需要拼接,拿到全路径
// 2, 获取模型中图片资源最后一个目录名
NSString *fileName = [app.icon lastPathComponent];
// 3, 拼接路径
NSString *fullPath = [cachesPath stringByAppendingPathComponent:fileName];
// 注意 : 在沙盒中的保存的资源是以二进制的形式存在的.我们还需要判断沙盒中是否有下载过的图片
// 4, 通过图片路径,拿到下载的图片
NSData *imageData = [NSData dataWithContentsOfFile:fullPath];
// 5, 判断沙盒中是否有值
if (imageData) {
// 来到这里说明沙盒中有下载好的图片,直接将二进制转为图片显示即可,但是最后还需要讲图片报讯到图片缓存中,方便下次直接获取.
UIImage *image = [UIImage imageWithData:imageData];
// 显示图片
cell.imageView.image = image;
// 将图片保存到图片缓存中
[self.images setObject:image forKey:app.icon];
} else
{
//设置展占位图片
cell.imageView.image = [UIImage imageNamed:@"占位图片"];
//查看该图片的下载操作是否存在
NSBlockOperation *download = [self.operations objectForKey:app.icon];
if (download == nil) {
download = [NSBlockOperation blockOperationWithBlock:^{
// 下载图片
NSURL *url = [NSURL URLWithString:app.icon];
// 阻塞1秒
[NSThread sleepForTimeInterval:1.0];
// 将图片转为二进制保存到本地沙盒中
NSData *data = [NSData dataWithContentsOfURL:url];
// 将二进制转为图片显示
UIImage *image = [UIImage imageWithData:data];
// 如果没有图片,一定要讲下载操作从字典中移除
if (image == nil) {
[self.operations removeObjectForKey:app.icon];
return ;
}
//保存图片到内存缓存
[self.images setObject:image forKey:app.icon];
//保存图片到沙河缓存
[data writeToFile:fullPath atomically:YES];
//线程间通信
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
//刷新cell
[tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}];
[self.operations removeObjectForKey:app.icon];
}];
//加入到操作缓存
[self.operations setObject:download forKey:app.icon];
//把操作添加到队列
[self.queue addOperation:download];
}
}
}
return cell;
}
@end
我们使用框架来实现同样功能
- 以上就是模仿SDWebImage内部实现多图片下载的原理,接下来我们使用SDWebImage来实现多图片下载
#import "ViewController.h"
#import "WGApps.h"
#import "UIImageView+WebCache.h"
@interface ViewController ()
/** apps数据源*/
@property(nonatomic, strong) NSArray *apps;
@end
@implementation ViewController
#pragma mark - 生命周期方法
- (void)viewDidLoad
{
[super viewDidLoad];
self.tableView.rowHeight = 84;
}
#pragma mark ----------------
#pragma mark - lazyLoading
- (NSArray *)apps {
if (_apps == nil) {
// 加载plist文件
NSString *path = [[NSBundle mainBundle] pathForResource:@"apps.plist" ofType:nil];
// 加载数据
NSArray *dictArray = [NSArray arrayWithContentsOfFile:path];
NSMutableArray *tempArray = [NSMutableArray arrayWithCapacity:dictArray.count];
// 字典数组转模型数组
for (NSDictionary *dict in dictArray) {
[tempArray addObject:[WGApps appsWithDict:dict]];
}
_apps = tempArray;
}
return _apps;
}
#pragma mark - 数据源方法
// 一共有多少个cell
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.apps.count;
}
// 每一个cell显示什么内容
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// 定义ID(最好和storyboard中定义的ID一致)
static NSString *ID = @"apps";
// 创建cell
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
// 通过模型拿到对应的资源
WGApps *app = self.apps[indexPath.row];
// 设置数据
cell.textLabel.text = app.name;
cell.detailTextLabel.text = app.download;
// 下载图片
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:app.icon] placeholderImage:[UIImage imageNamed:@"placehoder"]];
return cell;
}
@end
总结
- 从上面的代码来看,使用第三方框架大大减少了我们的工作量,不过在使用第三方框架时,我们还是最好多了解一点框架的内部原理,这样即使出错了,我们很快就找到问题所在,就像上面优化代码时,我遇到了一个bug : 我在拼接绝对路径时,使用错了方法,本应该使用:stringByAppendingPathComponent.但是我的粗心使用了 : stringByAppendingString,导致出现了问题.经过这样去了解,这种bug我以后是不会再犯了,即使犯了,也会很快改正过来o