iOS 多線程之 NSOperation

1 線程池

多線程處理任務(wù)的過(guò)程中,頻繁的創(chuàng)建/銷毀線程會(huì)很大程度上影響處理效率,新起的線程數(shù)過(guò)多會(huì)降低系統(tǒng)性能甚至引起app崩潰。在Java和C#開發(fā)過(guò)程中可以使用線程池來(lái)解決這些問題,線程池緩存一些線程,在接到任務(wù)的時(shí)候,系統(tǒng)就在線程池中調(diào)度一個(gè)閑置的線程來(lái)處理這個(gè)任務(wù),免去了頻繁創(chuàng)建/銷毀的過(guò)程。從NSOperation的使用過(guò)程就能體會(huì)到,它和線程池非常類似,下面我們就來(lái)介紹一下NSOperation的使用。

2. NSOperation簡(jiǎn)介

NSOperation是一個(gè)抽象類,實(shí)際開發(fā)中需要使用其子類NSInvocationOperation、NSBlockOperation。首先創(chuàng)建一個(gè)NSOperationQueue,再建多個(gè)NSOperation實(shí)例(設(shè)置好要處理的任務(wù)、operation的屬性和依賴關(guān)系等),然后再將這些operation放到這個(gè)queue中,線程就會(huì)被依次啟動(dòng)。蘋果官網(wǎng)對(duì)于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的使用過(guò)程,并自定義一個(gè)繼承于NSOperation的子類并實(shí)現(xiàn)內(nèi)部相應(yīng)的方法。

2.1 NSInvocationOperation

NSInvocationOperation繼承于NSOperation,NSInvocationOperation的定義如下:

@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來(lái)加載一張圖片,示例方法如下:

- (void)loadImageWithMultiThread {
    /*創(chuàng)建一個(gè)調(diào)用操作
     object:調(diào)用方法參數(shù)
    */
    NSInvocationOperation *invocationOperation=[[NSInvocationOperation alloc]initWithTarget:self selector:@selector(loadImage) object:nil];
    //創(chuàng)建完NSInvocationOperation對(duì)象并不會(huì)調(diào)用,它由一個(gè)start方法啟動(dòng)操作,但是注意如果直接調(diào)用start方法,則此操作會(huì)在主線程中調(diào)用,一般不會(huì)這么操作,而是添加到NSOperationQueue中
//    [invocationOperation start];
    
    //創(chuàng)建操作隊(duì)列
    NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init];
    //注意添加到操作隊(duì)后,隊(duì)列會(huì)開啟一個(gè)線程執(zhí)行此操作
    [operationQueue addOperation:invocationOperation];
}
2.2 NSBlockOperation

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

下面我們來(lái)使用NSOperation,實(shí)現(xiàn)多個(gè)線程加載圖片,示例代碼如下:

//// 首先 定義一個(gè)OperationImage的Model

@interface OperationImage : NSObject

@property (nonatomic, assign) NSInteger index;
@property (nonatomic, strong) NSData *imgData;

@end

@implementation OperationImage

@end



//// 使用NSOperation實(shí)現(xiàn)多線程加載圖片

#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.直接使用操隊(duì)列添加操作
        //[operationQueue addOperationWithBlock:^{
        //    [self loadImg:operationImg];
        //}];
        
        ////2.創(chuàng)建操作塊添加到隊(duì)列
        NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
            [self loadImg:operationImg];
        }];
        if (i > 0) {// 設(shè)置依賴
            [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 - 請(qǐng)求圖片數(shù)據(jù)

- (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 {
    
    // 請(qǐng)求數(shù)據(jù)
    operationImg.imgData = [self requestData];
    
    // 更新UI界面(mainQueue是UI主線程)
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        [self updateImage:operationImg];
    }];
    
    // 打印當(dāng)前線程
    NSLog(@"current thread: %@", [NSThread currentThread]);
}

@end

在加載網(wǎng)絡(luò)圖片的代碼上打一個(gè)斷點(diǎn),查看斷點(diǎn)信息,從的運(yùn)行過(guò)程可以看出NSOperation底層涉及到對(duì)GCD的封裝:


NSOperation底層對(duì)GCD的封裝

3. 關(guān)于自定義封裝NSOperation

我們用到的很多三方庫(kù)都自定義封裝NSOperation,如MKNetworkOperation、SDWebImage等。自定義封裝抽象類NSOperation只需要重寫其中的main或start方法,在多線程執(zhí)行任務(wù)的過(guò)程中需要注意線程安全問題,我們還可以通過(guò)KVO監(jiān)聽isCancelled、isExecuting、isFinished等屬性,確切的回調(diào)當(dāng)前任務(wù)的狀態(tài)。下面就是對(duì)NSOperation的自定義封裝代碼:

@interface MyOperation ()

//要下載圖片的地址
@property (nonatomic, copy) NSString *urlString;
//執(zhí)行完成后,回調(diào)的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;
}

// 監(jiān)聽/重寫readonly屬性的set、get方法
- (void)setTaskFinished:(BOOL)taskFinished {
    [self willChangeValueForKey:@"isFinished"];
    _taskFinished = taskFinished;
    [self didChangeValueForKey:@"isFinished"];
}

- (BOOL)isFinished {

    return self.taskFinished;
}

//- (void)main {
//
//    // 打印當(dāng)前線程
//    NSLog(@"%@", [NSThread currentThread]);
//
//    //判斷是否被取消,取消正在執(zhí)行的操作
//    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 {
    
    // 打印當(dāng)前線程
    NSLog(@"%@", [NSThread currentThread]);
    
    //判斷是否被取消,取消正在執(zhí)行的操作
    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

調(diào)用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];
    }
}

說(shuō)明:

  1. 在運(yùn)行上面的代碼時(shí),我們發(fā)現(xiàn)同時(shí)重寫start和main方法時(shí),start方法優(yōu)先執(zhí)行,main方法不會(huì)被執(zhí)行;如果只重寫main方法,則main方法會(huì)被執(zhí)行。
  2. 因?yàn)閕sFinished是readonly屬性,因此我們通過(guò)自定義變量taskFinished來(lái)重寫isFinished的set、get方法,實(shí)現(xiàn)方式詳見代碼。
  3. 如果只重寫start方法,并且其中沒有self.taskFinished = YES時(shí),且在testMyOperation設(shè)置如下:
    運(yùn)行結(jié)果

    可以看到log只能能打出來(lái)執(zhí)行了5次(正好是maxConcurrentOperationCount的值),之后便卡死不動(dòng)。如果不設(shè)置maxConcurrentOperationCount或?qū)axConcurrentOperationCount設(shè)置的足夠大,則可正常執(zhí)行至結(jié)束。如果打開start方法中的self.taskFinished = YES,則也可正常執(zhí)行至結(jié)束??梢妔tart方法中的任務(wù)執(zhí)行結(jié)束后,系統(tǒng)并沒有將線程的isFinished置為YES,導(dǎo)致之后的任務(wù)無(wú)法對(duì)其重用。

  4. 如果只重寫main方法,并且其中沒有self.taskFinished = YES時(shí),testMyOperation方法都是可以正常執(zhí)行的,也就是說(shuō)main執(zhí)行結(jié)束時(shí)系統(tǒng)將線程的isFinished置為YES了,其余任務(wù)可對(duì)其重用。
  5. 比較start與main方法,兩個(gè)方法的執(zhí)行過(guò)程都是并行的;start方法更容易通過(guò)KVO監(jiān)聽到任務(wù)的執(zhí)行狀態(tài),但是需要手動(dòng)設(shè)置一些狀態(tài);main自動(dòng)化程度更高。
  6. 使用NSOperationQueue時(shí),我們打印代碼執(zhí)行,過(guò)程中的線程,發(fā)現(xiàn)線程池中線程的最大個(gè)數(shù)在66個(gè)左右。

    以上驗(yàn)證過(guò)程,得到了昆哥的指教,非常感謝!??

4. NSOperation中的依賴

用NSThread來(lái)實(shí)現(xiàn)多線程時(shí),線程間的執(zhí)行順序很難控制,但是使用NSOperation時(shí)可以通過(guò)設(shè)置操作的依賴關(guān)系來(lái)控制執(zhí)行順序。假設(shè)操作A依賴于操作B,線程操作隊(duì)列在啟動(dòng)線程時(shí)就會(huì)首先執(zhí)行B操作,然后執(zhí)行A。例如在第三節(jié)testMyOperation方法中,我們從第二個(gè)任務(wù)一次設(shè)置了關(guān)系:

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:

  1. NSOperationQueue的maxConcurrentOperationCount一般設(shè)置在5個(gè)以內(nèi),數(shù)量過(guò)多可能會(huì)有性能問題。maxConcurrentOperationCount為1時(shí),隊(duì)列中的任務(wù)串行執(zhí)行,maxConcurrentOperationCount大于1時(shí),隊(duì)列中的任務(wù)并發(fā)執(zhí)行;
  2. 不同的NSOperation實(shí)例之間可以設(shè)置依賴關(guān)系,不同queue的NSOperation之間也可以創(chuàng)建依賴關(guān)系 ,但是要注意不要“循環(huán)依賴”;
  3. NSOperation實(shí)例之間設(shè)置依賴關(guān)系應(yīng)該在加入隊(duì)列之前;
  4. 在沒有使用 NSOperationQueue時(shí),在主線程中單獨(dú)使用 NSBlockOperation 執(zhí)行(start)一個(gè)操作的情況下,操作是在當(dāng)前線程執(zhí)行的,并沒有開啟新線程,在其他線程中也一樣;
  5. NSOperationQueue可以直接獲取mainQueue,更新界面UI應(yīng)該在mainQueue中進(jìn)行;
  6. 區(qū)別自定義封裝NSOperation時(shí),重寫main或start方法的不同;
  7. 自定義封裝NSOperation時(shí)需要我們完全重載start,在start方法里面,我們還要查看isCanceled屬性,確保start一個(gè)operation前,task是沒有被取消的。如果我們自定義了dependency,我們還需要發(fā)送isReady的KVO通知。

參考文章:http://www.cocoachina.com/articles/20404

工程源碼GitHub地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • iOS多線程之NSoperation 目前在 iOS 和 OS X 中有兩套先進(jìn)的同步 API 可供我們使用:NS...
    comst閱讀 848評(píng)論 0 0
  • 前言 GCD是一種很強(qiáng)大的多線程解決方案,但NSOperation同樣也支持多樣性的操作。 NSOperation...
    ChenJZ閱讀 9,296評(píng)論 4 21
  • 前面我們已經(jīng)對(duì) iOS 多線程中的 NSThread 和 GCD 作了初步了解與使用,在 iOS 中,使用 NSO...
    BWLi420閱讀 531評(píng)論 0 11
  • 我們?cè)谇懊鎯晒?jié)分別講了iOS多線程的Pthrea、NSThread和GCD,那么我們關(guān)于多線程的學(xué)習(xí)就剩下最后一個(gè)...
    隨風(fēng)流逝閱讀 347評(píng)論 0 0
  • 緊接著上一篇GCD 之后 今天給大家 分享和總結(jié)的是NSOperation 廢話不多說(shuō):來(lái)看看NSOperatio...
    土鱉不土閱讀 917評(píng)論 0 6

友情鏈接更多精彩內(nèi)容