iOS 多線程 - 解決異步任務(wù)依賴(lài)問(wèn)題

在開(kāi)發(fā)中曾經(jīng)遇到一個(gè)這樣的問(wèn)題:

在過(guò)渡到一個(gè)新的Controller之前,有一些數(shù)據(jù)是必須先傳遞過(guò)去的,但是這些數(shù)據(jù)必須從不同的接口中獲取。

也就是從接口A中獲取data1,從接口B中獲取data2,將data1和data2賦值給Controller的屬性,然后再push過(guò)去。push這個(gè)動(dòng)作依賴(lài)于data1和data2。

最簡(jiǎn)單的方法可以在接口A的請(qǐng)求的成功和失敗的回調(diào)中去執(zhí)行一個(gè)接口B的請(qǐng)求,然后在B的成功或者失敗的回調(diào)中去做push的動(dòng)作。但是這樣的做法代碼會(huì)很丑陋,而且B的請(qǐng)求變成依賴(lài)于A的請(qǐng)求的回調(diào)完成。

使用NSOperation解決

如果了解NSOperation的一些API(可見(jiàn)上一篇NSOperation),很容易想到通過(guò)給NSOperation設(shè)置依賴(lài)來(lái)解決:

- (void)orginalOp{
    NSString *url1 = @"http://www.baidu.com";
    NSString *url2 = @"http://www.itdecent.cn";
    NSOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        [self getHtmlOfUrl:url1 success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable response) {
            NSLog(@"get data of %@ success",url1);
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
            NSLog(@"get html of %@ fail",url1);
        }];
    }];
    
    NSOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        [self getHtmlOfUrl:url2 success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable response) {
            NSLog(@"get data of %@ success",url2);
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
            NSLog(@"get html of %@ fail",url2);
        }];
    }];
    
    NSOperation *pushOp = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"push to new controller after all data is ready!");
    }];
    [pushOp addDependency:op1];
    [pushOp addDependency:op2];
    
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:op1];
    [queue addOperation:op2];
    [queue addOperation:pushOp];
}

- (void)getHtmlOfUrl:(NSString *)url
             success:(void (^)(NSURLSessionDataTask * _Nonnull, id _Nullable))success
             failure:(void (^)(NSURLSessionDataTask * _Nullable, NSError * _Nonnull))failure{
    url = [url stringByRemovingPercentEncoding];
    
    AFHTTPSessionManager *sessionManager = [AFHTTPSessionManager manager];
    sessionManager.responseSerializer = [AFHTTPResponseSerializer serializer];
    sessionManager.responseSerializer.acceptableContentTypes = [sessionManager.responseSerializer.acceptableContentTypes setByAddingObject:@"text/html"];
    [sessionManager GET:url parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        success(task,responseObject);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        failure(task,error);
    }];
}

查看運(yùn)行結(jié)果:

2017-04-21 09:24:49.367 ConcurrentProgram[3733:210543] push to new controller after all data is ready!
2017-04-21 09:24:49.791 ConcurrentProgram[3733:195552] get data of http://www.itdecent.cn success
2017-04-21 09:24:49.802 ConcurrentProgram[3733:195552] get data of http://www.baidu.com success

顯然并沒(méi)有達(dá)到我們預(yù)期的效果。稍作分析即可理解:pushOp依賴(lài)于op1和op2的完成,那么op1和op2什么時(shí)候完成呢?在本例中,由于請(qǐng)求是異步的,并不是獲取到請(qǐng)求結(jié)果之后才完成,而是執(zhí)行完請(qǐng)求動(dòng)作之后該operation就已經(jīng)算是完成了。所以就會(huì)有上述的結(jié)果。

那么要怎么樣才能把operation的完成設(shè)置在請(qǐng)求成功或者失敗之后呢?NSOperation中有一個(gè)isFinished的方法,如果該方法返回YES那么就代表operation已經(jīng)完成了。所以,我們要新建一個(gè)類(lèi)繼承NSOperation來(lái)實(shí)現(xiàn)這個(gè)需求。代碼如下:

#import "AFRequestOperation.h"
@interface AFRequestOperation (){
    BOOL finished;
    BOOL executing;
}
@end

@implementation AFRequestOperation
- (instancetype)init{
    self = [super init];
    if (self) {
        finished = NO;
        executing = NO;
    }
    return self;
}

- (void)start{
    // Always check for cancellation before launching the task.
    if ([self isCancelled])
    {
        // Must move the operation to the finished state if it is canceled.
        [self willChangeValueForKey:@"isFinished"];
        finished = YES;
        [self didChangeValueForKey:@"isFinished"];
        return;
    }
    
    // If the operation is not canceled, begin executing the task.
    [self willChangeValueForKey:@"isExecuting"];
    executing = YES;
    [self didChangeValueForKey:@"isExecuting"];
    
    [self performTask];
}

- (void)performTask{
    AFHTTPSessionManager *sessionManager = [AFHTTPSessionManager manager];
    sessionManager.responseSerializer = [AFHTTPResponseSerializer serializer];
    sessionManager.responseSerializer.acceptableContentTypes = [sessionManager.responseSerializer.acceptableContentTypes setByAddingObject:@"text/html"];
    [sessionManager GET:_url parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        if (self.successHandler) {
            self.successHandler(responseObject);
        }
        [self willChangeValueForKey:@"isExecuting"];
        executing = NO;
        [self didChangeValueForKey:@"isExecuting"];
        
        [self willChangeValueForKey:@"isFinished"];
        finished = YES;
        [self didChangeValueForKey:@"isFinished"];
        
        
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        if (self.errorHandler) {
            self.errorHandler(error);
        }
        
        [self willChangeValueForKey:@"isExecuting"];
        executing = NO;
        [self didChangeValueForKey:@"isExecuting"];
        
        [self willChangeValueForKey:@"isFinished"];
        finished = YES;
        [self didChangeValueForKey:@"isFinished"];
    }];
}

- (BOOL)isFinished {
    return finished;
}

- (BOOL)isExecuting {
    return executing;
}
@end

在調(diào)用的時(shí)候:

- (void)customOp{
    NSString *url1 = @"http://www.baidu.com";
    NSString *url2 = @"http://www.itdecent.cn";
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    AFRequestOperation *op1 = [[AFRequestOperation alloc] init];
    op1.url = url1;
    op1.successHandler = ^(id response) {
        NSLog(@"get data of %@ success",url1);
    };
    op1.errorHandler = ^(NSError *error) {
        NSLog(@"get data of %@ fail",url1);
    };
    [queue addOperation:op1];
    
    AFRequestOperation *op2 = [[AFRequestOperation alloc] init];
    op2.url = url2;
    op2.successHandler = ^(id response) {
        NSLog(@"get data of %@ success",url2);
    };
    op2.errorHandler = ^(NSError *error) {
        NSLog(@"get data of %@ fail",url2);
    };
    [queue addOperation:op2];
    
    NSOperation *pushOp = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"push to new controller after all data is ready!");
    }];
    [pushOp addDependency:op1];
    [pushOp addDependency:op2];
    [queue addOperation:pushOp];
}

運(yùn)行結(jié)果如下:

2017-04-21 09:37:54.833 ConcurrentProgram[4225:249946] get data of http://www.baidu.com success
2017-04-21 09:37:54.988 ConcurrentProgram[4225:249946] get data of http://www.itdecent.cn success
2017-04-21 09:37:54.988 ConcurrentProgram[4225:262299] push to new controller after all data is ready!

為了可以兼容不同的請(qǐng)求這里把一個(gè)請(qǐng)求的url以及成功和失敗的回調(diào)等傳入自定義的operation中。最關(guān)鍵的一點(diǎn)就在于完成請(qǐng)求的成功或者失敗的回調(diào)之后將isFinish設(shè)置為YES,此時(shí)operation才算是完成。

使用GCD解決

雖然這樣可以解決這個(gè)問(wèn)題,但是看起來(lái)比較復(fù)雜,要寫(xiě)一個(gè)類(lèi)繼承自NSOperation。之前也了解過(guò)GCD,那么能否考慮是使用GCD的方式來(lái)解決呢?

GCD中有一個(gè)dispatch_group_t的API,使用dispatch_group_t可以監(jiān)控一組block,dispatch_group_t會(huì)追蹤組內(nèi)的block的執(zhí)行狀態(tài),當(dāng)group中所有的block完成以后,可以使用dispatch_group_notify來(lái)執(zhí)行一個(gè)額外的block。增加dispatch_group_t中block的數(shù)量有兩種方式,一種是使用dispatch_group_async或者dispatch_group_sync后面添加block,另外一種是使用dispatch_group_enter和dispatch_group_leave組合來(lái)代表一個(gè)block的開(kāi)始和結(jié)束。在這里我們需要使用dispatch_group_enter和dispatch_group_leave組合。

代碼如下:

- (void)gcdGroup{
    NSString *url1 = @"http://www.baidu.com";
    NSString *url2 = @"http://www.itdecent.cn";
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_enter(group);
    [self getHtmlOfUrl:url1 success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable response) {
        NSLog(@"get data of %@ success",url1);
        dispatch_group_leave(group);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"get html of %@ fail",url1);
        dispatch_group_leave(group);
    }];
    
    dispatch_group_enter(group);
    [self getHtmlOfUrl:url2 success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable response) {
        NSLog(@"get data of %@ success",url2);
        dispatch_group_leave(group);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"get html of %@ fail",url2);
        dispatch_group_leave(group);
    }];
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"push to new controller after all data is ready!");
    });
}

實(shí)際上dispatch_group_t代表了一個(gè)信號(hào)量,該信號(hào)量有一個(gè)初始值。當(dāng)調(diào)用dispatch_group_enter時(shí)該信號(hào)量的值-1,而掉用dispatch_group_leave的時(shí)候信號(hào)量的值加1.讓信號(hào)量的值等于初始值時(shí),就會(huì)執(zhí)行dispatch_group_notify中的block。有興趣的可以自己嘗試一下使用dispatch_group_async看能不能解決異步任務(wù)依賴(lài)的需求。我個(gè)人的實(shí)驗(yàn)結(jié)果是和第一個(gè)例子的結(jié)果是一樣的。

最后編輯于
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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