http://www.cocoachina.com/game/20151201/14517.htmlhtml
http://blog.csdn.net/qinlicang/article/details/42221585git
本文是投稿文章,做者:RyanJIN(簡書)
對於iOS的併發編程, 用的最廣泛的就是GCD了, GCD結合Block能夠so easy的實現多線程併發編程. 但若是你看一些諸如AFNetworking, SDWebImage的源碼, 你會發現它們使用的都是NSOperation, 納尼? 難道NSOperation這貨更屌? YES, 它確實更屌! Okay, 那咱們就先來簡單PK下GCD和NSOperation(固然這裏也包括NSOperationQueue).github
1). NSOperation是基於GCD之上的更高一層封裝, 擁有更多的API(e.g. suspend, resume, cancel等等).編程
2). 在NSOperationQueue中, 能夠指定各個NSOperation之間的依賴關係.swift
3). 用KVO能夠方便的監測NSOperation的狀態(isExecuted, isFinished, isCancelled).api
4). 更高的可定製能力, 你能夠繼承NSOperation實現可複用的邏輯模塊.網絡
Soga, 原來NSOperation這麼拽! Apple官方文檔和網絡上有不少NSOperation的資料, 但大部分都是很書面化的解釋(臣妾看不懂啊%>_<%), 看着看着就雲深不知處了. 因此這篇文章我會以灰常通俗的方式來解釋NSOperation的併發編程. Okay, let's go!多線程
併發編程的幾個概念併發
併發編程簡單來講就是讓CPU在同一時間運行多個任務. 這裏面有幾個容易混淆的概念, 咱們先來一個個的梳理下:異步
1). 串行(Serial) VS. 並行(Concurrent)
串行和並行描述的是任務和任務之間的執行方式. 串行是任務A執行完了任務B才能執行, 它們倆只能順序執行. 並行則是任務A和任務B能夠同時執行.
2). 同步(Synchronous) VS. 異步(Asynchronous)
同步和異步描述的其實就是函數何時返回. 好比用來下載圖片的函數A: {download image}, 同步函數只有在image下載結束以後才返回, 下載的這段時間函數A只能搬個小板凳在那兒坐等... 而異步函數, 當即返回. 圖片會去下載, 但函數A不會去等它完成. So, 異步函數不會堵塞當前線程去執行下一個函數!
3). 併發(Concurrency) VS. 並行(Parallelism)
這個更容易混淆了, 先用Ray大神的示意圖和說明來解釋一下: 併發是程序的屬性(property of the program), 而並行是計算機的屬性(property of the machine).
仍是很抽象? 那我再來解釋一下, 並行和併發都是用來讓不一樣的任務能夠"同時執行", 只是並行是僞同時, 而併發是真同時. 假設你有任務T1和任務T2(這裏的任務能夠是進程也能夠是線程):
a. 首先若是你的CPU是單核的, 爲了實現"同時"執行T1和T2, 那隻能分時執行, CPU執行一下子T1後立刻再去執行T2, 切換的速度很是快(這裏的切換也是須要消耗資源的, context switch), 以致於你覺得T1和T2是同時執行了(但其實同一時刻只有一個任務佔有着CPU).
b. 若是你是多核CPU, 那麼恭喜你, 你能夠真正同時執行T1和T2了, 在同一時刻CPU的核心core1執行着T1, 而後core2執行着T2, great!
其實咱們日常說的併發編程包括狹義上的"並行"和"併發", 你不能保證你的代碼會被並行執行, 但你能夠以併發的方式設計你的代碼. 系統會判斷在某一個時刻是否有可用的core(多核CPU核心), 若是有就並行(parallelism)執行, 不然就用context switch來分時併發(concurrency)執行. 最後再以Ray大神的話結尾: Parallelism requires Concurrency, but Concurrency does not guarantee Parallelism!
併發吧, NSOperation!
NSOperation能夠本身獨立執行(直接調用[operation start]), 也能夠放到NSOperationQueue裏面執行, 這兩種狀況下是否併發執行是不一樣的. 咱們先來看看NSOperation獨立執行的併發狀況.
1. 獨立執行的NSOperation
NSOperation默認是非併發的(non-concurrent), 也就說若是你把operation放到某個線程執行, 它會一直block住該線程, 直到operation finished. 對於非併發的operation你只須要繼承NSOperation, 而後重寫main()方法就妥妥滴了, 好比咱們用非併發的operation來實現一個下載需求:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@implementation YourOperation
- (void)main
{
@autoreleasepool {
if
(self.isCancelled)
return
;
NSData *imageData = [[NSData alloc] initWithContentsOfURL:imageURL];
if
(self.isCancelled) { imageData = nil;
return
; }
if
(imageData) {
UIImage *downloadedImage = [UIImage imageWithData:imageData];
}
imageData = nil;
if
(self.isCancelled)
return
;
[self.delegate performSelectorOnMainThread:@selector(imageDownloaderDidFinish:)
withObject:downloadedImage
waitUntilDone:NO];
}
}
@end
|
因爲NSOperation是能夠cancel的, 因此你須要在operation程序內部執行過程當中判斷當前operation是否已經被cancel了(isCancelled). 若是已經被cancel那就不往下執行了. 當你在外面調用[operation cancel]後, isCancelled會被置爲YES.
NSOperation有三個狀態量isCancelled, isExecuting和isFinished. isCancelled上面解釋過. main函數執行完成後, isExecuting會被置爲NO, 而isFinished則被置爲YES.
那腫麼實現併發(concurrent)的NSOperation呢? 也很簡單:
1). 重寫isConcurrent函數, 返回YES, 這個告訴系統各單位注意了我這個operation是要併發的.
2). 重寫start()函數.
3). 重寫isExecuting和isFinished函數
爲何在併發狀況下須要本身來設定isExecuting和isFinished這兩個狀態量呢? 由於在併發狀況下系統不知道operation何時finished, operation裏面的task通常來講是異步執行的, 也就是start函數返回了operation不必定就是finish了, 這個你本身來控制, 你何時將isFinished置爲YES(發送相應的KVO消息), operation就何時完成了. Got it? Good.
仍是上面那個下載的例子, 咱們用併發的方式來實現:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
- (BOOL)isConcurrent {
return
YES;
}
- (void)start
{
[self willChangeValueForKey:@
"isExecuting"
];
_isExecuting = YES;
[self didChangeValueForKey:@
"isExecuting"
];
NSURLRequest * request = [NSURLRequest requestWithURL:imageURL];
_connection = [[NSURLConnection alloc] initWithRequest:request
delegate:self];
if
(_connection == nil) [self finish];
}
- (void)finish
{
self.connection = nil;
[self willChangeValueForKey:@
"isExecuting"
];
[self willChangeValueForKey:@
"isFinished"
];
_isExecuting = NO;
_isFinished = YES;
[self didChangeValueForKey:@
"isExecuting"
];
[self didChangeValueForKey:@
"isFinished"
];
}
#pragma mark - NSURLConnection delegate
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
// to do something...
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
// to do something...
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[self finish];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
[self finish];
}
@end
|
Wow, 並行的operation好像有那麼點意思了. 這裏面還有幾點須要mark一下:
a). operation的executing和finished狀態量須要用willChangeValueForKey/didChangeValueForKey來觸發KVO消息.
b). 在調用完NSURLConnection以後start函數就返回了, 後面就坐等connection的回調了.
c). 在connection的didFinish或didFail回調裏面設置operation的finish狀態, 告訴系統operation執行完畢了.
若是你是在主線程調用的這個併發的operation, 那一切都是很是的perfect, 就算你當前在操做UI也不影響operation的下載操做. BUT, 若是你是在子線程調用的, 或者把operation加到了非main queue, 那麼問題來了, 你會發現這貨的NSURLConnection delegate不走了, what's going on here? 要解釋這個問題就要請出另一個武林高手NSRunLoop, Okay, 下面進入NSRunLoop的show time.
Hey, NSRunLoop你是神馬東東?
關於NSRunLoop推薦看一下孫源@sunnnyxx的分享視頻. 其實從字面上就能夠看出來, RunLoop就是跑圈, 保證程序一直在執行. App運行起來以後, 即便你什麼都不作, 放在那兒它也不會退出, 而是一直在"跑圈", 這就是RunLoop乾的事. 主線程會自動建立一個RunLoop來保證程序一直運行. 但子線程默認不建立NSRunLoop, 因此子線程的任務一旦返回, 線程就over了.
上面的併發operation當start函數返回後子線程就退出了, 當NSURLConnection的delegate回調時, 線程已經木有了, 因此你也就收不到回調了. 爲了保證子線程持續live(等待connection回調), 你須要在子線程中加入RunLoop, 來保證它不會被kill掉.
RunLoop在某一時刻只能在一種模式下運行, 更換模式時須要暫停當前的Loop, 而後重啓新的Loop. RunLoop主要有下面幾個模式:
NSDefalutRunLoopMode : 默認Mode, 一般主線程在這個模式下運行
UITrackingRunLoopMode : 滑動ScrollView是會切換到這個模式
NSRunLoopCommonModes: 包括上面兩個模式
這邊須要特別注意的是, 在滑動ScrollView的狀況下, 系統會自動把RunLoop模式切換成UITrackingRunLoopMode來保證ScrollView的流暢性.
1
2
3
4
5
|
[NSTimer scheduledTimerWithTimeInterval:1.f
target:self
selector:@selector(timerAction:)
userInfo:nil
reports:YES];
|
當你在滑動ScrollView的時候上面的timer會失效, 緣由是Timer是默認加在NSDefalutRunLoopMode上的, 而滑動ScrollView後系統把RunLoop切換爲UITrackingRunLoopMode, 因此timer就不會執行了. 解決方法是把該Timer加到NSRunLoopCommonModes下, 這樣即便滑動ScrollView也不會影響timer了.
1
|
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
|
另外還有一個trick是當tableview的cell從網絡異步加載圖片, 加載完成後在主線程刷新顯示圖片, 這時滑動tableview會形成卡頓. 一般的思路是tableview滑動的時候延遲加載圖片, 等中止滑動時再顯示圖片. 這裏咱們能夠經過RunLoop來實現.
1
2
3
4
|
[self.cellImageView performSelector:@sector(setImage:)
withObject:downloadedImage
afterDelay:0
inModes:@[NSDefaultRunLoopMode]];
|
當NSRunLoop爲NSDefaultRunLoopMode的時候tableview確定中止滑動了, why? 由於若是還在滑動中, RunLoop的mode應該是UITrackingRunLoopMode.
好了, 既然咱們已經瞭解RunLoop的東東了, 咱們能夠回過頭來解決上面子線程併發NSOperation下NSURLConnection的Delegate不走的問題, 各位童鞋且繼續往下看^_^
呼叫NSURLConnection的異步回調
如今解決方案已經很清晰了, 就是利用RunLoop來監督線程, 讓它一直等待delegate的回調. 上面已經說到Main Thread是默認建立了一個RunLoop的, 因此咱們的Option 1是讓start函數在主線程運行(即便[operation start]是在子線程調用的).
1
2
3
4
5
6
7
8
9
10
|
- (void)start
{
if
(![NSThread isMainThread]) {
[self performSelectorOnMainThread:@selector(start)
withObject:nil
waitUntilDone:NO];
return
;
}
// set up NSURLConnection...
}
|
或者這樣:
1
2
3
4
5
6
|
- (void)start
{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.connection = [NSURLConnection connectionWithRequest:self.request delegate:self];
}];
}
|
這樣咱們能夠簡單直接的使用main run loop, 由於數據delivery是很是快滴. 而後咱們就能夠將處理incoming data的操做放到子線程去...
Option 2是讓operation的start函數在子線程運行, 可是咱們爲它建立一個RunLoop. 而後把URL connection schedule到上面去. 咱們先來瞅瞅AFNetworking是怎麼作滴:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
+ (void)networkRequestThreadEntryPoint:(id)__unused object
{
@autoreleasepool {
[[NSThread currentThread] setName:@
"AFNetworking"
];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread
{
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return
_networkRequestThread;
}
- (void)start
{
[self.lock lock];
if
([self isCancelled]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
else
if
([self isReady]) {
self.state = AFOperationExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
[self.lock unlock];
}
|
AFNetworking建立了一個新的子線程(在子線程中調用NSRunLoop *runloop = [NSRunLoop currentRunLoop]; 獲取RunLoop對象的時候, 就會建立RunLoop), 而後把它加到RunLoop裏面來保證它一直運行.
這邊咱們能夠簡單的判斷下當前start()的線程是子線程仍是主線程, 若是是子線程則調用[NSRunLoop currentRunLoop]創新RunLoop, 不然就直接調用[NSRunLoop mainRunLoop], 固然在主線程下就不必調用[runLoop run]了, 由於它原本就是一直run的.
P.S. 咱們還可使用CFRunLoop來啓動和中止RunLoop, 像下面這樣:
1
2
3
|
[self.connection scheduleInRunLoop:[NSRunLoop currentRunLoop]
forMode:NSRunLoopCommonModes];
CFRunLoopRun();
|
等到該Operation結束的時候, 必定要記得調用CFRunLoopStop()中止當前線程的RunLoop, 讓當前線程在operation finished以後能夠退出.
2. NSOperationQueue裏面執行NSOperation
NSOpertion能夠add到NSOperationQueue裏面讓Queue來觸發其執行, 一旦NSOperation被add到Queue裏面那麼咱們就不care它自身是否是併發設計的了, 由於被add到Queue裏面的operation一定是併發的. 並且咱們能夠設置Queue的maxConcurrentOperationCount來指定最大的併發數(也就是幾個operation能夠同時被執行, 若是這個值設爲1, 那這個Queue就是串行隊列了).
爲嘛添加到Queue裏面的operation必定會是併發執行的呢? Queue會爲每個add到隊列裏面的operation建立一個線程來運行其start函數, 這樣每一個start都分佈在不一樣的線程裏面來實現operation們的併發執行.
重要的事情再強調一遍: 咱們這邊所說的併發都是指NSOperation之間的併發(多個operation同時執行), 若是maxConcurrentOperationCount設置爲1或者把operation放到[NSOperationQueue mainQueue]裏面執行, 那它們只會順序(Serial)執行, 固然就不可能併發了.
[NSOperationQueue mainQueue]返回的主隊列, 這個隊列裏面任務都是在主線程執行的(固然若是你像AFNetworking同樣在start函數建立子線程了, 那就不是在主線程執行了), 並且它會忽略一切設置讓你的任務順序的非併發的執行, 因此若是你把NSOperation放到mainQueue裏面了, 那你就放棄吧, 無論你怎麼折騰, 它是絕對不會併發滴. 固然, 若是是[[NSOperationQueue alloc] init]那就是子隊列(子線程)了.
那...那不對呀, 若是我在子線程調用[operation start]函數, 或者把operation放到非MainQueue裏面執行, 可是在operation的內部把start拋到主線程來執行(利用主線程的main run loop), 那多個operation其實不都是要在主線程執行的麼, 這樣還能併發? Luckily, 仍然是併發執行的(其實我想說的是那必須能併發啊...哈哈).
咱們能夠先來看看單線程和多線程下的各個任務(task)的併發執行示意圖:
Yes! 和上面討論狹義併發(Concurency)和並行(Parallelism)概念時的理解是同樣的, 在單線程狀況下(也就是mainQueue的主線程), 各個任務(在咱們這裏就是一個個的NSOperation)能夠經過分時來實現僞並行(Parallelism)執行.
而在多線程狀況下, 多個線程同時執行不一樣的任務(各個任務也會不停的切換線程)實現task的併發執行.
另外, 咱們在往Queue裏面添加operation的時候能夠指定它們的依賴關係, 好比[operationB addDependency:operationA], 那麼operationB會在operationA執行完畢以後纔會執行. 還記得這邊"執行完畢(isFinished)"的概念嗎? 在併發狀況下這個狀態量是由你本身設定的, 好比operationA是用來異步下載一張圖片, 那麼只有圖片下載完成以後或者超過timeout下載失敗以後, isFinished狀態量被標記爲YES, 這時Queue纔會從隊列裏面移除operationA, 並啓動operationB. 是否是很cool? O(∩_∩)O~~
NSOperation實驗課
下面咱們進入實驗課啦, 要想真正瞭解某個東東, 仍是須要打開Xcode, 寫上幾行代碼, 而後Commard+R. 爲了幫Apple提高Xcode的使用率:-D, 我會給出幾個case, 童鞋們能夠本身編寫test code來驗證:
1). 建立兩個operation, 而後直接[operation start], 在NSOperation併發設計和非併發設計的狀況下, 查看這兩個operation是否同時執行了(最簡單的打log看是否是交替打印).
2). 在主線程和子線程下分別調用[operation start], 看看執行狀況.
3). 建立operation並放到NSOperationQueue裏面執行, 分別看看mainQueue和非mainQueue下的執行狀況.
4). maxConcurrentOperationCount設置後的執行狀況.
5). 試試NSOperation的依賴關係設置, [operationB addDependency:operationA].
6). 寫個完整的demo吧, 好比簡單的HTTP Downloader.
最後送上乾貨Demo, RJHTTPDownloader, 用NSOperation實現的一個下載類. 有的童鞋確定會說用AFNetwroking就能夠了, 爲嘛要本身去寫呢? 這個嘛, 偶是以爲別人的代碼再怎麼看和用都不是你的, 本身動手寫的才真正belongs to you! 並且這也不算是重複造輪子, 只是學習輪子是怎麼構造的, 這樣一步一步的慢慢積累, 總有一天咱們也能寫出像AFNetworking這樣的代碼! 共勉.