你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 知其然亦知其所以然

知其然亦知其所以然

編輯:IOS開發基礎

128557780.png

本文是投稿文章,作者:RyanJIN(簡書)
對於iOS的並發編程, 用的最普遍的就是GCD了, GCD結合Block可以so easy的實現多線程並發編程. 但如果你看一些諸如AFNetworking, SDWebImage的源碼, 你會發現它們使用的都是NSOperation, 納尼? 難道NSOperation這貨更屌? YES, 它確實更屌! Okay, 那我們就先來簡單PK下GCD和NSOperation(當然這裡也包括NSOperationQueue).

1). NSOperation是基於GCD之上的更高一層封裝, 擁有更多的API(e.g. suspend, resume, cancel等等).

2). 在NSOperationQueue中, 可以指定各個NSOperation之間的依賴關系.

3). 用KVO可以方便的監測NSOperation的狀態(isExecuted, isFinished, isCancelled).

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).

blob.png

還是很抽象? 那我再來解釋一下, 並行和並發都是用來讓不同的任務可以"同時執行", 只是並行是偽同時, 而並發是真同時. 假設你有任務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來實現一個下載需求:

@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.

還是上面那個下載的例子, 我們用並發的方式來實現:

- (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的流暢性.

[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了.

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

另外還有一個trick是當tableview的cell從網絡異步加載圖片, 加載完成後在主線程刷新顯示圖片, 這時滑動tableview會造成卡頓. 通常的思路是tableview滑動的時候延遲加載圖片, 等停止滑動時再顯示圖片. 這裡我們可以通過RunLoop來實現.

[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]是在子線程調用的).

- (void)start 
{
    if (![NSThread isMainThread]) {
        [self performSelectorOnMainThread:@selector(start)
                               withObject:nil
                            waitUntilDone:NO];
        return;
    }
    // set up NSURLConnection...
}

或者這樣:

- (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是怎麼做滴:

+ (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, 像下面這樣:

[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)的並發執行示意圖:

blob.png

Yes! 和上面討論狹義並發(Concurency)和並行(Parallelism)概念時的理解是一樣的, 在單線程情況下(也就是mainQueue的主線程), 各個任務(在我們這裡就是一個個的NSOperation)可以通過分時來實現偽並行(Parallelism)執行.

blob.png

而在多線程情況下, 多個線程同時執行不同的任務(各個任務也會不停的切換線程)實現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這樣的代碼! 共勉.

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved