有段時(shí)間沒寫博客了,不過這也不是一次兩次了。
嗯,就不找理由也不檢討了,直奔主題吧。
在今天的博客中你將會(huì)看到:
- 異步線程同步
- NSOperation子類重寫
- 條件模塊
- 請求類封裝
異步線程同步
老司機(jī)今天講的不是多線程的基本用法,這個(gè)東西往上的博客其實(shí)蠻多的,而且也基本是多線程的基本用法。老司機(jī)今天主要的是介紹多個(gè)異步線程執(zhí)行結(jié)束后進(jìn)行回調(diào)的解決方案,如果說這么說不太清楚的話,最常見的場景就是多個(gè)網(wǎng)絡(luò)請求都結(jié)束后觸發(fā)列表刷新。
其實(shí)這個(gè)需求呢,還是挺常見的。主要呢,目前有兩種解決思路,一種呢是GCD中的dispatch_group,一種是NSOperation。
dispatch_group
這個(gè)方案呢,實(shí)現(xiàn)起來還比較簡單,先放一下代碼吧。
-(void)testGCDGroup {
dispatch_group_t g = dispatch_group_create();
dispatch_queue_t q = dispatch_queue_create("com.test.queue", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"Will enter task1");
dispatch_group_enter(g);
dispatch_group_async(g, q, ^{
[self task1];
dispatch_group_leave(g);
});
NSLog(@"Will enter task2");
dispatch_group_enter(g);
dispatch_group_async(g, q, ^{
[self task2];
dispatch_group_leave(g);
});
NSLog(@"Come to notify");
dispatch_group_notify(g, q, ^{
NSLog(@"Enter notify");
[self taskComplete];
});
NSLog(@"Pass notify");
}
-(void)task1 {
NSLog(@"Enter sleep 10.");
[NSThread sleepForTimeInterval:10];
NSLog(@"Leave sleep 10.");
}
-(void)task2 {
NSLog(@"Enter sleep 5.");
[NSThread sleepForTimeInterval:5];
NSLog(@"Leave sleep 5.");
}
-(void)taskComplete {
NSLog(@"All task finished.");
}
控制臺(tái)輸出是這個(gè)樣子的:
2018-03-26 14:28:02.317556+0800 test[3446:287435] Will enter task1.
2018-03-26 14:28:02.317714+0800 test[3446:287435] Will enter task2.
2018-03-26 14:28:02.317733+0800 test[3446:287484] Enter sleep 10.
2018-03-26 14:28:02.317847+0800 test[3446:287435] Come to notify.
2018-03-26 14:28:02.317865+0800 test[3446:287486] Enter sleep 5.
2018-03-26 14:28:02.318093+0800 test[3446:287435] Pass notify.
2018-03-26 14:28:07.318474+0800 test[3446:287486] Leave sleep 5.
2018-03-26 14:28:12.321389+0800 test[3446:287484] Leave sleep 10.
2018-03-26 14:28:12.321740+0800 test[3446:287484] Enter notify.
2018-03-26 14:28:12.321932+0800 test[3446:287484] All task finished.
他呢,基本流程就是當(dāng)調(diào)用的dispatch_group_leave()與dispatch_group_enter()相等時(shí),就會(huì)調(diào)用dispatch_group_notify()中的回調(diào)。不過這種實(shí)現(xiàn)方案呢,還是有一個(gè)需要注意的點(diǎn)就是dispatch_group_enter()與dispatch_group_leave()要成對使用,否則就會(huì)進(jìn)入無限的等待狀態(tài)。
第二個(gè)解決方案就是使用NSOperation。吶,我會(huì)放在第二節(jié)著重介紹一下的。
NSOperation子類重寫
我們知道,NSOperation是蘋果提供的一套面向?qū)ο蟮幕贕CD封裝的多線程解決方案。他在使用上更加符合面向?qū)ο蟮乃枷耄臃奖愕臑槿蝿?wù)添加依賴關(guān)系,同時(shí)提供了四個(gè)支持KVO監(jiān)聽的代表當(dāng)前任務(wù)執(zhí)行狀態(tài)的屬性cancelled、executing、finished、ready。NSOperation內(nèi)部對這四個(gè)狀態(tài)行為作了預(yù)處理,根據(jù)任務(wù)的不同狀態(tài)這四個(gè)屬性的值會(huì)自動(dòng)改變。當(dāng)NSOperation配合NSOperationQueue使用時(shí),Queue會(huì)監(jiān)聽所有Operation的狀態(tài)從而分配任務(wù)的啟動(dòng)時(shí)機(jī)。總之,NSOperation隱藏了很多內(nèi)部細(xì)節(jié),讓我們開發(fā)者無需關(guān)心任務(wù)的各種狀態(tài)。
系統(tǒng)行為
首先,為了模仿系統(tǒng)行為,我們先觀察下系統(tǒng)的NSOperation的cancelled、executing、finished、ready四個(gè)屬性的狀態(tài)變化情況。那我們?nèi)ケO(jiān)聽一下NSOperation的四個(gè)屬性。代碼如下:
TestBlockOperation * bp1 = [TestBlockOperation blockOperationWithBlock:^{
NSLog(@"enter bp1");
[NSThread sleepForTimeInterval:3];
NSLog(@"leave bp1");
}];
NSArray * keyPathes = @[@"isReady",@"isCancelled",@"isExecuting",@"isFinished"];
[self logOp:bp1 keyPathes:keyPathes];
[self addObserverForOp:bp1 keyPathes:keyPathes];
[bp1 start];
[bp1 cancel];
控制臺(tái)輸出:
2018-04-18 11:45:01.277354+0800 OperationDemo[72212:1655503] bp1 isReady = true
2018-04-18 11:45:01.277539+0800 OperationDemo[72212:1655503] bp1 isCancelled = false
2018-04-18 11:45:01.278212+0800 OperationDemo[72212:1655503] bp1 isExecuting = false
2018-04-18 11:45:01.278449+0800 OperationDemo[72212:1655503] bp1 isFinished = false
2018-04-18 11:45:01.278682+0800 OperationDemo[72212:1655503] bp1 before start
2018-04-18 11:45:01.278954+0800 OperationDemo[72212:1655503] bp1---isExecuting---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 11:45:01.279063+0800 OperationDemo[72212:1655503] bp1 before main
2018-04-18 11:45:01.279245+0800 OperationDemo[72212:1655503] enter bp1
2018-04-18 11:45:04.279669+0800 OperationDemo[72212:1655503] leave bp1
2018-04-18 11:45:04.280074+0800 OperationDemo[72212:1655503] bp1 after main
2018-04-18 11:45:04.281164+0800 OperationDemo[72212:1655503] bp1---isExecuting---{
kind = 1;
new = 0;
old = 1;
}
2018-04-18 11:45:04.281404+0800 OperationDemo[72212:1655503] bp1---isFinished---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 11:45:04.281557+0800 OperationDemo[72212:1655503] bp1 after start
2018-04-18 11:45:04.281782+0800 OperationDemo[72212:1655503] bp1 before cancel
2018-04-18 11:45:04.281917+0800 OperationDemo[72212:1655503] bp1 after cancel
上述代碼中,我們監(jiān)聽了四個(gè)屬性并執(zhí)行了Operation。根據(jù)日志我們可以總結(jié)如下:
初始狀態(tài)下,ready為YES,其他均為NO
當(dāng)我們調(diào)用 -start 后,執(zhí)行 -main 之前 isExecuting 屬性從NO被置為YES
調(diào)用 -main 之后開始執(zhí)行提交到Operation中的任務(wù)
任務(wù)完成后 isExecuting 屬性從YES被置為NO,isFinished 屬性從NO被置為YES
我們再看一下如果在執(zhí)行 -start 之前先執(zhí)行 -cancel 后會(huì)是什么狀態(tài):
TestBlockOperation * bp2 = [TestBlockOperation blockOperationWithBlock:^{
NSLog(@"enter bp2");
[NSThread sleepForTimeInterval:3];
NSLog(@"leave bp2");
}];
[self addObserverForObj:bp2 keyPathes:keyPathes];
self.bp2 = bp2;
[bp2 cancel];
[bp2 start];
控制臺(tái)輸出:
2018-04-18 11:44:03.597414+0800 OperationDemo[72184:1653790] bp2 before cancel
2018-04-18 11:44:03.597684+0800 OperationDemo[72184:1653790] bp2---isCancelled---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 11:44:03.597881+0800 OperationDemo[72184:1653790] bp2---isReady---{
kind = 1;
new = 1;
old = 1;
}
2018-04-18 11:44:03.598051+0800 OperationDemo[72184:1653790] bp2 after cancel
2018-04-18 11:44:03.598138+0800 OperationDemo[72184:1653790] bp2 before start
2018-04-18 11:44:03.598279+0800 OperationDemo[72184:1653790] bp2---isFinished---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 11:44:03.598393+0800 OperationDemo[72184:1653790] bp2 after start
在執(zhí)行 -start 之前調(diào)用 -cancel 后,isCancelled 屬性從NO被置為YES,isReady 屬性無論什么狀態(tài)都會(huì)被置為YES。這里后面講到dependency的時(shí)候會(huì)說明。
-cancel 之后再調(diào)用 -start ,會(huì)將 isFinished 屬性從NO被置為YES,然后并不調(diào)用 -main 方法。
單個(gè)Operation的行為我們已經(jīng)基本了解,那么接下來我們來看一下當(dāng)兩個(gè)Operation添加到Queue中會(huì)是什么結(jié)果。
TestBlockOperation * bp1 = [TestBlockOperation blockOperationWithBlock:^{
NSLog(@"enter bp1");
[NSThread sleepForTimeInterval:3];
NSLog(@"leave bp1");
}];
bp1.name = @"bp1";
bp1.completionBlock = ^{
NSLog(@"bp1 complete");
};
TestBlockOperation * bp2 = [TestBlockOperation blockOperationWithBlock:^{
NSLog(@"enter bp2");
[NSThread sleepForTimeInterval:3];
NSLog(@"leave bp2");
}];
bp2.name = @"bp2";
bp2.completionBlock = ^{
NSLog(@"bp2 complete");
};
NSArray * keyPathes = @[@"isReady",@"isCancelled",@"isExecuting",@"isFinished"];
[self addObserverForOp:bp1 keyPathes:keyPathes];
[self addObserverForOp:bp2 keyPathes:keyPathes];
NSOperationQueue * q = [NSOperationQueue new];
[bp1 addDependency:bp2];
[q addOperation:bp1];
[q addOperation:bp2];
控制臺(tái)輸出:
2018-04-18 16:37:16.004963+0800 OperationDemo[84411:1940169] bp1 before addDependency:
2018-04-18 16:37:16.005291+0800 OperationDemo[84411:1940169] bp1---isReady---{
kind = 1;
new = 0;
old = 1;
}
2018-04-18 16:37:16.005640+0800 OperationDemo[84411:1940169] bp1 after addDependency:
2018-04-18 16:37:16.005842+0800 OperationDemo[84411:1940219] bp2 before start
2018-04-18 16:37:16.006277+0800 OperationDemo[84411:1940219] bp2---isExecuting---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 16:37:16.007394+0800 OperationDemo[84411:1940219] bp2 before main
2018-04-18 16:37:16.007669+0800 OperationDemo[84411:1940219] enter bp2
2018-04-18 16:37:19.010134+0800 OperationDemo[84411:1940219] leave bp2
2018-04-18 16:37:19.010351+0800 OperationDemo[84411:1940219] bp2 after main
2018-04-18 16:37:19.010701+0800 OperationDemo[84411:1940218] bp1 before start
2018-04-18 16:37:19.010707+0800 OperationDemo[84411:1940219] bp1---isReady---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 16:37:19.010857+0800 OperationDemo[84411:1940218] bp1---isExecuting---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 16:37:19.011126+0800 OperationDemo[84411:1940219] bp2---isExecuting---{
kind = 1;
new = 0;
old = 1;
}
2018-04-18 16:37:19.011134+0800 OperationDemo[84411:1940218] bp1 before main
2018-04-18 16:37:19.011143+0800 OperationDemo[84411:1940220] bp2 complete
2018-04-18 16:37:19.011229+0800 OperationDemo[84411:1940218] enter bp1
2018-04-18 16:37:19.011233+0800 OperationDemo[84411:1940219] bp2---isFinished---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 16:37:19.011458+0800 OperationDemo[84411:1940219] bp2 after start
2018-04-18 16:37:22.011382+0800 OperationDemo[84411:1940218] leave bp1
2018-04-18 16:37:22.011571+0800 OperationDemo[84411:1940218] bp1 after main
2018-04-18 16:37:22.012029+0800 OperationDemo[84411:1940218] bp1---isExecuting---{
kind = 1;
new = 0;
old = 1;
}
2018-04-18 16:37:22.012050+0800 OperationDemo[84411:1940219] bp1 complete
2018-04-18 16:37:22.012375+0800 OperationDemo[84411:1940218] bp1---isFinished---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 16:37:22.013382+0800 OperationDemo[84411:1940218] bp1 after start
當(dāng)為bp1添加bp2作為依賴以后,bp1的 isReady 屬性從YES置為NO。
由于bp2是bp1的依賴,所以優(yōu)先執(zhí)行bp2。
在bp2中任務(wù)完成之后,-main 方法調(diào)用結(jié)束之后, -start 方法調(diào)用結(jié)束之前,bp1調(diào)用 -start 并將 isReady 屬性置為YES。
其他行為與單個(gè)調(diào)用時(shí)基本一致。
我們再來看看當(dāng)bp1添加bp2作為依賴,并且在調(diào)用之前bp2調(diào)用 -cancel 時(shí)的狀態(tài)變化,代碼基本一致,唯一變化是在添加在q之前bp2調(diào)用 -cancel,我就不放代碼了,直接看日志輸出:
2018-04-18 16:39:38.612072+0800 OperationDemo[84462:1944038] bp1 before addDependency:
2018-04-18 16:39:38.612500+0800 OperationDemo[84462:1944038] bp1---isReady---{
kind = 1;
new = 0;
old = 1;
}
2018-04-18 16:39:38.612712+0800 OperationDemo[84462:1944038] bp1 after addDependency:
2018-04-18 16:39:38.613460+0800 OperationDemo[84462:1944038] bp2 before cancel
2018-04-18 16:39:38.613984+0800 OperationDemo[84462:1944038] bp2---isCancelled---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 16:39:38.614337+0800 OperationDemo[84462:1944038] bp2---isReady---{
kind = 1;
new = 1;
old = 1;
}
2018-04-18 16:39:38.614512+0800 OperationDemo[84462:1944038] bp2 after cancel
2018-04-18 16:39:38.614804+0800 OperationDemo[84462:1944152] bp2 before start
2018-04-18 16:39:38.615286+0800 OperationDemo[84462:1944158] bp1 before start
2018-04-18 16:39:38.615321+0800 OperationDemo[84462:1944152] bp1---isReady---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 16:39:38.615614+0800 OperationDemo[84462:1944158] bp1---isExecuting---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 16:39:38.615629+0800 OperationDemo[84462:1944150] bp2 complete
2018-04-18 16:39:38.615661+0800 OperationDemo[84462:1944152] bp2---isFinished---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 16:39:38.616030+0800 OperationDemo[84462:1944158] bp1 before main
2018-04-18 16:39:38.616115+0800 OperationDemo[84462:1944152] bp2 after start
2018-04-18 16:39:38.616132+0800 OperationDemo[84462:1944158] enter bp1
2018-04-18 16:39:41.618815+0800 OperationDemo[84462:1944158] leave bp1
2018-04-18 16:39:41.619170+0800 OperationDemo[84462:1944158] bp1 after main
2018-04-18 16:39:41.619551+0800 OperationDemo[84462:1944152] bp1 complete
2018-04-18 16:39:41.619591+0800 OperationDemo[84462:1944158] bp1---isExecuting---{
kind = 1;
new = 0;
old = 1;
}
2018-04-18 16:39:41.619941+0800 OperationDemo[84462:1944158] bp1---isFinished---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 16:39:41.620073+0800 OperationDemo[84462:1944158] bp1 after start
與單個(gè)Operation調(diào)用 -cancel 行為一致,不影響 -start 的調(diào)用,同樣不會(huì)調(diào)用 -main,不同點(diǎn)是在bp1調(diào)用 -start 之前 isReady 屬性會(huì)被置為YES,之后行為與單個(gè)Operation調(diào)用 -start 一致。
上述行為可以用一張流程圖來表現(xiàn):
重寫子類
通過觀察上述的日志我們可以看出,當(dāng)一個(gè)任務(wù)作為另一個(gè)任務(wù)的依賴時(shí),只有當(dāng)被依賴的任務(wù)完成后,才會(huì)執(zhí)行另一個(gè)任務(wù),而這個(gè)時(shí)間點(diǎn)的時(shí)候,executing、finished兩個(gè)屬性會(huì)發(fā)生變化。那我們需要做的就是實(shí)現(xiàn)一個(gè)NSOperation的子類,讓他可以再我們需要的時(shí)候才被標(biāo)記為完成狀態(tài),這樣,我們只要給刷新列表任務(wù)添加網(wǎng)絡(luò)請求任務(wù)作為依賴即可。所以,我們需要做的只有兩件事,就是接過executing、finished兩個(gè)屬性的管理權(quán)以及在我們需要的時(shí)候改變他們的狀態(tài)。
需求知道了,實(shí)現(xiàn)就很簡單了。老司機(jī)直接就放一份簡單的源碼就好了。
@class DWManualOperation;
typedef void(^OperationHandler)(DWManualOperation * op);
@interface DWManualOperation : NSOperation
/**
以需要實(shí)現(xiàn)的任務(wù)生成operation對象
@param handler 需要實(shí)現(xiàn)的任務(wù)
@return operation實(shí)例
*/
+(instancetype)manualOperationWithHandler:(OperationHandler)handler;
/**
立刻將當(dāng)前任務(wù)標(biāo)識為完成狀態(tài),isExecuting 為 NO,isFinished 為 YES。
*/
-(void)finishOperation;
@end
@interface DWManualOperation ()
@property (nonatomic ,assign ,getter=isFinished) BOOL finished;
@property (nonatomic ,assign ,getter=isExecuting) BOOL executing;
@property (nonatomic ,copy) OperationHandler handler;
@property (nonatomic ,strong) DWManualOperation * cycleSelf;
@end
@implementation DWManualOperation
@synthesize finished = _finished;
@synthesize executing = _executing;
#pragma mark --- interface method ---
+(instancetype)manualOperationWithHandler:(OperationHandler)handler {
DWManualOperation * op = [DWManualOperation new];
if (handler) {
op.handler = handler;
}
return op;
}
-(void)finishOperation {
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
_finished = YES;
_executing = NO;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
#pragma mark --- override ---
-(instancetype)init {
if (self = [super init]) {
_concurrentHandler = YES;
self.completionBlock = nil;
}
return self;
}
-(void)start {
NSLog(@"start");
///如果是被取消狀態(tài)則置為完成狀態(tài)并返回,為了配合NSOperationQueue使用
if (self.isCancelled) {
[self willChangeValueForKey:@"isFinished"];
_finished = YES;
[self didChangeValueForKey:@"isFinished"];
return;
}
if (self.isExecuting || self.isFinished) {///正在執(zhí)行或已經(jīng)完成的任務(wù)不可以調(diào)用開始方法。
return;
}
self.cycleSelf = self;
[super start];
}
-(void)cancel {
[super cancel];
}
-(void)main {///系統(tǒng)實(shí)現(xiàn)中 -start 方法中會(huì)調(diào)用 -main 方法
[self willChangeValueForKey:@"isExecuting"];
_executing = YES;
[self didChangeValueForKey:@"isExecuting"];
[super main];
__weak typeof(self)weakSelf = self;
if (self.handler) {
self.handler(weakSelf);
}
}
-(void)dealloc {
NSLog(@"dealloc");
}
#pragma mark --- tool func ---
static inline void freeOperation(DWManualOperation * op) {
op.cycleSelf = nil;
}
#pragma mark --- setter/getter ---
-(void)setCompletionBlock:(void (^)(void))completionBlock {
__weak typeof(self)weakSelf = self;
dispatch_block_t ab = ^(void) {
if (completionBlock) {
completionBlock();
}
freeOperation(weakSelf);
};
[super setCompletionBlock:ab];
}
@end
吶,寫到這里,我們就基本實(shí)現(xiàn)了一個(gè)跟系統(tǒng)Operation具有相同行為,但是我們可以隨意控制Operation是否完成的子類了。
條件模塊
不知道該叫什么我就隨便起了個(gè)名,其實(shí)就是一個(gè)應(yīng)用,場景就是操作A一定要建立在某種狀態(tài)下才能執(zhí)行。最簡單的就是比如點(diǎn)贊功能必須是登錄后才可進(jìn)行,那么我們就要對這種狀態(tài)做出判斷。如下圖:
你可能說這無非就是一個(gè)判斷的事,的確是,不過像登錄狀態(tài)這種很多地方都要用的功能這樣寫也能很好的復(fù)用。這個(gè)思路能主要還是借鑒的大神Delpan的這篇博客:《操作抽象設(shè)計(jì)-實(shí)踐》,寫的很好,同學(xué)們感興趣可以去看看。
Demo傳送門
請求類封裝
吶,寫到這里其實(shí)就只是講思路了,至此我們已經(jīng)具有了一個(gè)可以控制完成時(shí)機(jī)的Operation了,只要我們將網(wǎng)絡(luò)請求與Operation同時(shí) -start 后,請求回調(diào)結(jié)束后標(biāo)志Operation為完成狀態(tài)后就可以為請求添加依賴了,同時(shí)也可以配合系統(tǒng)的其他Operation和Queue同時(shí)使用完成線程間通信。
說到這要是就結(jié)束了那就太虎頭蛇尾了,而且真愛粉們應(yīng)該知道,一般到這個(gè)時(shí)候就是老司機(jī)的軟廣環(huán)節(jié)了,著急的童鞋們可以關(guān)掉瀏覽器了哈~
老司機(jī)給予這個(gè)思路對AFN進(jìn)行了二次封裝,寫了一個(gè)自用的請求框架DWFlashFlow
。
首先它具有NSOperation的所有特性,可以跟普通Operation結(jié)合在一起使用,其次我還封裝了批量請求和請求量功能,并且在功能層和邏輯層上進(jìn)行了分離,也就是說你可以自由更換你的請求核心類,而邏輯層不變~哎,最近都不會(huì)吹牛逼了,剩下的東西喜歡的同學(xué)自己看吧~
傳送門:DWFlashFlow
喜歡的童靴給點(diǎn)個(gè)小星星唄~