Painted painted levels: fostered fostered fostered

Tags: “iOS” “multi-threading” “NSOperation” author: Dac_1033 Review: QiShare team

The last article introduced NSThreads. This article will introduce NSOperation for iOS Multithreading.

Thread pools

In the process of multi-threaded task processing, the frequent creation/destruction of threads will greatly affect the processing efficiency, and the excessive number of new threads will reduce the system performance and even cause app crash. Thread pools can be used in Java and C# development to solve these problems. Thread pools cache threads, and when a task is received, the system dispatches an idle thread from the pool to handle the task, eliminating the need for frequent creation/destruction. NSOperation is very similar to a thread pool. Let’s take a look at the use of NSOperation.

Ii. Introduction to NSOperation

NSOperation is an abstract class, and its subclasses NSInvocationOperation and NSBlockOperation need to be used in actual development. Create an NSOperationQueue, then create multiple instances of NSOperation (set up the tasks to be processed, operation properties, dependencies, and so on), then place these operations in the queue, and the threads will be started one by one. The common methods of NSOperation and its subclasses are as follows:

//// 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;
Copy the code

Next, we introduce the process of using NSInvocationOperation and NSBlockOperation, and define a subclass inheriting from NSOperation and implement the corresponding internal methods.

2.1 NSInvocationOperation

NSInvocationOperation inherits from NSOperation. NSInvocationOperation is defined as follows:

@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; @endCopy the code

To load an image, use NSInvocationOperation as an example:

- (void)loadImageWithMultiThread {/* Create a call operation object: call method argument */ NSInvocationOperation *invocationOperation=[[NSInvocationOperation alloc]initWithTarget:self selector:@selector(loadImage) object:nil]; // The NSInvocationOperation object is not called after it is created. It is started by a start method, but note that if the start method is called directly, the operation will be called in the main thread. Instead of doing this, add it to NSOperationQueue // [invocationOperation Start]; NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init]; / / note added to the operation team, the queue will open a thread to perform this operation [operationQueue addOperation: invocationOperation]; }Copy the code
2.2 NSBlockOperation

NSBlockOperation is inherited from NSOperation. NSBlockOperation is defined as follows:

@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
Copy the code

Let’s use NSOperation to load images from multiple threads. Example code is as follows:

//// First define a model@interface OperationImage OperationImage: nsobject@property (nonatomic, assign) NSInteger index; @property (nonatomic, strong) NSData *imgData; @end @implementation OperationImage@end //// implement multithreaded loading image with 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: @ "the image" forState: UIControlStateNormal]; [self.view addSubview:button]; } # pragma mark - multi-threaded download image - (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; //[operationQueue addOperationWithBlock:^{//[self loadImg:operationImg]; //}]; ////2. Create an operation block and add it to the queue NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{[self loadImg:operationImg]; }]; If (I > 0) {/ / set dependent on [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 *url 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 = 15405 76114151 "]; NSData *data = [NSData dataWithContentsOfURL:url]; return data; } #pragma mark - load image - (void)loadImg:(OperationImage *)operationImg {// request data operationimg.imgdata = [self requestData]; [[NSOperationQueue mainQueue] addOperationWithBlock:^{[self updateImage:operationImg];}]; // Print the current thread NSLog(@"current thread: %@", [NSThread currentThread]); } @endCopy the code

Break a breakpoint on the code of loading network pictures, check the breakpoint information, from the running process of NSOperation, we can see that the bottom layer of NSOperation involves the encapsulation of GCD:

3. About user-defined encapsulation NSOperation

Many of the tripartite libraries we use have custom encapsulated NSOperation, such as MKNetworkOperation and SDWebImage. To customize NSOperation, you only need to rewrite the main or start method. In the process of executing a multi-threaded task, you need to pay attention to thread safety. You can also listen isCancelled, isExecuting, isFinished and other properties through KVO. The exact callback to the current task status. Here is the custom wrapper for NSOperation:

@interface MyOperation () @property (nonatomic, copy) NSString *urlString; // Block @property (nonatomic, copy) void (^finishedBlock)(NSData *data); @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; Void setTaskFinished:(BOOL)taskFinished {[self willChangeValueForKey:@"isFinished"]; _taskFinished = taskFinished; [self didChangeValueForKey:@"isFinished"]; } - (BOOL)isFinished { return self.taskFinished; } / / - (void) the main {/ / / / / / / / print the current thread NSLog (@ "% @", [NSThread currentThread]); // // // Check whether the cancelled operation is cancelled // 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) {// // back to main thread update UI // // [[NSOperationQueue mainQueue] addOperationWithBlock:^{// self.finishedBlock(data); // }]; // }]; // [task resume]; } - (void)start {// print the currentThread NSLog(@"%@", [NSThread currentThread]); // Check whether the operation is cancelled, cancelled if (self. } NSURLSessionTask *task = [NSURLSession.sharedSession dataTaskWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:self.urlString]] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {// Back to main thread update UI [[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.finishedBlock(data); }]; self.taskFinished = YES; }]; [task resume]; } @endCopy the code

Call the method in 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]; }}Copy the code

Description:

  1. When we run the above code, we find that when we overwrite both the start and main methods, the start method takes precedence over the main method. If only the main method is overridden, the main method is executed.
  2. Because isFinished is a readonly property, we use taskFinished to override the set and GET methods of isFinished. For details, see the code.
  3. If only the start method is overridden and there is no self.taskFinished = YES, and testMyOperation is set as follows:

You can see the log can only play performed 5 times (exactly maxConcurrentOperationCount value), then card to death. If you don’t set maxConcurrentOperationCount or set maxConcurrentOperationCount is large enough, can perform well to the end. If self.taskFinished = YES is opened in the start method, the execution will continue to the end. After the tasks in the start method are executed, the system does not set isFinished to YES for the thread. As a result, the thread cannot be reused for subsequent tasks.

  1. If the main method is overridden and there is no self.taskFinished = YES, the testMyOperation method will execute normally. If the main method is overridden and there is no self.taskFinished = YES, the testMyOperation method will execute normally.
  2. Compare the start and main methods. Both methods execute in parallel. The start method is easier to listen to the execution status of a task through KVO, but some states need to be set manually. Main is more automated.
  3. With NSOperationQueue, we print out the threads in the process of code execution, and we find that the maximum number of threads in the thread pool is around 66.

The above verification process has been instructed by Kunge. Thank you very much! 🙂

4. Dependency in NSOperation

When using NSThreads to implement multi-threading, it is difficult to control the order of execution between threads. However, when using NSOperation, you can control the order of execution by setting the dependency of operations. Assuming that operation A depends on operation B, the thread action queue will perform operation B first and then operation A when the thread is started. For example, in the testMyOperation method in Section 3, we set the relationship from the second task at once:

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];
    }
Copy the code

PS:

  1. NSOperationQueue maxConcurrentOperationCount general Settings within five (5), there may be performance too many problems. MaxConcurrentOperationCount to 1, the queue serial task execution, maxConcurrentOperationCount is greater than 1, the queue task concurrently;
  2. You can set dependencies between different NSOperation instances and between nsOperations of different queues, but be careful not to “loop dependency”.
  3. Dependencies between NSOperation instances should be set before enqueuing;
  4. In the case that an operation is performed (start) in the main thread using NSBlockOperation alone when NSOperationQueue is not used, the operation is performed in the current thread and no new thread is started, as well as in other threads;
  5. NSOperationQueue can get mainQueue directly, update UI should be done in mainQueue;
  6. The difference between overriding main or start methods when encapsulating NSOperation is customized.
  7. To encapsulate NSOperation, we need to override start completely. In the start method, we also need to check the isCanceled attribute to make sure that the task is not canceled before starting an operation. If we customize a dependency, we also need to send an isReady KVO notification.

Project source GitHub address


Recommended articles:

IOS Winding Rules iOS signature mechanism iOS Scan qr code/bar code iOS learn about Xcode Bitcode iOS Redraw drawRect odd dance weekly