利用内存结构及多线程优化多图片下载(IOS篇)

利用内存结构及多线程优化多图片下载(IOS篇)

前言

下载地址, 后续发布, 请继续关注本blog

在IOS中,我们常常遇到多图片下载的问题。最简单的解决方案是直接利用别人写好的框架。但是这如同练武,只练外功而不练内功。
在这些框架中,SDWebImage这个框架是比较常用的框架,对于该框架的使用,不在这再做详细介绍。主要从计算机的视角和多线程
引发的一些问题来分享下如何自己做,或者说SDWebImage大体上也是基于这种方式来做的。在这之前,有必要先说下一些操作系统的基本架构和原理。

内存结构

其实在操作系统中,所谓的内存结构,不是指我们电脑中的内存。在专业术语中,电脑中的内存称为主存。而内存结构指的是由磁盘+主存+缓存
构成的结构。在这个构架中,从磁盘的速度比主存的速度慢,而主存的速度又比缓存的速度慢。这三种存储物质也是由不同的材料所做成,
所以缓存的价格大于主存的价格,而主存又大于磁盘的价格。要不然你都可以把电脑磁盘替换成内存了,那将是十分的快,当然的保证你电脑是不断电的。所以程序启动的时候
,都是从磁盘中读取数据,到主存中完成整个程序的加载,这时候,程序就在主存中。

重点

同样的道理,我们在做App的时候,对于图片下载这种问题。我们深知,必须得使用多线程来下载图片,然后另外一个线程来刷新界面。这才不会导致因为下载事件过长而引起的界面十分不流畅。同时,我们为了避免重复的下载图片,为用户节省流量,并且也为了提高图片的加载速度。我们有必要利用内存结构的特点来解决这个问题。所以对于这种问题,我们主要的思路就是
1.将下载的图片缓存到主存中开辟的一块缓存图片的空间。进行UI渲染的时候到缓存中取到对应的图片,渲染UI界面。

判断逻辑过程如下:

1.先判断主存缓存中有没有图片,如果没有进行第二步

2.判断磁盘有没有缓存的图片,如果有将其加载进内存,并缓存到主存中的缓存图片的位置。如果没有进行第三步

3.从网络中下载图片,并缓存到内存和磁盘上。

4.应用程序需用用到图片的时候,直接从内存中的缓存图片的位置拿。

存在的问题:

A.第一个是需要用子线程来下载图片,主线程进行渲染, 从而提高程序流畅性。

B.第二个是解决因为图片过大或者图片数据下载过慢时候,图片还没有下载完,还没缓存到内存中时候。用户不断拖拽TableView,

由于UITableViewCell循环利用,使得在进行判断1的时候,重复下载图片。

C.第三个是将主存中的图片缓存写入磁盘在渲染UI之前,但是我们可以为期在开个线程让两个同时进行,提高程序的效率。

对于第一个问题,很好解决。请看代码, 这是自定义cell中针对传入模型数据进行的处理。只需关注该重点,想要测试程序自行
到我的github上下载,如果你觉得这个程序对你有学习价值,记得给个star。

因为不想重复的粘贴代码,所以以下代码是最终版本的核心代码,但是为了说明问题。问题重现,所以请跟着我的步骤来,一步步的
打开被注释的代码,观察效果。
现在请你忽略所有注释的代码,先搞懂这是为了解决问题A。

- (void)setApp:(SWPApp *)app {

    _app = app;

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        // 1.子线程下载数据

        SWPCache * cache = [SWPCache sharedInstance];

        // 1.1内存无缓存
// if ( cache.imageCache[app.icon] == nil ) { NSString * folderPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; NSString * filePath = [folderPath stringByAppendingPathComponent:[app.icon lastPathComponent]]; BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath: filePath]; if (!fileExists) [self loadImageWithURLOrFilePath:app.icon isFilePath: NO]; // 1.2 磁盘无缓存则从网络下载
else [self loadImageWithURLOrFilePath:filePath isFilePath: YES]; // 1.3 磁盘有缓存, 直接加载进内存中的缓存 // } // [NSThread sleepForTimeInterval: 1]; // 2.主线程渲染cell的UI
dispatch_async(dispatch_get_main_queue(), ^{ self.textLabel.text = app.name; self.imageView.image = cache.imageCache[app.icon]; self.detailTextLabel.text = app.download; }); }); } - (void)loadImageWithURLOrFilePath:(NSString *)url isFilePath:(BOOL)isFilePath { SWPCache * cache = [SWPCache sharedInstance];
NSData * data = nil;
// 1.先判断下载该图片的操作是否已经执行过
// 如果执行过, 那么图片缓存中必定存在图片.
// if (!cache.operationCache[self.app.icon]) {
static int i = 0;
NSLog(@"---%d", i);
data = isFilePath ? [NSData dataWithContentsOfFile: url]
: (i++, [NSData dataWithContentsOfURL: [NSURL URLWithString: url ]]); // 如果数据下载失败
if (!data) { [cache.operationCache removeObjectForKey: self.app.icon]; } else { UIImage * image = [UIImage imageWithData: data]; cache.imageCache[self.app.icon] = image; cache.operationCache[self.app.icon] = [NSNumber numberWithBool: true]; // if (!isFilePath) {
// 1.为让其一边显示一边写入
//dispatch_async(dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[data writeToFile:url atomically: YES];
// });
// } } // } }

解决完UI界面的流畅度问题,我们就需要利用内存结构来节约用户流量和提高UI再次渲染的速度。

所以此时,还没将图片缓存到主存中,所以请看下面动态图。再将第12,24行打开,再看第二种动态图会发现,打印值只到16,也就是所只下载了
16次图片。这就大大提高了的说明了能节约用户流量和提高UI再次渲染的速度。
如下图(没加入主存时候)

利用内存结构及多线程优化多图片下载(IOS篇)

分析B: 接着我们来看问题B。也许这时候你觉得程序已经不存在问题了,确实,现在的程序是不存在问题了,但是可能会遇到问题。就是遇到一种十分
极端的情况,这种情况可以通过断网来进行模拟。(模拟数据量过大,或者下载速度太慢,此时用户不断滚动TableView)会造成,因为图片没下载好,也就还没缓存到主存,所以当要取图片的时候,到主存对应的位置
去取,却发现没有,这时候,就会调用网络下载,下载图片,就造成了不断重复的下载。
如下图

利用内存结构及多线程优化多图片下载(IOS篇)

解决B
这时候我们就需要某种标志来,标志该下载已经存在,不需要重新下载。所以我用了一个字典来映射各个下载图片的操作,在下载操作执行前从字典中取出,判断有没有该操作,有则不重复下载。这是可以打开第52,和82行即可,观察到效果。(记得打开网络!)
如下图

利用内存结构及多线程优化多图片下载(IOS篇)

分析C: 其实C问题所起来很好解决,阅读我的源代码,你可以看到第75行是在当前线程中写入数据到磁盘,这就造成了,要等待该写入操作完成后才退出该函数,接着才将渲染任务交给主线程。但是写入操作和渲染操作其实是可以同时进行的。所以我们可以在这里使用异步函数

解决C:
打开对应的注释(72和77), 验证就不在做了,可以自己打印时间观察。

所以对于多图片下载的问题我们主要是这么做:
1.通过多线程的方式,解决UI能流畅渲染,。
2.通过利用内存构架提高UI渲染的速度,并且解决了第一种图片重复下载问题。
3.通过标记操作,实现同一下载互斥,解决UITableViewCell重用机制造成的第二种图片重复下载问题。

具体判断逻辑与细节:

1.先判断主存缓存中有没有图片,如果没有进行第二步

2.判断磁盘有没有缓存的图片,如果有则直接加载进主存缓存中,并记录该次操作,如果没有进行第三步。

3.先判断该下载操作是否存在,如果存在,则不进行下载操作。如果不存在进行第四步。

4.从网络中下载图片,并且判断下载是否成功,如果成功下载,则记录该次下载操作,实现互斥。再将图片写入主存缓存,并开启另外一个线程将图片写入磁盘。

如果没有下载成功或者从磁盘中没有加载成功,则移除该次的下载标志, 解除该次下载互斥。

5.主线程直接从主存中的图片缓存位置来图片,渲染到UI界面。

上一篇:深入理解MyBatis框架的的配置信息


下一篇:1001.A+B Format (20)代码自查(补足版)