问题说明:假设tableView的每个cell上的imageView的image都是从网络上获取的数据。如何解决图片延迟加载(显示很慢)、程序卡顿、图片错误显示、图片跳动的问题。

需要解决的问题:

1.程序运行过程中,每次滚动tableView让新的cell进入视野的时候,都要从网络获取image,浪费了大量的用户流量,严重影响了手机性能和流畅度。

2.每次程序启动 ,都要再次从网络上获取image,浪费了大量的用户流量,严重影响了的手机性能和流畅度。

3.快速拖动tableView,会出现程序卡顿、无反应的现象(主线程阻塞),导致人机交互延迟,严重影响了用户体验。

4.快速拖动tableView,会出现图片显示错位、图片跳动的现象,严重影响用户体验。

5.快速拖动tableView,会出现程序占用内存飙升,程序不流畅的现象,严重影响用户体验。

针对于以上问题,解决方案依次如下:

1、声明可变字典属性,把下载好的图片放入这个可变字典属性(以下简称“图片内存缓存”或“内存缓存”或“缓存”),以图片的下载地址作为key来唯一标识区别其他图片。

2、获取本地cache目录(以下简称“本地缓存”或“本地”),把下载好的图片存入本地缓存

3、开启子线程(新线程),把下载图片这种耗时操作交给子线程来完成,图片下载完成后,跳回主线程更新UI,解决主线程中下载图片岛主主线程阻塞的问题

4、 多线程重复设置问题:多线程会存在这么一种情况:当cell的图片下载的时候,会开启一个新的子线程,由于多种原因(用户滑动的比较快、网速太差、图片太 大)。假如,下载当前cell的这一张图片需要十分钟,用户滚动tableView的时候,cell的图片还没下载完cell就被回收到 tableView的缓存池中,而此时被回收的cell的图片仍然在子线程中努力的下载中。当缓存池中的这个cell被重用的时候(此时cell的图片还 没下载完成),系统又会开启一个新的线程给这个cell下载对应的新图片(无论cell被重用到原来的位置还是新的位置,只要缓存或者本地没有对应的图片 都会再开启一个新的线程去下载),当第一个图片下载完后会显示到cell上(此时导致了图片的错误显示),当第二个图片下载完也会显示到cell上(此时 导致图片的快速跳动)
解决方案:更新UI的时候只刷新指定行[self.tableView
reloadRowsAtIndexPaths:@[indexPath]
withRowAnimation:UITableViewRowAnimationNone];不要使用cell.imageView.image =
image;

5、多线程重复下载问题:和重复设置问题类似,多线程会存在这么一种情况,当cell的图片下载的时候,会开启一个新的子线
程,由于多种原因(用户滑动的比较快、网速太差、图片太大)。假如,下载当前cell的这一张图片需要十分钟,用户滚动tableView的时
候,cell的图片还没下载完cell就被回收到tableView的缓存池中,而此时被回收的cell的图片仍然在子线程中努力的下载中。此时用户又回
滚tableView,缓存池中的cell又被重用到原来的位置,而此时无论是缓存中还是本地都没有这个cell对应的图片,所以系统又会开启一个新的线
程下载这个cell对应的图片。所以,这样一来就导致同一个cell的图片有两个线程在下载。如果用户抽风,不断的上下滚动tableView,导致同一
个cell不但的在缓存池和tableView之间切换(也就是系统不断的回收同一个cell到缓存池,然后又重用缓存池中的这个cell到cell原来
的位置(cell被回收之前在tableView上的位置))那么这种情况下,同一个cell的图片不止只是有两个子线程在下载,可能会有更多个子线程在
同事下载同一张图片,这样开辟了多个不必要的子线程,极大地浪费了用户手机的内存。
解决方案:增加NSMutableDictionary类型的
成员变量,开启NSOperation缓存,把每个正在执行的操作添加到字典,以图片的下载地址作为key来唯一标识其他NSOperation。每次开
启新线程下载图片之前,先判断字典中是否已经存在该key对应的操作,如果不存在,则开启子线程进行下载,否则什么都不做。

另外,需要注意的是,操作完成或失败,需要在字典中移除该操作,如果下载操作失败但没有从字典中移除,那么下次检测到字典中有这个key对应的操作,就永远不会开启新线程。


要考虑因为网络或者服务器宕机等其他不可控原因造成的下载数据data为nil的情况。这种情况下,需要判断data是否为nil,如果为nil,则直接
return,不需要再执行后面的代码。否则造成的后果是:data生成的image是空,把空的image赋值给图片缓存(字典),系统报错:
reason:
'*** setObjectForKey: object cannot be nil (key:
http://p0.qhimg.com/t01ad71850a5fae7e97.png)'。PS:后面()中的key为调试时候系统打印的,因为这
里我把image的下载地址作为了key。根据每个人自己程序中字典key的具体情况key的打印信息会存在差异。

本例采用MVC模式,需要根据plist的存储结构来构建数据模型,以下为程序用到的所有文件 以及 plist文件的存储结构:

根据plist文件的存储结构构建数据模型:

数据模型的.h文件:

#import <Foundation/Foundation.h>

@interface WSAppItem : NSObject

@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) NSString *download;
@property (nonatomic,copy) NSString *icon; - (instancetype)initWithDict:(NSDictionary *)dict;
+ (instancetype)itemWithDict:(NSDictionary *)dict; @end

数据模型的.m文件:

#import "WSAppItem.h"

@implementation WSAppItem

- (instancetype)initWithDict:(NSDictionary *)dict
{
if (self = [super init]) {
[self setValuesForKeysWithDictionary:dict];
}
return self;
} + (instancetype)itemWithDict:(NSDictionary *)dict
{
return [[self alloc] initWithDict:dict];
}
@end

NSString的分类的.h文件:

#import <Foundation/Foundation.h>

@interface NSString (WS)
/** 用于生成文件在caches目录中的路径 */
- (instancetype)cacheDir;
/** 用于生成文件在document目录中的路径 */
- (instancetype)docDir;
/** 用于生成文件在tmp目录中的路径 */
- (instancetype)tmpDir;
@end

NSString的分类的.m文件:

本程序中,以下方法只用到了cacheDir

#import "NSString+WS.h"

@implementation NSString (WS)

- (instancetype)cacheDir
{
// 获取cache(本地缓存)目录
NSString *path = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSLog(@"%@",path);
// 拼接绝对路径
return [path stringByAppendingPathComponent:[self lastPathComponent]];
} - (instancetype)docDir
{
NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentationDirectory, NSUserDomainMask, YES) lastObject];
return [path stringByAppendingString:[self lastPathComponent]];
} - (instancetype)tmpDir
{
NSString *path = NSTemporaryDirectory(); // 临时文件夹
return [path stringByAppendingString:[self lastPathComponent]];
}
@end

控制器.m文件:

#import "ViewController.h"
#import "WSAppItem.h"
#import "NSString+WS.h" @interface ViewController ()
/** 模型数组 */
@property(nonatomic,strong) NSArray *apps;
/** 图片缓存 */
@property(nonatomic,strong) NSMutableDictionary *imageCaches;
/** 操作缓存 */
@property(nonatomic,strong) NSMutableDictionary *operations;
@end @implementation ViewController - (void)viewDidLoad {
[super viewDidLoad]; // 设置storyBoard中的cell的高度:
// 1.需要拖动cell来设置cell的高度,不能通过尺寸检查器中的rowHeight设置
// 2.通过代码设置
// 3.通过代理设置
} #pragma mark - 懒加载
- (NSArray *)apps
{
if (_apps == nil) {
_apps = [NSArray array];
// 加载plist->数组
NSString *path = [[NSBundle mainBundle] pathForResource:@"apps.plist" ofType:nil];
NSArray *appsArr = [NSArray arrayWithContentsOfFile:path]; NSMutableArray *arrM = [NSMutableArray arrayWithCapacity:appsArr.count];
for (NSDictionary *dict in appsArr) {
WSAppItem *appItem = [WSAppItem itemWithDict:dict];
[arrM addObject:appItem];
}
// 创建不可变副本
_apps = [arrM copy];
}
return _apps;
} - (NSMutableDictionary *)imageCaches
{
if (_imageCaches == nil) {
_imageCaches = [[NSMutableDictionary alloc] init];
}
return _imageCaches;
} - (NSMutableDictionary *)operations
{
if (_operations == nil) {
_operations = [NSMutableDictionary dictionary];
}
return _operations;
}
#pragma mark - 数据源
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.apps.count;
} - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// 加载storyBoard中的cell
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"app"]; // 给cell设置数据
WSAppItem *appItem = self.apps[indexPath.row];
cell.textLabel.text =appItem.name;
cell.detailTextLabel.text = appItem.download;
// 设置占位图
cell.imageView.image = [UIImage imageNamed:@"temp"]; // 在block内部访问外面的对象,外面的对象必须要用__block修饰
__block UIImage *image = self.imageCaches[appItem.icon];
// 1.1、如果缓存中的图片为空,判断本地是否为空
if (image == nil) { // 拼接image在本地存储的路径
NSString *iconPath = [appItem.icon cacheDir]; // 获取image的存储在本地的二进制数据
NSData *data = [NSData dataWithContentsOfFile:iconPath]; // 2.1、如果本地存储的图片为空,则再判断operation缓存中是否已经开启了对应的操作
if (data == nil) {
// 3.0、获取operation中对应的操作
NSOperation *op = self.operations[appItem.icon];
// 3.1、如果在operation缓存中获取的op为空,则开启新线程下载
if (op == nil) {
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 开启子线程下载图片
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"下载的 图片");
// 把路径转换为URL->二进制数据
NSURL *url = [NSURL URLWithString:appItem.icon];
NSData *data = [NSData dataWithContentsOfURL:url];
// 如果下载失败或者data为空,则也要把操作从操作缓存中移除
if (data == nil) {
[self.operations removeObjectForKey:appItem.icon];
return;
}
NSLog(@"如果data为nil不能执行到这");
// 根据data获取图片
image = [UIImage imageWithData:data]; // 把下载好的图片放入缓存中
self.imageCaches[appItem.icon] = image; // 把下载好的图片写入本地
[data writeToFile:iconPath atomically:YES]; // 回到主线程更新UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// cell.imageView.image = image;
// 刷新指定行,避免重复设置
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; // 下载成功,把操作从操作缓存移除
[self.operations removeObjectForKey:appItem.icon];
}]; }]; // 把操作添加到操作缓存
self.operations[appItem.icon] = operation; // 把操作添加到队列
[queue addOperation:operation];
}else{
// 3.2、如果operatin缓存中有对应的操作,那么什么都不做
} }else{
NSLog(@"本地的 图片");
// 2.2、如果本地不为空,则加载本地图片
image = [UIImage imageWithData:data]; // 将本地图片缓存到缓存中,以后就直接从缓存中取
       // 注意:如果不添加到缓存中,那么每次程序启动都是从本地读取而非动缓存读取图片
         self.imageCaches[appItem.icon] = image;
// 更新UI
cell.imageView.image = image;
} }else{
NSLog(@"缓存的 图片");
// 1.2、如果缓存中的图片不为空,就加载缓存中的图片
image = self.imageCaches[appItem.icon]; // 更新UI
cell.imageView.image = image;
} return cell;
} @end

注意:为什么"重启程序"后显示读取的是本地图片,不是应该先本地后缓存吗???
 因为,受if语句嵌套的影响,外层if...else语句是判断缓存中有没有图片,内层if...else语句是判断本地有没有图片。所以,每次程序启动加载图片的顺序是,先判断缓存中有没有,再判断本地有没有;显然程序启动后,缓存中没有,那么就会去本地中查找,如果本地中也没有就会开启子线程下载图片,然后跳回主线程显示图片(也就是执行内层if语句);如果本地查找有相应图片的话,那么就会加载本地的图片(也就是执行内层if语句的else语句),所以这种情况下,永远不会加载缓存中的图片(也就是永远不会执行外层if语句的else语句)。解决这种问题的方式,可以在加载本地图片的时候,把本地图片添加到缓存当再次显示图片的时候,缓存不为空,所以就会加载缓存的图片,这样直接和缓存交互,速度和效率会更快一些。

self.imageCaches[appItem.icon] = image;

为什么有时候程序启动没有图片?设置占位图的作用?

1.如果程序第一次启动,那么肯定会开启子线程下载图片,如果不设置占位图,主线程执行完成,子线程图片没有下载完成,这种情况下,图片下载完成后因为没有刷新表格所以不会显示图片。

2.如果在没有联网的情况下第一次启动程序,没有设置占位图,程序会崩溃。(事实证明这句话是错误的,程序崩溃是因为data为空,根据data生成的image也是空,空对象赋值给字典自然会崩溃)

3.如果程序不是第一次启动,则不会开启子线程,直接加载缓存或者本地图片。

cell.imageView.image = [UIImage imageNamed:@"temp"];

总结:

预先准备:
 1>、声明可变字典属性,把下载好的图片放入缓存(字典)
 2>、声明可变字典属性,把正在执行的操作放入operation缓存(字典)

1.1、加载图片的时候,先判断内存缓存中有没有对应的图片。如果没有,则再判断本地缓存是否有对应图片
 2.1、如果本地缓存中没有对应图片,则再判断operation缓存中有没有对应的操作(有对应的操作说明该图片正在下载中,不需要再次开启新线程下载)

3.1、如果operation缓存中也没有对应操作,则真正开启子线程下载图片

注意:操作加入队列之前,把操作添加到operation缓存,操作完成或者失败,把操作从operation缓存移除

3.2、如果operation缓存中有对应的操作,则什么都不做
 2.2、如果本地有对应图片则获取本地图片
 1.2、如果内存缓存中有对应的图片,则加载缓存中的图片
 
 这样可以保证程序再次启动后,不会去下载图片,除非本地没有可用的图片
 面试主要针对以下几个方面回答:

1.重复下载  : 图片内存缓存和磁盘缓存

2.主线程阻塞  :  开启子线程

3.重复下载  :  增加NSOperation字典

4.重复设置  :  刷新指定行

5.下载失败或无网络  :  判断data是否为nil

版权说明:此博客由博主本人编写而成,转载请注明出处,如有不正确或者有待改进之处还请指正,谢谢!

tableView异步下载图片/SDWebImage图片缓存原理的更多相关文章

  1. IOS GCD图片数据异步下载,下载完成后合成显示

    关于GCD使用详解,请看我的上一篇blog:http://www.cnblogs.com/xin-lang/p/6278606.html 前段时间遇到个需要异步下载,下载完成后再组合显示的东西.这里我 ...

  2. 使用开源库 SDWebImage 异步下载缓存图片(持续更新)

    source  https://github.com/rs/SDWebImage APIdoc  http://hackemist.com/SDWebImage/doc Asynchronous im ...

  3. iOS利用SDWebImage图片下载缓存

    一.我们先来了解一下SDWebImage的使用: 1.导入框架,引入头文件: #import "UIImageView+WebCache.h" 也可以直接使用CocoaPods来引 ...

  4. iOS网络编程(三) 异步加载及缓存图片---->SDWebImage

    @SDWebImage提供一个UIImageView的类别以支持加载来自网络的远程图片.具有缓存管理.异步下载.同一个URL下载次数控制和优化等特征. @SDWebImage的导入1.https:// ...

  5. Android异步下载图片并且缓存图片到本地

    Android异步下载图片并且缓存图片到本地 在Android开发中我们经常有这样的需求,从服务器上下载xml或者JSON类型的数据,其中包括一些图片资源,本demo模拟了这个需求,从网络上加载XML ...

  6. 【iOS系列】-多图片多线程异步下载

    多图片多线程异步下载 开发中非常常用的就是就是图片下载,我们常用的就是SDWebImage,但是作为开发人员,不仅要能会用,还要知道其原理.本文就会介绍多图下载的实现. 本文中的示例Demno地址,下 ...

  7. SDWebImage 图片加载和缓存

    SDWebImage托管在github上.https://github.com/rs/SDWebImage 这个类库提供一个UIImageView类别以支持加载来自网络的远程图片.具有缓存管理.异步下 ...

  8. android ListView异步加载图片(双缓存)

    首先声明,参考博客地址:http://www.iteye.com/topic/685986 对于ListView,相信很多人都很熟悉,因为确实太常见了,所以,做的用户体验更好,就成了我们的追求... ...

  9. [翻译] AsyncImageView 异步下载图片

    AsyncImageView  https://github.com/nicklockwood/AsyncImageView AsyncImageView is a simple extension ...

随机推荐

  1. 网络流-最大流问题 ISAP 算法解释(转自Renfei Song's Blog)

    网络流-最大流问题 ISAP 算法解释 August 7, 2013 / 编程指南 ISAP 是图论求最大流的算法之一,它很好的平衡了运行时间和程序复杂度之间的关系,因此非常常用. 约定 我们使用邻接 ...

  2. HTML 学习笔记(链接)

    HTML链接 超链接可以是一个字,一个词,或者一组词,也可以是一幅图像,您可以点击这些内容来跳转到新的文档或者当前文档中的某个部分. 当您把鼠标指针移动到网页中的某个链接上时,箭头会变为一只小手. 我 ...

  3. offsetLeft与offsetTop详解

    offsetLeft与offsetTop使用方法一样,只是一个是找距离定位父级(position:relative)左边的距离,一个是找距离定位父级上边的距离 没有定位则找body,我们还是看看ie7 ...

  4. redis存在大量脏页问题的追查记录

    from:https://www.zybuluo.com/SailorXiao/note/136014 case现场 线上发现一台机器内存负载很重,top后发现一个redis进程占了大量的内存,TOP ...

  5. 深入运用js

    1,eval()函数 这个函数是获取参数的字符串,并将其作为js来处理,所以这里就有可能有人用这个来搞破坏(比如注入JS脚本文件等),所以最好的是方法是尽量少用,或者可以用new function() ...

  6. 040医疗项目-模块四:采购单模块—采购单创建好之后跳转到采购单修改页面(editcgd.action)

    我们上一篇文章写到了要从editcgd.action为入口讲.我们要做的事根据edicgd.acion进入到Action层的一个函数,在这个函数里面要做的就是从数据库中把采购单表里面的数据都查出来显示 ...

  7. C#获取文件MD5字符串

    备注 哈希函数将任意长度的二进制字符串映射为固定长度的小型二进制字符串.加密哈希函数有这样一个属性:在计算不大可能找到散列为相同的值的两个不同的输入:也就是说,两组数据的哈希值仅在对应的数据也匹配时才 ...

  8. Scrapy 爬虫

    Scrapy 爬虫 使用指南 完全教程   scrapy note command 全局命令: startproject :在 project_name 文件夹下创建一个名为 project_name ...

  9. opencv 中对一个像素的rgb值或像素值进行操作的几个常用小办法【转】

    You can access the Image pixels in many ways:1. One using the Inbuilt macro2. One using the pointer ...

  10. 实现Linux与Windows下一致的命令行

    这其实是个非常简单的东西. 我们会写一些命令行的工具,一般跨平台的话,会用python或者perl写,比如叫foo.py,然后在Windows和Linux下调用这个脚本: Linux: foo.py ...