梳理一下,在开发中利用SDWebImage下载图片 ,这个框架会帮我们做什么事情。
这里自己写代码来实现解决所有的问题。
项目准备:
-
1.首先创建数据源数组
@implementation ViewController { /// 数据源数组 NSArray *_appsList; } -(void)viewDidLoad { [super viewDidLoad]; [self loadJsonData]; }
-
2.利用第三方框架
AFNetworking
获取网络数据///定义获取JSON的主方法 -(void)loadJsonData{ //1、创建网络请求管理者 AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; //2、获取 [manager GET:@"https://raw.githubusercontent.com/lcy237777480/FYLoadImage/ master/apps.json" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, NSArray * responseObject) { //请求网络执行 回调(成功/失败)都是在主线程 NSLog(@"%@ %@ \n %@",[responseObject class],responseObject,[NSThread currentThread]); //responseObject就是获取到的json数据 //1、遍历数据数组字典转模型 //4、创建可变数组用来保存模型 NSMutableArray *modelsArr = [NSMutableArray arrayWithCapacity:responseObject.count]; [responseObject enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { //2、创建模型类 //3、赋值 FYAppModel *model = [FYAppModel appWithDict:obj]; [modelsArr addObject:model]; }]; _appsArrM = modelsArr.copy; //网络请求是耗时操作,拿到数据一定要reloadData [self.tableView reloadData]; } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { }]; }
-
3.实现tableView的数据源方法
#pragma mark - 数据源方法 -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ return _appsArrM.count; } -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"reuseCellID" forIndexPath:indexPath]; FYAppModel *model = _appsArrM[indexPath.row]; // 给cell的子控件赋值 cell.textLabel.text = model.name; cell.detailTextLabel.text = model.download; //利用SDWebImage框架 下载图片 [cell.imageView sd_setImageWithURL:[NSURL URLWithString:model.icon]]; return cell; }
项目准备完毕
接下来的实现不再用SDWebImage,自己实现NSBlockOperation异步下载图片,看看我们遇到了什么问题,也就是他帮助我们做了什么。
增加全局队列
@implementation ViewController {
/// 数据源数组
NSArray *_appsList;
/// 全局队列
NSOperationQueue *_queue;
}
实例化队列
-(void)viewDidLoad {
[super viewDidLoad];
// 实例化队列
_queue = [[NSOperationQueue alloc] init];
[self loadJsonData];
}
问题1 : 列表显示出来后,并不显示图片,来回滚动cell或者点击cell ,图片才会显示。
解决办法 : 自定义cell
修改数据源方法
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
APPCell *cell = [tableView dequeueReusableCellWithIdentifier:@"AppsCell" forIndexPath:indexPath];
// 获取cell对应的数据模型
AppsModel *app = _appsList[indexPath.row];
// 给cell传入模型对象
cell.app = app;
#**pragma mark - NSBlockOperation实现图片的异步下载**
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
// 模拟网络延迟
[NSThread sleepForTimeInterval:0.2];
// URL
NSURL *URL = [NSURL URLWithString:app.icon];
// data
NSData *data = [NSData dataWithContentsOfURL:URL];
// image
UIImage *image = [UIImage imageWithData:data];
// 图片下载完成之后,回到主线程更新UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
cell.iconImageView.image = image;
}];
}];
// 把操作添加到队列
[_queue addOperation:op];
return cell;
}
原理:
1.cell上的系统默认的子控件都是懒加载上去的
2.在返回cell之前,如果没有给cell上的默认的子控件赋值,那么这个默认的子控件就不会加载到cell上;
3.跟cell做交互(点击)时,默认会自动调用layoutSubViews方法,重新布局了子控件。
问题2 : 当有网络延迟时,来回滚动cell,会出现cell上图片的闪动;因为cell有复用
解决办法 : 占位图
// 在图片下载之前,先设置占位图
cell.iconImageView.image = [UIImage imageNamed:@"user_default"];
问题3 : 图片每次展示,都要重新下载,用户流量流失快
解决办法 : 设计内存缓存策略 (字典)
-
3.1增加图片缓存池
@implementation ViewController { /// 数据源数组 NSArray *_appsList; /// 全局队列 NSOperationQueue *_queue; /// 图片缓存池 NSMutableDictionary *_imagesCache; }
-
3.2实例化图片缓存池
-(void)viewDidLoad { [super viewDidLoad]; // 实例化队列 _queue = [[NSOperationQueue alloc] init]; // 实例化图片缓存池 _imagesCache = [[NSMutableDictionary alloc] init]; [self loadJsonData]; }
-
3.3 在cell的数据源方法中向缓存池中获取图片
// 在建立下载操作之前,判断要下载的图片在图片缓存池里面有没有 UIImage *memImage = [_imagesCache objectForKey:app.icon]; //如果获取到图片 if (memImage) { NSLog(@"从内存中加载...%@",app.name); //赋值 cell.iconImageView.image = memImage; //直接返回,不执行后续操作 return cell; }
-
3.4在异步下载图片的时候,将下载的图片放入图片缓存池中
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"从网络中加载...%@",app.name); // 模拟网络延迟 [NSThread sleepForTimeInterval:0.2]; // URL NSURL *URL = [NSURL URLWithString:app.icon]; // data NSData *data = [NSData dataWithContentsOfURL:URL]; // image UIImage *image = [UIImage imageWithData:data]; // 图片下载完成之后,回到主线程更新UI [[NSOperationQueue mainQueue] addOperationWithBlock:^{ cell.iconImageView.image = image; // 把图片保存到图片缓存池 if (image != nil) { [_imagesCache setObject:image forKey:app.icon]; } }]; }];
问题4 : 当有网络延迟时,滚动cell会出现图片错行的问题
解决办法 : 刷新对应的行
// 图片异步下载完成之后,刷新对应的行,并且不要动画(偷偷的,不让用户发现)
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
问题5 : 当有网络延迟时,来回滚动cell,会重复建立下载操作
[站外图片上传中……(6)]
解决办法 : 操作缓存池 (字典)
-
5.1建立操作缓存池
@implementation ViewController { /// 数据源数组 NSArray *_appsList; /// 全局队列 NSOperationQueue *_queue; /// 图片缓存池 NSMutableDictionary *_imagesCache; /// 操作缓存池 NSMutableDictionary *_OPCache; }
-
5.2实例化
_OPCache = [[NSMutableDictionary alloc] init];
-
5.3模拟网络延迟
// 在建立下载操作之前,判断下载操作是否存在 if ([_OPCache objectForKey:app.icon] != nil) { NSLog(@"正在下载中...%@",app.name); return cell; } NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{ *************************************** // 模拟网络延迟 : 让屏幕之外的图片的下载延迟时间比较长 if (indexPath.row > 9) { [NSThread sleepForTimeInterval:15.0]; } *************************************** NSURL *URL = [NSURL URLWithString:app.icon]; NSData *data = [NSData dataWithContentsOfURL:URL]; UIImage *image = [UIImage imageWithData:data]; // 图片下载完成之后,回到主线程更新UI [[NSOperationQueue mainQueue] addOperationWithBlock:^{ if (image != nil) { [_imagesCache setObject:image forKey:app.icon]; [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; } *************************************** // 图片下载完成之后,需要把操作缓存池的操作移除 [_OPCache removeObjectForKey:app.icon]; *************************************** }]; }]; *************************************** // 把下载操作添加到操作缓存池 [_OPCache setObject:op forKey:app.icon]; *************************************** // 把操作添加到队列 [_queue addOperation:op]; return cell; }
[站外图片上传中……(7)]
问题6 : 处理内存警告
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// 清除图片缓存池
[_imagesCache removeAllObjects];
// 清除操作缓存池
[_OPCache removeAllObjects];
// 清除队列里面所有的操作
[_queue cancelAllOperations];
}
问题7 : 当程序再次启动时,内存缓存失效了;要设计沙盒缓存策略
-
7.1在cell数据源方法中
// 在建立下载操作之前,内存缓存判断之后,判断沙盒缓存 UIImage *cacheImage = [UIImage imageWithContentsOfFile:[app.icon appendCachesPath]]; if (cacheImage) { NSLog(@"从沙盒中加载...%@",app.name); // 在内存缓存保存一份 [_imagesCache setObject:cacheImage forKey:app.icon]; // 赋值 cell.iconImageView.image = cacheImage; return cell; }
-
7.2 创建了一个字符串的分类,获取沙盒图片缓存路径
- (NSString *)appendCachesPath { // 获取沙盒路径 NSString *path = NSSearchPathForDirectoriesInDomains(NSCachesDirectory,NSUserDomainMask, YES).lastObject; // 获取文件名 : //如http://p16.qhimg.com/dr/48_48_/t0125e8d438ae9d2fbb.png // self : 这个方法的调用者 // lastPathComponent : 截取网络地址最后一个`/`后面的内容(就是图片名) NSString *name = [self lastPathComponent]; // 路径拼接文件名 // stringByAppendingPathComponent : 会自动添加`/` NSString *filePath = [path stringByAppendingPathComponent:name]; return filePath; }
搞定
[站外图片上传中……(8)]