有段時間沒寫博客了,不過這也不是一次兩次了。
嗯块茁,就不找理由也不檢討了,直奔主題吧辫狼。
在今天的博客中你將會看到:
- 異步線程同步
- NSOperation子類重寫
- 條件模塊
- 請求類封裝
異步線程同步
老司機(jī)今天講的不是多線程的基本用法初斑,這個東西往上的博客其實蠻多的,而且也基本是多線程的基本用法膨处。老司機(jī)今天主要的是介紹多個異步線程執(zhí)行結(jié)束后進(jìn)行回調(diào)的解決方案见秤,如果說這么說不太清楚的話,最常見的場景就是多個網(wǎng)絡(luò)請求都結(jié)束后觸發(fā)列表刷新真椿。
其實這個需求呢鹃答,還是挺常見的。主要呢突硝,目前有兩種解決思路测摔,一種呢是GCD中的dispatch_group,一種是NSOperation解恰。
dispatch_group
這個方案呢锋八,實現(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.");
}
控制臺輸出是這個樣子的:
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()相等時,就會調(diào)用dispatch_group_notify()中的回調(diào)腐宋。不過這種實現(xiàn)方案呢紊服,還是有一個需要注意的點就是dispatch_group_enter()與dispatch_group_leave()要成對使用,否則就會進(jìn)入無限的等待狀態(tài)胸竞。
第二個解決方案就是使用NSOperation欺嗤。吶,我會放在第二節(jié)著重介紹一下的撤师。
NSOperation子類重寫
我們知道剂府,NSOperation是蘋果提供的一套面向?qū)ο蟮幕贕CD封裝的多線程解決方案。他在使用上更加符合面向?qū)ο蟮乃枷胩甓埽臃奖愕臑槿蝿?wù)添加依賴關(guān)系腺占,同時提供了四個支持KVO監(jiān)聽的代表當(dāng)前任務(wù)執(zhí)行狀態(tài)的屬性cancelled、executing痒谴、finished衰伯、ready。NSOperation內(nèi)部對這四個狀態(tài)行為作了預(yù)處理积蔚,根據(jù)任務(wù)的不同狀態(tài)這四個屬性的值會自動改變意鲸。當(dāng)NSOperation配合NSOperationQueue使用時,Queue會監(jiān)聽所有Operation的狀態(tài)從而分配任務(wù)的啟動時機(jī)≡豕耍總之读慎,NSOperation隱藏了很多內(nèi)部細(xì)節(jié),讓我們開發(fā)者無需關(guān)心任務(wù)的各種狀態(tài)槐雾。
系統(tǒng)行為
首先夭委,為了模仿系統(tǒng)行為,我們先觀察下系統(tǒng)的NSOperation的cancelled募强、executing株灸、finished、ready四個屬性的狀態(tài)變化情況擎值。那我們?nèi)ケO(jiān)聽一下NSOperation的四個屬性慌烧。代碼如下:
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];
控制臺輸出:
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)聽了四個屬性并執(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 后會是什么狀態(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];
控制臺輸出:
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)都會被置為YES玄括。這里后面講到dependency的時候會說明。
-cancel 之后再調(diào)用 -start ,會將 isFinished 屬性從NO被置為YES肉瓦,然后并不調(diào)用 -main 方法遭京。
單個Operation的行為我們已經(jīng)基本了解,那么接下來我們來看一下當(dāng)兩個Operation添加到Queue中會是什么結(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];
控制臺輸出:
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。
其他行為與單個調(diào)用時基本一致疫剃。
我們再來看看當(dāng)bp1添加bp2作為依賴钉疫,并且在調(diào)用之前bp2調(diào)用 -cancel 時的狀態(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
與單個Operation調(diào)用 -cancel 行為一致,不影響 -start 的調(diào)用城菊,同樣不會調(diào)用 -main备燃,不同點是在bp1調(diào)用 -start 之前 isReady 屬性會被置為YES,之后行為與單個Operation調(diào)用 -start 一致凌唬。
上述行為可以用一張流程圖來表現(xiàn):
重寫子類
通過觀察上述的日志我們可以看出赚爵,當(dāng)一個任務(wù)作為另一個任務(wù)的依賴時,只有當(dāng)被依賴的任務(wù)完成后法瑟,才會執(zhí)行另一個任務(wù),而這個時間點的時候唁奢,executing霎挟、finished兩個屬性會發(fā)生變化。那我們需要做的就是實現(xiàn)一個NSOperation的子類麻掸,讓他可以再我們需要的時候才被標(biāo)記為完成狀態(tài)酥夭,這樣,我們只要給刷新列表任務(wù)添加網(wǎng)絡(luò)請求任務(wù)作為依賴即可脊奋。所以熬北,我們需要做的只有兩件事,就是接過executing诚隙、finished兩個屬性的管理權(quán)以及在我們需要的時候改變他們的狀態(tài)讶隐。
需求知道了,實現(xiàn)就很簡單了久又。老司機(jī)直接就放一份簡單的源碼就好了巫延。
@class DWManualOperation;
typedef void(^OperationHandler)(DWManualOperation * op);
@interface DWManualOperation : NSOperation
/**
以需要實現(xiàn)的任務(wù)生成operation對象
@param handler 需要實現(xiàn)的任務(wù)
@return operation實例
*/
+(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)實現(xiàn)中 -start 方法中會調(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
吶脉执,寫到這里疼阔,我們就基本實現(xiàn)了一個跟系統(tǒng)Operation具有相同行為,但是我們可以隨意控制Operation是否完成的子類了半夷。
條件模塊
不知道該叫什么我就隨便起了個名婆廊,其實就是一個應(yīng)用,場景就是操作A一定要建立在某種狀態(tài)下才能執(zhí)行玻熙。最簡單的就是比如點贊功能必須是登錄后才可進(jìn)行否彩,那么我們就要對這種狀態(tài)做出判斷。如下圖:
你可能說這無非就是一個判斷的事嗦随,的確是列荔,不過像登錄狀態(tài)這種很多地方都要用的功能這樣寫也能很好的復(fù)用敬尺。這個思路能主要還是借鑒的大神Delpan的這篇博客:《操作抽象設(shè)計-實踐》,寫的很好贴浙,同學(xué)們感興趣可以去看看砂吞。
Demo傳送門
請求類封裝
吶,寫到這里其實就只是講思路了崎溃,至此我們已經(jīng)具有了一個可以控制完成時機(jī)的Operation了蜻直,只要我們將網(wǎng)絡(luò)請求與Operation同時 -start 后,請求回調(diào)結(jié)束后標(biāo)志Operation為完成狀態(tài)后就可以為請求添加依賴了袁串,同時也可以配合系統(tǒng)的其他Operation和Queue同時使用完成線程間通信概而。
說到這要是就結(jié)束了那就太虎頭蛇尾了,而且真愛粉們應(yīng)該知道囱修,一般到這個時候就是老司機(jī)的軟廣環(huán)節(jié)了赎瑰,著急的童鞋們可以關(guān)掉瀏覽器了哈~
老司機(jī)給予這個思路對AFN進(jìn)行了二次封裝,寫了一個自用的請求框架DWFlashFlow
破镰。
首先它具有NSOperation的所有特性餐曼,可以跟普通Operation結(jié)合在一起使用,其次我還封裝了批量請求和請求量功能鲜漩,并且在功能層和邏輯層上進(jìn)行了分離源譬,也就是說你可以自由更換你的請求核心類,而邏輯層不變~哎孕似,最近都不會吹牛逼了踩娘,剩下的東西喜歡的同學(xué)自己看吧~
傳送門:DWFlashFlow
喜歡的童靴給點個小星星唄~