級別: ★★☆☆☆
標籤:「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
奇舞週刊