GCD和NSOperation比较
- GCD :任务+队列
- 步骤:创建队列--->使用函数将封装的任务添加到队列中
- NSOperation: 操作+队列
- 3种方式
- 1.自定义NSOperation
- 2.NSBlockOperation
- 3.NSInvocationOperation)
- 步骤:创建队列--->封装操作--->添加操作到队列
- 3种方式
GCD和NSOperation的对比:
(0)GCD中的队列分为主队列,串行队列,全局并发队列,并发队列。NSOperationQueue中的队列分为主队列和非主队列。所以前提要知道是在GCD中还是NSOperationQueue,才能知道具体某个队列怎么使用。
1)GCD是纯C语言的API,而操作队列则是Object-C的对象。
2)在GCD中,任务用块(block)来表示,而块是个轻量级的数据结构;
相反操作队列中的『操作』NSOperation则是个更加重量级的Object-C对象。
3)具体该使用GCD还是使用NSOperation需要看具体的情况
NSOperation和NSOperationQueue的好处有:
1)NSOperationQueue可以方便的调用cancel方法来取消某个操作,而GCD中的任务是无法被取消的(安排好任务之后就不管了)。
2)NSOperation可以方便的指定操作间的依赖关系。
3)NSOperation可以通过KVO提供对NSOperation对象的精细控制(如监听当前操作是否被取消或是否已经完成等)
4)NSOperation可以方便的指定操作优先级。操作优先级表示此操作与队列中其它操作之间的优先关系,优先级高的操作先执行,优先级低的后执行。
5)通过自定义NSOperation的子类可以实现操作重用(实现代码复用性),
NSOperationQueue中的两种队列
- 主队列:
- 本质是串行队列。和GCD中的一样。不开子线程
- 方式方式:[NSOperationQueue mainQueue]
- 非主队列
- 同时具备了并发队列和串行队列,默认是并发队列(通过maxConcurrentOperationCount的值来控制是并发队列还是串行队列),默认情况可以开子线程
- 创建方式:[[NSOperationQueue alloc]init]
操作依赖+操作监听
- 可以控制内容输出顺序。
- 下图因为添加了操作依赖,根据依赖的顺序决定了输出顺序,所以输出顺序为3->2->4->1。
-
下图又因为给4添加了操作监听,所以执行完4之后,就会立刻执行监听的内容,然后才会执行1。
NSOperation的3个子类
- 因为NSOperation是个抽象类,并不具备封装操作的能力。通过子类继承父类的形式(NSInvocationOperation : NSOperation),子类就拥有了父类的方法和属性,可以执行封装操作
第一个子类:NSBlockOperation(常用)
- blockOperation +queue
简单使用:
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1---%@",[NSThread currentThread]);
}];
[op1 start];
---------
具体应用:
具体应用----->内部已经将调用的start方法封装在底层,不需要写出来了
//1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
//2.封装操作
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1----%@",[NSThread currentThread]);
}];
//3.添加操作到队列
[queue addOperation:op1];
---
具体应用的简便方法
//1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
//2.封装操作+添加操作到队列
[queue addOperationWithBlock:^{
NSLog(@"6---%@",[NSThread currentThread]);
}];
第二个子类:NSInvocationOperation
NSInvocationOperation *op1 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(download1) object:nil];
[op1 start];
---
具体应用:
//1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
//2.封装操作
NSInvocationOperation *op1 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(download1) object:nil];
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
//3.添加操作到队列
//addOperation方法内部调用了start方法,start方法内部调用了main方法
[queue addOperation:op1];
-(void)download1{
NSLog(@"%s---%@",__func__,[NSThread currentThread]);
}
第三个子类:自定义NSOperation
- customOperation + queue
- 1.有利于代码复用(外界直接用[[ZBOperation alloc]init],不需要管内部是怎么操作)
- 2.有利于代码隐蔽(只给外界提供了调用的接口,内部的代码外界看不见)
简单使用:
ZBOperation *op1 = [[ZBOperation alloc]init];//ZBOperation继承NSOperation
[op1 start];//调用start本质是调用main方法
--------
具体应用:
//1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
//2.封装任务
ZBOperation *op1 = [[ZBOperation alloc]init];
//3.添加操作到队列
[queue addOperation:op1];
ZBOperation继承NSOperation。并且在NSOperation.m文件中重写了main方法
-(void)main{
NSLog(@"1----%@",[NSThread currentThread]);
}
最大并发数:maxConcurrentOperationCount
- maxConcurrentOperationCount为1就是串行队列,大于1就是并发队列
- 如果设置为了串行队列,那么不能使用追加操作,否则追加的操作不是串行队列(因为下面截图中打印为7,4,5,6,而串行队列打印结果应为4,5,6,7),但是追加操作之前的仍为串行队列。
NSOperation实现线程间通信
ViewController.m文件
#import "ViewController.h"
@interface ViewController ()
/** 声明全局变量,防止出了花括号被销毁*/
@property (nonatomic ,strong) UIImage *image1;
@property (nonatomic ,strong) UIImage *image2;
//已经和storyboard的UIImageView控件关联
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end
@implementation ViewController
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self combie];
}
//思想:下载图片的操作一定要放在子线程做(规定).步骤1,2可以实现下载图片的在子线程做
// 刷新UI的操作一定要在主线程执行(规定)。可以通过[NSOperationQueue mainQueue]的形式回到主线程
-(void)combie
{
//1.创建队列
NSOperationQueue *queue =[[NSOperationQueue alloc]init];
//2.封装操作
NSBlockOperation *download1 = [NSBlockOperation blockOperationWithBlock:^{
//2.1 请求url
NSURL *url = [NSURL URLWithString:@"http://img.qiyenet.net/upload/image/2016/03/05/1457133748832510.png"];
//2.2 下载图片的二进制数据
NSData *data = [NSData dataWithContentsOfURL:url];
//2.3 转换图片
self.image1 = [UIImage imageWithData:data];
}];
//3.下载图片2
NSBlockOperation *download2 = [NSBlockOperation blockOperationWithBlock:^{
//3.1 请求url
NSURL *url = [NSURL URLWithString:@"http://www.52tq.net/uploads/allimg/160226/1021043B3-3.jpg"];
//3.2 下载图片的二进制数据
NSData *data = [NSData dataWithContentsOfURL:url];
//3.3转换图片
self.image2 = [UIImage imageWithData:data];
}];
//4.合并图片
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
UIGraphicsBeginImageContext(CGSizeMake(200, 200));
[self.image1 drawInRect:CGRectMake(0, 0, 100, 200)];
[self.image2 drawInRect:CGRectMake(100, 0, 100, 200)];
self.image1 = nil;
self.image2 = nil;
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//线程间通信(在主线程中执行)
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
self.imageView.image = image;
}];
}];
//5.设置依赖关系
[op addDependency:download1];
[op addDependency:download2];
//6.添加操作到队列
[queue addOperation:op];
[queue addOperation:download1];
[queue addOperation:download2];
}
@end
多图下载综合案例(重复下载+卡顿)
#import "ViewController.h"
#import "ZBApp.h"
@interface ViewController ()
@property (nonatomic, strong)NSArray *apps;
/** 图片缓存*/
@property (nonatomic ,strong) NSMutableDictionary *images;
@end
@implementation ViewController
-(NSMutableDictionary *)images
{
if (_images == nil) {
_images = [NSMutableDictionary dictionary];
}
return _images;
}
-(NSArray *)apps
{
if (_apps == nil) {
//字典数组
NSArray *arrayM = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle]pathForResource:@"apps.plist" ofType:nil]];
//字典数组 --- >模型数组
NSMutableArray *arrayMode = [NSMutableArray array];
for (NSDictionary *dict in arrayM) {
ZBApp *x = [ZBApp appWithDict:dict];
[arrayMode addObject:x];
}
_apps = arrayMode;
}
return _apps;
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.apps.count;
}
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
//1.创建cell
static NSString *ID = @"app";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
//2.设置cell的数据
//2.1 拿到该行cell 对应的数据
ZBApp *appM = self.apps[indexPath.row];
//2.2 设置标题
cell.textLabel.text = appM.name;
//2.3 设置子标题
cell.detailTextLabel.text = appM.download;
//2.4 设置图片
//出现的问题1:重复下载问题:当再次浏览之前已经下载过的图片的时候,用户依旧会花流量下载这张图片
//出现的问题2:卡顿,UI不流畅问题:因为是在在主线程下载图片,通过打印也可以得知,所以会卡顿,放在子线程里下载即可
NSURL *url = [NSURL URLWithString:appM.icon];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
cell.imageView.image = image;
NSLog(@"%zd--直接下载--%@",indexPath.row,[NSThread currentThread]);
//3.返回cell
return cell;
}
- 解决问题1(卡顿问题):
UIImage *image = [self.images objectForKey:appM.icon];//NSMutableDictionary获取元素的全写形式
//和上面的代码等价 UIImage *image = self.images[appM.icon];//NSMutableDictionary获取元素的简写形式
if(image){
//直接设置
cell.imageView.image = image;
NSLog(@"%zd使用了内存缓存",indexPath.row);
}else{// 自己下载
NSURL *url = [NSURL URLWithString:appM.icon];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
cell.imageView.image = image;
//把图片存到内存缓存中
[self.images setObject:image forKey:appM.icon];
NSLog(@"%zd--直接下载--%@",indexPath.row,[NSThread currentThread]);
}
多图下载综合案例(优化完毕)
代码中的设置图片的过程(也就是SDWebImage的底层实现原理):
文字描述上述截图(SDWebImage的底层实现原理)
入口是UIImageView+WebCache这个分类,在分类里面执行下载图片的方法sd_setImageWithURL:placeholderImage: ;
1.首先以url作为数据的索引先在内存中寻找是否有对应的缓存(内存缓存),如果找到了,就设置图片显示;
2.如果在内存缓存中没找到,就会通过MD5处理过的key来磁盘中查询数据(磁盘缓存,沙盒缓存),如果找到了,就把磁盘中的数据加载到内存中,并设置图片显示;
3.如果在磁盘中没找到,就会检查操作缓存,看有没有正在下载,有,就什么也不做;没有,就会向远程服务器发出请求,下载图片,下载后的图片会加入到缓存中,并写入磁盘。
注意:获取图片的过程都是在子线程中执行,获取图片后回到主线程显示图片
```
---
- 从内存缓存中取数据比在磁盘缓存中去数据快。就比如你把钱放在兜里(内存缓存)和放在银行里(磁盘缓存),你要花钱时,哪个能马上拿出来使用呢
- 注意点:apps.plist文件中的图片都是网络上的图片,而不是导入的项目中的图片
- 字典转模型的方法 setValuesForKeysWithDictionary:从字面意思可以看出来,后面的参数必须是字典NSDictionary类型的。即可逆推,必须先把数组先转换成NSDictionary类型的。然后才可以把字典转成模型。
- 磁盘缓存(即沙盒缓存,在Library/Caches路径下)
- 只要用户下载了一次,重新打开时,不需要耗费流量
---
####ViewController.m文件
```objc
#import "ViewController.h"
#import "ZBApp.h"
@interface ViewController ()
@property (nonatomic, strong)NSArray *apps;
/** 队列*/
@property (nonatomic ,strong) NSOperationQueue *queue;
/** 图片缓存*/
@property (nonatomic ,strong) NSMutableDictionary *images;
@property (nonatomic ,strong) NSMutableDictionary *operations;
@end
@implementation ViewController
#pragma mark --------------------
#pragma mark lazy loading
-(NSOperationQueue *)queue
{
if (_queue == nil) {
_queue = [[NSOperationQueue alloc]init];
// 最大并发数,最多允许5个操作
_queue.maxConcurrentOperationCount = 5;
}
return _queue;
}
-(NSMutableDictionary *)images
{
if (_images == nil) {
_images = [NSMutableDictionary dictionary];
}
return _images;
}
-(NSArray *)apps
{
if (_apps == nil) {
//字典数组
NSArray *arrayM = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle]pathForResource:@"apps.plist" ofType:nil]];
//字典数组 --- >模型数组
NSMutableArray *arrayMode = [NSMutableArray arrayWithCapacity:arrayM.count];
for (NSDictionary *dict in arrayM) {
[arrayMode addObject:[ZBApp appWithDict:dict]];
}
_apps = arrayMode;
}
return _apps;
}
-(NSMutableDictionary *)operations
{
if (_operations == nil) {
_operations = [NSMutableDictionary dictionary];
}
return _operations;
}
#pragma mark --------------------
#pragma mark UITableViewDataSource
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.apps.count;
}
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//1.创建cell
static NSString *ID = @"app";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
//2.设置cell的数据
//2.1 拿到该行cell 对应的数据
ZBApp *appM = self.apps[indexPath.row];
//2.2 设置标题
cell.textLabel.text = appM.name;
//2.3 设置子标题
cell.detailTextLabel.text = appM.download;
/* //精华流程:呕心沥血. 总结:都是要保存到内存缓存,下次访问,直接从内存缓存里面取就行
//解决卡顿问题:在子线程中下载图片
//三大方面彻底解决了图片重复下载的问题:1.判断内存中有没有这张图片(术语:内存缓存)
2.判断磁盘(沙盒--->Library/Caches)中有没有这张图片(术语:磁盘缓存)
3.判断图片有没有正在下载。(属于:操作缓存)
总结:从1,2,3三大方面彻底解决了图片重复下载的问题
//解决数据错乱的问题:1.清空图片-------> cell.imageView.image =nil;
2.设置占位图片---> cell.imageView.image = [UIImage imageNamed:@"Snip20200808_172"];
数据错乱问题原因:之前下载的图片会放到内存缓存中,因为是循环利用,所以会循环利用到其他的cell上,但是其他的cell有自己要显示的内容,其他的cell的内容自己下载,然后显示到自己的cell上
//设置并显示图片的过程大解析:
有--->直接设置图片
先检查内存缓存--< 有---->使用二进制数据+设置图片,并保存到内存缓存
没有-->检查磁盘(沙盒)缓存< 有--->等待图片下载完就行,你不需要做任何事情
没有---->判断图片有没有正在下载<
没有--->封装操作(操作的block里实现下载图片),将操作添加到队列中
*/
//2.4 设置图片
//检查缓存
// 根据字典中的key获取value,这里的可以就是appM.icon
UIImage *image = [self.images objectForKey:appM.icon];//NSMutableDictionary获取元素的全写形式
//和上面的代码等价 UIImage *image = self.images[appM.icon];//NSMutableDictionary获取元素的简写形式
if(image)
{
//直接设置
cell.imageView.image = image;
NSLog(@"%zd使用了内存缓存",indexPath.row);
}else
{
NSString *cachesPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];// a
//得到图片的名称(图片的最后一个结点 ,就是最后一个/后面的名称)
NSString *filename = [appM.icon lastPathComponent];// b
//拼接文件的全路径(沙盒路径+图片链接中的最后一个下划线+下划线后面的内容)--->将来下载的图片保存到这个全路径中
NSString *fullPath = [cachesPath stringByAppendingPathComponent:filename];// c
NSLog(@"%@",fullPath);
// fullPath = cachesPath+ / + filename(每一行cell对应不同的filename)
// 即fullPath 等于 Library/Caches + / + t017bc3cfcf3981b197.png
//去检查磁盘缓存
//a,b,c都是为d做准备,即a,b,c最终就是得到文件(图片)的全路径fullPath)
NSData *data = [NSData dataWithContentsOfFile:fullPath];// d
data = nil;
if(data)
{
UIImage *image = [UIImage imageWithData:data];
cell.imageView.image = image;
//把图片存到内存缓存中(将appM.icon这个key对应的image的内容存到可变字典中),解决重复下载的问题
[self.images setObject:image forKey:appM.icon];
NSLog(@"%zd使用了磁盘缓存",indexPath.row);
}else
{
//清空图片或者是设置占位图片 什么图片都可以 这里是设置了占位图片,网上的图片下载到本地之前先用占位图片顶替上。当网上的图片下载完毕之后,自动替换掉占位图
//cell.imageView.image = [UIImage imageNamed:@"Snip20200808_172"];
//检查操作缓存
//创建非主队列(让这个非主队列存储16个任务)-->也是并发队列
//通过判断dowbloadOperation有没有值,可以得知某一行cell的图片有没有被下载。因为[self.queue addOperation:dowbloadOperation];
NSBlockOperation *dowbloadOperation = [self.operations objectForKey:appM.icon];
//等价于 NSBlockOperation *dowbloadOperation = self.operations[appM.icon];
if (dowbloadOperation) {
//如果存在,那么什么都不做
}else
{ //因为下载图片(a,b,c)是耗时操作(需要一定的时间才能下载完),所以应将下载图片放到子线程NSBlockOperation中执行
dowbloadOperation = [NSBlockOperation blockOperationWithBlock:^{//注意:以下都是block块里面的内容.折叠就可以看出来
NSURL *url = [NSURL URLWithString:appM.icon];//a
NSData *data = [NSData dataWithContentsOfURL:url];//b 请求超时时间为30秒,超过30秒,请求失败
// 模拟延迟下载图片
// for(int i = 0;i<1000000000;i++){
//
// }
/***************************************这行才是真正的下载图片***************************************************/
UIImage *image = [UIImage imageWithData:data];//c
//容错操作。 如果apps.plist文件中的某个图片路径不存在,就把这个图片移除,用占位图片顶替。如果没有这个容错操作,那么程序将会崩掉.[已验证]
if (image == nil) {
[self.images removeObjectForKey:appM.icon];
return;
}
//把图片存到内存缓存中。就是把图片存到了self.images可变字典中
[self.images setObject:image forKey:appM.icon];
NSLog(@"%zd直接下载---%@",indexPath.row,[NSThread currentThread]);
//把图片保存到磁盘缓存(规定:只有用二进制数据才能够将图片保存到磁盘缓存)
[data writeToFile:fullPath atomically:YES];
//设置图片(必须放在主线程中(等同主队列),图片才能显示,如果放在子线程,则显示不了图片)
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
//刷新指定的行(局部刷新) 第一个参数:数组 第二个参数:动画
//之所以不用写cell.imageView.image = image;的原因是,执行reloadRowsAtIndexPaths方法刷新指定行时,程序又会执行cellForRowAtIndexPath方法,所以必定执行if(image){}里面的内容。所以图片会显示
[tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationMiddle];
}];
}];
//添加操作到缓存中
/*
*********************************************************************************
* 目的:防止重复下载
* 出现重复下载的原因:网速的原因。详细原因如下面4个步骤。如果用户浏览的图片马上能显示,不用10秒,就不会有下面的问题了
* 1.用户浏览某一行的cell的图片,就会调用cellForRowAtIndexPath方法,这一行cell的图片需要10秒才能下载完,然后用户没等这行cell的图片下载完(没有下载完,图片就不会存到内存缓存
* 中),就去浏览下一页的cell了。
* 2.当4秒后,用户再次浏览这一行cell的图片时,这张图片需要6秒才可以下载完,说明这时候图片仍没有存到内存缓存中
* 3.这个时候要显示这一行cell的时候,系统底层又会自动调用cellForRowAtIndexPath方法
* 4.在cellForRowAtIndexPath方法中:4.1先去检查内存缓存,内存缓存没有这张图片
* 4.2再检查磁盘缓存,磁盘缓存也没有
* 4.3此时就封装操作,将操作添加到队列中去下载图片,此时这张图片已经有两个下载任务
* ,所以最终造成了同一张图片被直接下载了两次,所以就出现了重复下载的问题。
* 解决办法:检查操作缓存,显示某一行cell的图片,调用cellForRowAtIndexPath方法,最终会检查操作中有没有这个图片的下载任务(dowbloadOperation),dowbloadOperation有值,就什么也不做,这就解决了重复下载图片的问题
*********************************************************************************
*/
//self.operations的数组中执行的是并发队列,一次就可以下载多张图片
[self.operations setObject:dowbloadOperation forKey:appM.icon];
//将dowbloadOperation中保存的操作添加到队列中进行下载图片
[self.queue addOperation:dowbloadOperation];
}
}
}
//3.返回cell
return cell;
}
-(void)didReceiveMemoryWarning{
//如果出现了内存警告,就做如下操作,保命要紧啊。
//移除内存缓存
[self.images removeAllObjects];
//取消队列中的操作
[self.queue cancelAllOperations];
}
@end
ZBApp.h文件
#import
@interface ZBApp : NSObject
/** 名称*/
@property (nonatomic ,strong) NSString *name;
/** 图标的地址*/
@property (nonatomic ,strong) NSString *icon;
/** 下载量*/
@property (nonatomic ,strong) NSString *download;
+(instancetype)appWithDict:(NSDictionary *)dict;
@end
ZBApp.m文件
#import "ZBApp.h"
@implementation BApp
+(instancetype)appWithDict:(NSDictionary *)dict
{
ZBApp *appM = [[ZBApp alloc]init];
[appM setValuesForKeysWithDictionary:dict];
return appM;
}
@end
本Demo核心代码:
//检查内存缓存
UIImage *image = [self.images objectForKey:appM.icon];
//检查磁盘缓存
NSData *data = [NSData dataWithContentsOfFile:fullPath];
//检查操作缓存
NSBlockOperation *dowbloadOperation = [self.operations objectForKey:appM.icon];
将得到的对象利用if进行判断是否有缓存,自己设定有缓存应该做什么操作,没有缓存应该做什么操作