级别: ★★☆☆☆
标签:「iOS」「多线程」「NSOperation」
做者: dac_1033
审校: QiShare团队php
上一篇介绍了NSThread,本篇将介绍“iOS多线程之NSOperation”。git
多线程处理任务的过程当中,频繁的建立/销毁线程会很大程度上影响处理效率,新起的线程数过多会下降系统性能甚至引发app崩溃。在Java和C#开发过程当中可使用线程池来解决这些问题,线程池缓存一些线程,在接到任务的时候,系统就在线程池中调度一个闲置的线程来处理这个任务,免去了频繁建立/销毁的过程。从NSOperation的使用过程就能体会到,它和线程池很是相似,下面咱们就来介绍一下NSOperation的使用。github
NSOperation是一个抽象类,实际开发中须要使用其子类NSInvocationOperation、NSBlockOperation。首先建立一个NSOperationQueue,再建多个NSOperation实例(设置好要处理的任务、operation的属性和依赖关系等),而后再将这些operation放到这个queue中,线程就会被依次启动。苹果官网对于NSOperation的介绍 NSOperation及其子类中的经常使用方法以下:缓存
//// NSOperation
@property (readonly, getter=isCancelled) BOOL cancelled;
@property (readonly, getter=isExecuting) BOOL executing;
@property (readonly, getter=isFinished) BOOL finished;
@property (readonly, getter=isReady) BOOL ready;
@property NSOperationQueuePriority queuePriority;
@property (readonly, copy) NSArray<NSOperation *> *dependencies;
@property (nullable, copy) NSString *name;
@property (nullable, copy) void (^completionBlock)(void);
- (void)start;
- (void)main;
- (void)cancel;
- (void)addDependency:(NSOperation *)op;
- (void)removeDependency:(NSOperation *)op;
- (void)waitUntilFinished;
复制代码
下面咱们依次介绍NSInvocationOperation、NSBlockOperation的使用过程,并自定义一个继承于NSOperation的子类并实现内部相应的方法。安全
NSInvocationOperation继承于NSOperation,NSInvocationOperation的定义以下:bash
@interface NSInvocationOperation : NSOperation {
@private
id _inv;
id _exception;
void *_reserved2;
}
- (nullable instancetype)initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg;
- (instancetype)initWithInvocation:(NSInvocation *)inv NS_DESIGNATED_INITIALIZER;property (readonly, retain) NSInvocation *invocation;
@property (nullable, readonly, retain) id result;
@end
复制代码
下面使用NSInvocationOperation来加载一张图片,示例方法以下:微信
- (void)loadImageWithMultiThread {
/*建立一个调用操做
object:调用方法参数
*/
NSInvocationOperation *invocationOperation=[[NSInvocationOperation alloc]initWithTarget:self selector:@selector(loadImage) object:nil];
//建立完NSInvocationOperation对象并不会调用,它由一个start方法启动操做,可是注意若是直接调用start方法,则此操做会在主线程中调用,通常不会这么操做,而是添加到NSOperationQueue中
// [invocationOperation start];
//建立操做队列
NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init];
//注意添加到操做队后,队列会开启一个线程执行此操做
[operationQueue addOperation:invocationOperation];
}
复制代码
NSBlockOperation继承于NSOperation,NSBlockOperation的定义以下:网络
@interface NSBlockOperation : NSOperation {
@private
id _private2;
void *_reserved2;
}
+ (instancetype)blockOperationWithBlock:(void (^)(void))block;
- (void)addExecutionBlock:(void (^)(void))block;
@property (readonly, copy) NSArray<void (^)(void)> *executionBlocks;
@end
复制代码
下面咱们来使用NSOperation,实现多个线程加载图片,示例代码以下:多线程
//// 首先 定义一个OperationImage的Model
@interface OperationImage : NSObject
@property (nonatomic, assign) NSInteger index;
@property (nonatomic, strong) NSData *imgData;
@end
@implementation OperationImage
@end
//// 使用NSOperation实现多线程加载图片
#define ColumnCount 4
#define RowCount 5
#define Margin 10
@interface MultiThread_NSOperation1 ()
@property (nonatomic, strong) NSMutableArray *imageViews;
@end
@implementation MultiThread_NSOperation1
- (void)viewDidLoad {
[super viewDidLoad];
[self setTitle:@"NSOperation1"];
[self.view setBackgroundColor:[UIColor whiteColor]];
self.edgesForExtendedLayout = UIRectEdgeNone;
[self layoutViews];
}
- (void)layoutViews {
CGSize size = self.view.frame.size;
CGFloat imgWidth = (size.width - Margin * (ColumnCount + 1)) / ColumnCount;
_imageViews=[NSMutableArray array];
for (int row=0; row<RowCount; row++) {
for (int colomn=0; colomn<ColumnCount; colomn++) {
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(Margin + colomn * (imgWidth + Margin), Margin + row * (imgWidth + Margin), imgWidth, imgWidth)];
imageView.backgroundColor = [UIColor cyanColor];
[self.view addSubview:imageView];
[_imageViews addObject:imageView];
}
}
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
button.frame = CGRectMake(15, (imgWidth + Margin) * RowCount + Margin, size.width - 15 * 2, 45);
[button addTarget:self action:@selector(loadImageWithMultiOperation) forControlEvents:UIControlEventTouchUpInside];
[button setTitle:@"加载图片" forState:UIControlStateNormal];
[self.view addSubview:button];
}
#pragma mark - 多线程下载图片
- (void)loadImageWithMultiOperation {
int count = RowCount * ColumnCount;
NSOperationQueue *operationQueue = [[NSOperationQueue alloc]init];
operationQueue.maxConcurrentOperationCount = 5;
NSBlockOperation *tempOperation = nil;
for (int i=0; i<count; ++i) {
OperationImage *operationImg = [[OperationImage alloc] init];
operationImg.index = i;
////1.直接使用操队列添加操做
//[operationQueue addOperationWithBlock:^{
// [self loadImg:operationImg];
//}];
////2.建立操做块添加到队列
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
[self loadImg:operationImg];
}];
if (i > 0) {// 设置依赖
[blockOperation addDependency:tempOperation];
}
[operationQueue addOperation:blockOperation];
tempOperation = blockOperation;
}
}
#pragma mark - 将图片显示到界面
-(void)updateImage:(OperationImage *)operationImg {
UIImage *image = [UIImage imageWithData:operationImg.imgData];
UIImageView *imageView = _imageViews[operationImg.index];
imageView.image = image;
}
#pragma mark - 请求图片数据
- (NSData *)requestData {
NSURL *url = [NSURL URLWithString:@"https://store.storeimages.cdn-apple.com/8756/as-images.apple.com/is/image/AppleInc/aos/published/images/a/pp/apple/products/apple-products-section1-one-holiday-201811?wid=2560&hei=1046&fmt=jpeg&qlt=95&op_usm=0.5,0.5&.v=1540576114151"];
NSData *data = [NSData dataWithContentsOfURL:url];
return data;
}
#pragma mark - 加载图片
- (void)loadImg:(OperationImage *)operationImg {
// 请求数据
operationImg.imgData = [self requestData];
// 更新UI界面(mainQueue是UI主线程)
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self updateImage:operationImg];
}];
// 打印当前线程
NSLog(@"current thread: %@", [NSThread currentThread]);
}
@end
复制代码
在加载网络图片的代码上打一个断点,查看断点信息,从的运行过程能够看出NSOperation底层涉及到对GCD的封装: 并发
咱们用到的不少三方库都自定义封装NSOperation,如MKNetworkOperation、SDWebImage等。自定义封装抽象类NSOperation只须要重写其中的main或start方法,在多线程执行任务的过程当中须要注意线程安全问题,咱们还能够经过KVO监听isCancelled、isExecuting、isFinished等属性,确切的回调当前任务的状态。下面就是对NSOperation的自定义封装代码:
@interface MyOperation ()
//要下载图片的地址
@property (nonatomic, copy) NSString *urlString;
//执行完成后,回调的block
@property (nonatomic, copy) void (^finishedBlock)(NSData *data);
// 自定义变量,用于重写父类isFinished的set、get方法
@property (nonatomic, assign) BOOL taskFinished;
@end
@implementation MyOperation
+ (instancetype)downloadDataWithUrlString:(NSString *)urlString finishedBlock:(void (^)(NSData *data))finishedBlock {
MyOperation *operation = [[MyOperation alloc] init];
operation.urlString = urlString;
operation.finishedBlock = finishedBlock;
return operation;
}
// 监听/重写readonly属性的set、get方法
- (void)setTaskFinished:(BOOL)taskFinished {
[self willChangeValueForKey:@"isFinished"];
_taskFinished = taskFinished;
[self didChangeValueForKey:@"isFinished"];
}
- (BOOL)isFinished {
return self.taskFinished;
}
//- (void)main {
//
// // 打印当前线程
// NSLog(@"%@", [NSThread currentThread]);
//
// //判断是否被取消,取消正在执行的操做
// if (self.cancelled) {
// return;
// }
//
// NSURLSessionTask *task = [NSURLSession.sharedSession dataTaskWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:self.urlString]] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
// //回到主线程更新UI
//
// [[NSOperationQueue mainQueue] addOperationWithBlock:^{
// self.finishedBlock(data);
// }];
// }];
// [task resume];
//}
- (void)start {
// 打印当前线程
NSLog(@"%@", [NSThread currentThread]);
//判断是否被取消,取消正在执行的操做
if (self.cancelled) {
return;
}
NSURLSessionTask *task = [NSURLSession.sharedSession dataTaskWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:self.urlString]] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
//回到主线程更新UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.finishedBlock(data);
}];
self.taskFinished = YES;
}];
[task resume];
}
@end
复制代码
调用MyOperation中的方法:
- (void)testMyOperation {
_queue = [[NSOperationQueue alloc] init];
_queue.maxConcurrentOperationCount = 3;
MyOperation *temp = nil;
for (NSInteger i=0; i<500; i++) {
MyOperation *operation = [MyOperation downloadDataWithUrlString:@"https://www.so.com" finishedBlock:^(NSData * _Nonnull data) {
NSLog(@"--- %d finished---", (int)i);
}];
if (temp) {
[operation addDependency:temp];
}
temp = operation;
[_queue addOperation:operation];
}
}
复制代码
说明:
- 在运行上面的代码时,咱们发现同时重写start和main方法时,start方法优先执行,main方法不会被执行;若是只重写main方法,则main方法会被执行。
- 由于isFinished是readonly属性,所以咱们经过自定义变量taskFinished来重写isFinished的set、get方法,实现方式详见代码。
- 若是只重写start方法,而且其中没有self.taskFinished = YES时,且在testMyOperation设置以下:
能够看到log只能能打出来执行了5次(正好是maxConcurrentOperationCount的值),以后便卡死不动。若是不设置maxConcurrentOperationCount或将maxConcurrentOperationCount设置的足够大,则可正常执行至结束。若是打开start方法中的self.taskFinished = YES,则也可正常执行至结束。可见start方法中的任务执行结束后,系统并无将线程的isFinished置为YES,致使以后的任务没法对其重用。 ![]()
- 若是只重写main方法,而且其中没有self.taskFinished = YES时,testMyOperation方法都是能够正常执行的,也就是说main执行结束时系统将线程的isFinished置为YES了,其他任务可对其重用。
- 比较start与main方法,两个方法的执行过程都是并行的;start方法更容易经过KVO监听到任务的执行状态,可是须要手动设置一些状态;main自动化程度更高。
- 使用NSOperationQueue时,咱们打印代码执行,过程当中的线程,发现线程池中线程的最大个数在66个左右。
以上验证过程,获得了昆哥的指教,很是感谢!🙂
用NSThread来实现多线程时,线程间的执行顺序很难控制,可是使用NSOperation时能够经过设置操做的依赖关系来控制执行顺序。假设操做A依赖于操做B,线程操做队列在启动线程时就会首先执行B操做,而后执行A。例如在第三节testMyOperation方法中,咱们从第二个任务一次设置了关系:
MyOperation *temp = nil;
for (NSInteger i=0; i<500; i++) {
MyOperation *operation = [MyOperation downloadDataWithUrlString:@"https://www.so.com" finishedBlock:^(NSData * _Nonnull data) {
NSLog(@"--- %d finished---", (int)i);
}];
if (temp) {
[operation addDependency:temp];
}
temp = operation;
[_queue addOperation:operation];
}
复制代码
PS:
- NSOperationQueue的maxConcurrentOperationCount通常设置在5个之内,数量过多可能会有性能问题。maxConcurrentOperationCount为1时,队列中的任务串行执行,maxConcurrentOperationCount大于1时,队列中的任务并发执行;
- 不一样的NSOperation实例之间能够设置依赖关系,不一样queue的NSOperation之间也能够建立依赖关系 ,可是要注意不要“循环依赖”;
- NSOperation实例之间设置依赖关系应该在加入队列以前;
- 在没有使用 NSOperationQueue时,在主线程中单独使用 NSBlockOperation 执行(start)一个操做的状况下,操做是在当前线程执行的,并无开启新线程,在其余线程中也同样;
- NSOperationQueue能够直接获取mainQueue,更新界面UI应该在mainQueue中进行;
- 区别自定义封装NSOperation时,重写main或start方法的不一样;
- 自定义封装NSOperation时须要咱们彻底重载start,在start方法里面,咱们还要查看isCanceled属性,确保start一个operation前,task是没有被取消的。若是咱们自定义了dependency,咱们还须要发送isReady的KVO通知。
小编微信:可加并拉入《QiShare技术交流群》。
关注咱们的途径有:
QiShare(简书)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公众号)
推荐文章:
iOS Winding Rules 缠绕规则
iOS 签名机制
iOS 扫描二维码/条形码
iOS 了解Xcode Bitcode
iOS 重绘之drawRect
奇舞周刊