利用内存结构及多线程优化多图片下载(IOS篇)
利用内存结构及多线程优化多图片下载(IOS篇)
前言
在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再次渲染的速度。。 如下图(没加入主存时候)
分析B: 接着我们来看问题B。也许这时候你觉得程序已经不存在问题了,确实,现在的程序是不存在问题了,但是可能会遇到问题。就是遇到一种十分 极端的情况,这种情况可以通过断网来进行模拟。(模拟数据量过大,或者下载速度太慢,此时用户不断滚动TableView)会造成,因为图片没下载好,也就还没缓存到主存,所以当要取图片的时候,到主存对应的位置 去取,却发现没有,这时候,就会调用网络下载,下载图片,就造成了不断重复的下载。 如下图
解决B 这时候我们就需要某种标志来,标志该下载已经存在,不需要重新下载。所以我用了一个字典来映射各个下载图片的操作,在下载操作执行前从字典中取出,判断有没有该操作,有则不重复下载。这是可以打开第52,和82行即可,观察到效果。(记得打开网络!) 如下图
分析C: 其实C问题所起来很好解决,阅读我的源代码,你可以看到第75行是在当前线程中写入数据到磁盘,这就造成了,要等待该写入操作完成后才退出该函数,接着才将渲染任务交给主线程。但是写入操作和渲染操作其实是可以同时进行的。所以我们可以在这里使用异步函数
解决C: 打开对应的注释(72和77), 验证就不在做了,可以自己打印时间观察。
所以对于多图片下载的问题我们主要是这么做: 1.通过多线程的方式,解决UI能流畅渲染,。 2.通过利用内存构架提高UI渲染的速度,并且解决了第一种图片重复下载问题。 3.通过标记操作,实现同一下载互斥,解决UITableViewCell重用机制造成的第二种图片重复下载问题。
具体判断逻辑与细节:
1.先判断主存缓存中有没有图片,如果没有进行第二步
2.判断磁盘有没有缓存的图片,如果有则直接加载进主存缓存中,并记录该次操作,如果没有进行第三步。
3.先判断该下载操作是否存在,如果存在,则不进行下载操作。如果不存在进行第四步。
4.从网络中下载图片,并且判断下载是否成功,如果成功下载,则记录该次下载操作,实现互斥。再将图片写入主存缓存,并开启另外一个线程将图片写入磁盘。
如果没有下载成功或者从磁盘中没有加载成功,则移除该次的下载标志, 解除该次下载互斥。5.主线程直接从主存中的图片缓存位置来图片,渲染到UI界面。