这一篇文章主要介绍iOS中实现多线程的第四种方案:NSOperation和NSOperationQueue,在日常的开发过程中也是用得相对较多的一种方式。
1、NSOperation和NSOperationQueue概念
NSOperation和NSOperationQueue是苹果对GCD的更高一层的封装,完全面向对象。了解过GCD之后再来看,可以知道NSOperation对应的GCD中的任务概念,而NSOperationQueue对应队列。
那么既然已经有了GCD,而且效率很高,使用也很方便,为什么还需要NSOperation呢?相较于GCD,NSOperation和NSOperationQueue有哪些不一样的特点?
2、 NSOperation和GCD比较
看一幅图:GCD的底层是C语言写的,我们知道越往底层效率越高,执行和操作更简单。NSOperation底层实际也是通过GCD实现的,而NSOperation是对GCD更高层次的抽象,这是他们之间最本质的区别。
NSOperation可以直接设置两个NSOperation之间的依赖,一个任务依赖另一个任务执行,这种具有依赖关系在开发中也很常见。GCD无法直接设置依赖关系,不过可以通过dispatch_barrier_async来实现类似效果。
NSOperation很容易判断任务当前的状态(比如是否执行,是否取消),对此GCD无法通过KVO进行判断。
NSOperation同时也提供了设置自身任务优先级的方法,但是优先级高的不一定先执行,需要参照一定条件,GCD只能设置队列的优先级,不能给执行的任务设置优先级。
NSOperation是一个抽象类,在开发中常用的两个子类是NSInvocationOperation和NSBlockOperation。另外如果需要更多需求,我们可以自定义NSOperation,更加灵活。GCD执行任务可以自由组装,没有继承那么高的代码复用度。
3、NSOperation的子类
由于NSOperation是一个抽象类,它本身并不能做任何操作,需要通过它的两个子类来完成一些特定的任务。
- NSInvocationOperation
使用NSInvocationOperation创建任务时代码如下:
// NSInvocationOperation
- (void)nsInvocationOperationTest
{
// NSInvocationOperation添加在当前线程执行的方法
NSInvocationOperation *invOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOperationMethod) object:nil];
// 开始执行
[invOperation start];
}
/ NSInvocationOperation执行的方法
- (void)invocationOperationMethod
{
NSLog(@"currentThread:%@", [NSThread currentThread]);
}
这个方法是添加任务到当前线程,如果没有指定任务执行所在的线程,那么一般该任务是在主线程中执行。
2019-06-22 22:54:20.377189+0800 Thread-Test[10082:146308] currentThread:<NSThread: 0x60000285e680>{number = 1, name = main}
如果想要开辟一个新线程来执行指定的任务,就需要配合NSThread的方法:
[NSThread detachNewThreadSelector:@selector(nsInvocationOperationTest) toTarget:self withObject:nil];
- NSBlockOperation
如果使用NSBlockOperation,创建任务直接在紧邻的Block中执行,不需要创建新的方法:
// NSBlockOperation
- (void)blockOperationTest
{
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"block-current-thread:%@", [NSThread currentThread]);
}];
[blockOperation start];
}
其中从打印结果来看,NSBlockOperation创建的任务也是在当前线程中执行。
2019-06-22 23:04:12.356336+0800 Thread-Test[10184:150775] block-current-thread:<NSThread: 0x600000e9d440>{number = 1, name = main}
不过NSBlockOperation还有另外一个方法,可以追加更多的操作,那就是addExecutionBlock。
// NSBlockOperation
NSBlockOperation *blockOperaton = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"要执行的任务:%@", [NSThread currentThread]);
}];
// 添加多个执行任务
[blockOperaton addExecutionBlock:^{
NSLog(@"新增任务1:%@", [NSThread currentThread]);
}];
[blockOperaton addExecutionBlock:^{
NSLog(@"新增任务2:%@", [NSThread currentThread]);
}];
[blockOperaton addExecutionBlock:^{
NSLog(@"新增任务3:%@", [NSThread currentThread]);
}];
打印结果:
2019-06-22 23:23:40.314257+0800 Thread-Test[10310:156634] 要执行的任务:<NSThread: 0x6000000d0f80>{number = 1, name = main}
2019-06-22 23:23:40.314270+0800 Thread-Test[10310:156667] 新增任务1:<NSThread: 0x600000087600>{number = 4, name = (null)}
2019-06-22 23:23:40.314290+0800 Thread-Test[10310:156673] 新增任务2:<NSThread: 0x60000008d580>{number = 5, name = (null)}
2019-06-22 23:23:40.314306+0800 Thread-Test[10310:156669] 新增任务3:<NSThread: 0x60000008d440>{number = 6, name = (null)}
2019-06-22 23:23:40.314399+0800 Thread-Test[10310:156634] 新增任务4:<NSThread: 0x6000000d0f80>{number = 1, name = main}
从打印结果可以看出,这几个任务并不是都在同一个线程执行的,而且可以看出是并发执行的。至于会通过多少个并发线程来执行这段任务,是由系统来决定的。
- 自定义NSOperation
如果NSOperation的两个子类不能满足我们的开发需求,就可以自定义一个继承于NSOperation的子类,让其在创建时就默认执行某些任务。
自定义NSOperation有两种类型,一种是非并发的,定义比较简单,另一种是并发的,相对步骤要多一点。
先来看一下非并发自定义NSOperation,按照苹果官方的说明,定义做如下步骤:
1、一个自定义的初始化方法(不定义也能实现)
2、重写main函数
JCNonconcurrentOperation.h文件:
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface JCNonconcurrentOperation : NSOperation
- (id)initWithFlag:(NSInteger)flag;
@end
NS_ASSUME_NONNULL_END
JCNonconcurrentOperation.m:
#import "JCNonconcurrentOperation.h"
@interface JCNonconcurrentOperation()
{
NSInteger _flag;
}
@end
@implementation JCNonconcurrentOperation
- (id)initWithFlag:(NSInteger)flag {
if (self = [super init])
{
_flag = flag;
}
return self;
}
-(void)main {
@try {
// 要运行的任务
NSLog(@"falg - %ld-JC-NonconcurentOperation - %@", _flag, [NSThread currentThread]);
}
@catch(...) {
// Do not rethrow exceptions.
}
}
@end
在这里为了便于比较输出结果,我们在初始化的地方定义传递一个flag的参数,在其他地方调用这个非并发的自定义NSOperation时按照如下代码:
// 自定义非并发NSOperation测试方法
- (void)nonconcurrentOperationTest
{
// JCNonconcurrentOperation *nonConcurrentOp = [[JCNonconcurrentOperation alloc] initWithData:@""];
// [nonConcurrentOp start];
JCNonconcurrentOperation *nonConcurrentOp = [[JCNonconcurrentOperation alloc] initWithFlag:0];
[nonConcurrentOp start];
NSLog(@"任务0执行完");
JCNonconcurrentOperation *nonConcurrentOp1 = [[JCNonconcurrentOperation alloc] initWithFlag:1];
[nonConcurrentOp1 start];
NSLog(@"任务1执行完");
JCNonconcurrentOperation *nonConcurrentOp2 = [[JCNonconcurrentOperation alloc] initWithFlag:2];
[nonConcurrentOp2 start];
NSLog(@"任务2执行完");
}
打印结果:
2019-06-23 14:43:57.171927+0800 Thread-Test[15078:331719] falg - 0-JC-NonconcurentOperation - <NSThread: 0x600002d1e900>{number = 1, name = main}
2019-06-23 14:43:57.172135+0800 Thread-Test[15078:331719] 任务0执行完
2019-06-23 14:43:57.172311+0800 Thread-Test[15078:331719] falg - 1-JC-NonconcurentOperation - <NSThread: 0x600002d1e900>{number = 1, name = main}
2019-06-23 14:43:57.172428+0800 Thread-Test[15078:331719] 任务1执行完
2019-06-23 14:43:57.172551+0800 Thread-Test[15078:331719] falg - 2-JC-NonconcurentOperation - <NSThread: 0x600002d1e900>{number = 1, name = main}
2019-06-23 14:43:57.172657+0800 Thread-Test[15078:331719] 任务2执行完
输出结果可以看出三个任务是串行的,一个执行完了才会执行下一个。
再来看一下怎样自定义一个并发的NSOperation,同样参照苹果官方文档可知,实现的步骤如下:
1、必须重写start方法
2、重写了start方法后,main方法重写可选,但是一般在自定义时,还是习惯把需要执行的任务放在main函数中。
3、必须手动管理isExecuting和isFinished状态,主要作用是在线程状态改变时,产生适当的KVO通知。
4、 重写isConcurrent的get方法,并返回YES。
我们手动再创建一个继承自NSOperation的类。
在JCConcurrentOperation.h中:
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface JCConcurrentOperation : NSOperation {
BOOL executing;
BOOL finished;
}
- (id)initWithFlag:(NSInteger)flag;
- (void)completeOperation;
@end
NS_ASSUME_NONNULL_END
在JCConcurrentOperation.m中:
#import "JCConcurrentOperation.h"
@interface JCConcurrentOperation()
{
NSInteger _flag;
}
@end
@implementation JCConcurrentOperation
- (id)initWithFlag:(NSInteger)flag {
if(self = [super init])
{
_flag = flag;
executing = NO;
finished = NO;
}
return self;
}
- (BOOL)isConcurrent {
return YES;
}
- (BOOL)isExecuting {
return executing;
}
- (BOOL)isFinished {
return finished;
}
- (void)start {
// 第一步就要检测是否被取消了,如果取消了,要实现相应的KVO
if ([self isCancelled])
{
// 如果任务取消了,要通过KVO设置isFinished
[self willChangeValueForKey:@"isFinished"];
finished = YES;
[self didChangeValueForKey:@"isFinished"];
return;
}
// 如果任务没有取消,需要设置isExecuting,表示正在执行
[self willChangeValueForKey:@"isExecuting"];
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
executing = YES;
[self didChangeValueForKey:@"isExecuting"];
}
- (void)main {
@try {
@autoreleasepool {
//在这里定义自己的并发任务
NSThread *thread = [NSThread currentThread];
NSLog(@"flag - %ld自定义并发操作NSOperation - %@", _flag, thread);
// 任务执行完成后要实现相应的KVO
[self completeOperation];
}
} @catch (NSException *exception) {
} @finally {
}
}
- (void)completeOperation {
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
executing = NO;
finished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
@end
来调用这个自定义的并发NSOperation:
JCConcurrentOperation *concurrentop0 = [[JCConcurrentOperation alloc] initWithFlag:0];
[concurrentop0 start];
NSLog(@"con-任务1完成");
JCConcurrentOperation *concurrentOp1 = [[JCConcurrentOperation alloc] initWithFlag:1];
[concurrentOp1 start];
NSLog(@"con-任务2完成");
JCConcurrentOperation *concurrentOp2 = [[JCConcurrentOperation alloc] initWithFlag:2];
[concurrentOp2 start];
NSLog(@"con-任务3完成");
打印结果:
2019-06-23 15:08:08.716065+0800 Thread-Test[15254:340594] con-任务1完成
2019-06-23 15:08:08.716562+0800 Thread-Test[15254:340594] con-任务2完成
2019-06-23 15:08:08.716773+0800 Thread-Test[15254:340662] flag - 1自定义并发操作NSOperation - <NSThread: 0x600002629b80>{number = 4, name = (null)}
2019-06-23 15:08:08.716682+0800 Thread-Test[15254:340661] flag - 0自定义并发操作NSOperation - <NSThread: 0x600002629b00>{number = 3, name = (null)}
2019-06-23 15:08:08.716832+0800 Thread-Test[15254:340594] con-任务3完成
2019-06-23 15:08:08.719572+0800 Thread-Test[15254:340663] flag - 2自定义并发操作NSOperation - <NSThread: 0x60000260f000>{number = 5, name = (null)}
比较上面两种自定义的方式输出的结果就能很明显的看出任务执行是并发还是非并发了。
4、NSOperationQueue创建队列
与GCD类似,很多时候我们使用NSOperation的时候,其中的任务也是会放在队列中执行的,而NSOperation中的队列就是由NSOperationQueue来管理和创建的。
NSOperationQueue有两种队列分类,一个是主队列,另外一个是除了主队列的自定义队列。
- 主队列的创建方式:
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
创建几个任务添加到主队列中:
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
NSInvocationOperation *task1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(mainQueueTaks1) object:nil];
NSInvocationOperation *task2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(mainQueueTaks2) object:nil];
NSBlockOperation *task3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"mainQueueTask3 - %@", [NSThread currentThread]);
}];
NSInvocationOperation *task4 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(mainQueueTaks4) object:nil];
[mainQueue addOperation:task1];
[mainQueue addOperation:task2];
[mainQueue addOperation:task3];
[mainQueue addOperation:task4];
- (void)mainQueueTaks1
{
NSLog(@"mainQueueTask1 - %@", [NSThread currentThread]);
}
- (void)mainQueueTaks2
{
NSLog(@"mainQueueTask2 - %@", [NSThread currentThread]);
}
- (void)mainQueueTaks4
{
NSLog(@"mainQueueTask4 - %@", [NSThread currentThread]);
}
打印结果:
2019-06-23 15:29:32.737797+0800 Thread-Test[15450:349967] mainQueueTask1 - <NSThread: 0x600003e75440>{number = 1, name = main}
2019-06-23 15:29:32.738290+0800 Thread-Test[15450:349967] mainQueueTask2 - <NSThread: 0x600003e75440>{number = 1, name = main}
2019-06-23 15:29:32.738491+0800 Thread-Test[15450:349967] mainQueueTask3 - <NSThread: 0x600003e75440>{number = 1, name = main}
2019-06-23 15:29:32.738714+0800 Thread-Test[15450:349967] mainQueueTask4 - <NSThread: 0x600003e75440>{number = 1, name = main}
可以看出四个任务不管是用NSInvocationOperation还是NSBlockOperation创建,同时添加到主队列时,是串行执行每个任务的。
这里有一种情况使用时要留心,就是NSBlockOperation的addExecutionBlock使用时,很容易产生幻觉造成错误的结果。
举个例子:
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
NSBlockOperation *blockTask1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"mainQueueBlockTask1 - %@", [NSThread currentThread]);
}];
[blockTask1 addExecutionBlock:^{
NSLog(@"mainQueueBlockTask1v1 - %@", [NSThread currentThread]);
}];
NSBlockOperation *blockTask2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"mainQueueBlockTask2 - %@", [NSThread currentThread]);
}];
NSBlockOperation *blockTask3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"mainQueueBlockTask3 - %@", [NSThread currentThread]);
}];
[mainQueue addOperation:blockTask1];
[mainQueue addOperation:blockTask2];
[mainQueue addOperation:blockTask3];
在task1上又通过addExecutionBlock添加一个任务,而此时mainQueueBlockTask1v1的任务行并不会在主线程中执行,而是自己新开了一个线程。
2019-06-23 15:33:45.917731+0800 Thread-Test[15491:351783] mainQueueBlockTask1 - <NSThread: 0x6000033ee900>{number = 1, name = main}
2019-06-23 15:33:45.917735+0800 Thread-Test[15491:351829] mainQueueBlockTask1v1 - <NSThread: 0x60000339ca80>{number = 3, name = (null)}
2019-06-23 15:33:45.918266+0800 Thread-Test[15491:351783] mainQueueBlockTask2 - <NSThread: 0x6000033ee900>{number = 1, name = main}
2019-06-23 15:33:45.918482+0800 Thread-Test[15491:351783] mainQueueBlockTask3 - <NSThread: 0x6000033ee900>{number = 1, name = main}
- 其他队列创建方法
其他通过init方法创建的队列都是自定义队列:
NSOperationQueue *otherQueue = [[NSOperationQueue alloc] init];
任务添加到队列的方法与主队列的操作一样。
- 更多方法添加队列
NSOperationQueue还有两个添加任务的方法,一个是一次性添加多个任务的方法,一个是直接通过Block添加任务。
// 一次性添加所有任务到队列中
[otherQueue addOperations:@[task1, task2] waitUntilFinished:YES];
// 通过block直接创建任务添加到队列中
[otherQueue addOperationWithBlock:^{
}];
5、NSOperationQueue的串行和并行实现
上面讲到其他队列的创建方法时,与主队列有一点不一样的是,如何实现其他队列的串行和并行呢?实际上在自定义NSOperation中已经实现过并发和非并发,但是有没有更快速的方法呢?答案是有。
NSOperationQueue提供了一个比较重要的属性maxConcurrentOperationCount,即最大的并发操作数,说明清楚这里是在一个队列中最多能同时执行的任务数,并不是只能开一个线程这个说法。那么对于这个属性有如下几种设置:
默认情况maxConcurrentOperationCount 为-1,表示不进行限制,可进行并发执行。
设置maxConcurrentOperationCount 为1时,队列为串行队列,只能串行执行。
设置maxConcurrentOperationCount 大于1时,队列为并发队列。
6、NSOperationQueue添加依赖
文章一开始就讲到了NSOperationQueue可以直接给不同的任务间添加依赖,从而实现很多常见的需求。
NSOperation中针对依赖关系,提供了三个接口:
(void)addDependency:(NSOperation *)op,这个方法用来添加依赖,当前的操作要依赖操作op,即op完成了才执行当前操作。
(void)removeDependency:(NSOperation *)op,这个方法是移除依赖,当前操作不再依赖操作op。
@property (readonly, copy) NSArray<NSOperation *> *dependencies,这个属性表示在当前操作开始执行之前完成执行的所有操作对象数组。
来实现几个任务:
NSOperationQueue *otherQueue = [[NSOperationQueue alloc] init];
NSBlockOperation *task1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"task1 - %@", [NSThread currentThread]);
}];
NSBlockOperation *task2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"task2 - %@", [NSThread currentThread]);
}];
NSBlockOperation *task3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"task3 - %@", [NSThread currentThread]);
}];
NSBlockOperation *task4 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"task4 - %@", [NSThread currentThread]);
}];
// 设置最大并发数
otherQueue.maxConcurrentOperationCount = 1;
// 任务1依赖任务2
[task1 addDependency:task2];
[otherQueue addOperation:task1];
[otherQueue addOperation:task2];
[otherQueue addOperation:task3];
[otherQueue addOperation:task4];
这里设置了最大并发数为1,表示四个任务将会串联执行,又设置了task1依赖于task2,所以task1会在task2后面执行。
2019-06-23 16:01:33.737829+0800 Thread-Test[15745:363410] task2 - <NSThread: 0x6000002daac0>{number = 3, name = (null)}
2019-06-23 16:01:33.738614+0800 Thread-Test[15745:363409] task1 - <NSThread: 0x6000002daa40>{number = 4, name = (null)}
2019-06-23 16:01:33.740211+0800 Thread-Test[15745:363410] task3 - <NSThread: 0x6000002daac0>{number = 3, name = (null)}
2019-06-23 16:01:33.741006+0800 Thread-Test[15745:363409] task4 - <NSThread: 0x6000002daa40>{number = 4, name = (null)}
注意,添加依赖的代码要放在任务添加到队列前面执行。
7、NSOperation的优先级设置
NSOperation中提供了一个设置任务优先级的属性queuePriority,从而可以调整任务执行的顺序。个人觉得这个属性名有点误导人,字面意思是队列优先级,实际反映的是添加到这个队列上的任务的优先级。
来看一下苹果官方文档对这个属性的说明:
从这段说明中可以得到如下信息
- 设置优先级的对象是任务(operations)。
- 任务执行的一般顺序是代码读取的顺序,当然这里并不是一定的,除非是设置了依赖关系。
- 依赖的优先级比设置优先级的顺序更靠前,即任务的执行顺序先看依赖关系,再看优先级
- 任务的默认优先级是normal级别的
NSOperation的优先级级别主要有下面几种:
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L, // 很低级别
NSOperationQueuePriorityLow = -4L, // 较低级别
NSOperationQueuePriorityNormal = 0, // 默认级别
NSOperationQueuePriorityHigh = 4, // 较高级别
NSOperationQueuePriorityVeryHigh = 8 // 很高级别
};
官方文档也强调了,任务的优先级是针对当前同一个队列而言的,如果是在两个不同的队列之间,则有可能优先级低的要比优先级高的优先执行。
另外,即使是在同一个队列中,不同任务设置了优先级时,也还要看这多个任务是否都处在就绪的状态(ready for execute),所以这个优先级又是在任务都处在了就绪状态的基础上的。
参考如下这篇文章:
iOS 多线程:『NSOperation、NSOperationQueue』详尽总结
当一个操作的所有依赖都已经完成时,操作对象通常会进入准备就绪状态,等待执行。如果任务没有添加过依赖,那么当他添加到队列中那一刻就是出于就绪状态了。
所以,综上所述,任务间的优先级遵循如下规则:
- 优先级是针对同一个队列中的任务
- 优先级是在就绪状态任务的基础上进行比较
- 优先级的优先比依赖关系的优先级低,即先比较依赖关系
来看一下优先级设置的代码,还是上面那段代码,但是把task4的优先级提高:
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
NSBlockOperation *blockTask1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"mainQueueBlockTask1 - %@", [NSThread currentThread]);
}];
// [blockTask1 addExecutionBlock:^{
// NSLog(@"mainQueueBlockTask1v1 - %@", [NSThread currentThread]);
// }];
NSBlockOperation *blockTask2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"mainQueueBlockTask2 - %@", [NSThread currentThread]);
}];
NSBlockOperation *blockTask3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"mainQueueBlockTask3 - %@", [NSThread currentThread]);
}];
NSBlockOperation *blockTask4 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"mainQueueBlockTask4 - %@", [NSThread currentThread]);
}];
[blockTask4 setQueuePriority:NSOperationQueuePriorityVeryHigh];
[mainQueue addOperation:blockTask1];
[mainQueue addOperation:blockTask2];
[mainQueue addOperation:blockTask3];
[mainQueue addOperation:blockTask4];
但是打印结果有点出人意料,目前还没有想明白,留个疑问吧。按照正常理解,这里没有设置任何依赖关系,提高了task4的优先级,应该task4最先执行才对,但是不管是提高task4,还是降低task1的优先级,这个task1都在task4前面执行。。。
打印结果:
2019-06-23 21:15:54.390891+0800 Thread-Test[16903:411016] mainQueueBlockTask1 - <NSThread: 0x600003ce8040>{number = 1, name = main}
2019-06-23 21:15:54.391317+0800 Thread-Test[16903:411016] mainQueueBlockTask4 - <NSThread: 0x600003ce8040>{number = 1, name = main}
2019-06-23 21:15:54.391483+0800 Thread-Test[16903:411016] mainQueueBlockTask2 - <NSThread: 0x600003ce8040>{number = 1, name = main}
2019-06-23 21:15:54.391681+0800 Thread-Test[16903:411016] mainQueueBlockTask3 - <NSThread: 0x600003ce8040>{number = 1, name = main}
如上就是NSOperation和NSOperationQueue相关的内容,关于线程安全我觉得需要用一个篇幅来详细讲解,这里先不做说明。