你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 主線程中也不絕對安全的 UI 操作

主線程中也不絕對安全的 UI 操作

編輯:IOS開發基礎

1458704331307837.jpg

授權轉載,作者:bestswifter

從最初開始學習 iOS 的時候,我們就被告知 UI 操作一定要放在主線程進行。這是因為 UIKit 的方法不是線程安全的,保證線程安全需要極大的開銷。那麼問題來了,在主線程中進行 UI 操作一定是安全的麼?

顯然,答案是否定的!

在蘋果的 MapKit 框架中,有一個叫做 addOverlay 的方法,它在底層實現的時候,不僅僅要求代碼執行在主線程上,還要求執行在 GCD 的主隊列上。這是一個極罕見的問題,但已經有人在使用 ReactiveCocoa 時踩到了坑,並提交了 issue。

蘋果的 Developer Technology Support 承認這是一個 bug。不管這是 bug 還是歷史遺留設計,也不管是不是在鑽牛角尖,為了避免再次掉進同樣的坑,我認為都有必要分析一下問題發生的原因和解決方案。

GCD 知識復習

在 GCD 中,使用 dispatch_get_main_queue() 函數可以獲取主隊列。調用 dispatch_sync() 方法會把任務同步提交到指定的隊列。

注意一下隊列和線程的區別,他們之間並沒有“擁有關系(ownership)”,當我們同步的提交一個任務時,首先會阻塞當前隊列,然後等到下一次 runloop 時再在合適的線程中執行 block。

在執行 block 之前,首先會尋找合適的線程來執行block,然後阻塞這個線程,直到 block 執行完畢。尋找線程的規則是: 任何提交到主隊列的 block 都會在主線程中執行,在不違背此規則的前提下,文檔還告訴我們系統會自動進行優化,盡可能的在當前線程執行 block。

順便補充一句,GCD 死鎖的充分條件是:“向當前隊列重復同步提交 block”。從原理來看,死鎖的原因是提交的 block 阻塞了隊列,而隊列阻塞後永遠無法執行完 dispatch_sync(),可見這裡完全和代碼所在的線程無關。

另一個例子也可以證明這一點,在主線程中向一個串行隊列同步的派發 block,根據上文選擇線程的原則,block 將在主線程中執行,但同樣不會導致死鎖:

dispatch_queue_t queue = dispatch_queue_create("com.kt.deadlock", nil);
dispatch_sync(queue, ^{
    NSLog(@"current thread = %@", [NSThread currentThread]);
});
// 輸出結果:
// current thread = {number = 1, name = main}
dispatch_queue_t queue = dispatch_queue_create("com.kt.deadlock", nil);
dispatch_sync(queue, ^{
    NSLog(@"current thread = %@", [NSThread currentThread]);
});
// 輸出結果:
// current thread = {number = 1, name = main}

原因分析

啰嗦了這麼多,回到之前描述的 bug 中來。現在我們知道,即使是在主線程中執行的代碼,也很可能不是運行在主隊列中(反之則必然)。如果我們在子隊列中調用 MapKit 的 addOverlay 方法,即使當前處於主線程,也會導致 bug 的產生,因為這個方法的底層實現判斷的是主隊列而非主線程。

更進一步的思考,有時候為了保證 UI 操作在主線程運行,如果有一個函數可以用來創建新的 UILabel,為了確保線程安全,代碼可能是這樣:

- (UILabel *)labelWithText: (NSString *)text {
    __block UILabel *theLabel;
    if ([NSThread isMainThread]) {
        theLabel = [[UILabel alloc] init];
        [theLabel setText:text];
    }
    else {
        dispatch_sync(dispatch_get_main_queue(), ^{
            theLabel = [[UILabel alloc] init];
            [theLabel setText:text];
        });
    }
    return theLabel;
}
- (UILabel *)labelWithText: (NSString *)text {
    __block UILabel *theLabel;
    if ([NSThread isMainThread]) {
        theLabel = [[UILabel alloc] init];
        [theLabel setText:text];
    }
    else {
        dispatch_sync(dispatch_get_main_queue(), ^{
            theLabel = [[UILabel alloc] init];
            [theLabel setText:text];
        });
    }
    return theLabel;
}

從嚴格意義上來講,這樣的寫法不是 100% 安全的,因為我們無法得知相關的系統方法是否存在上述 Bug。

解決方案

由於提交到主隊列的 block 一定在主線程運行,並且在 GCD 中線程切換通常都是由指定某個隊列引起的,我們可以做一個更加嚴格的判斷,即用判斷是否處於主隊列來代替是否處於主線程。

GCD 沒有提供 API 來進行相應的判斷,但我們可以另辟蹊徑,利用 dispatch_queue_set_specific 和 dispatch_get_specific 這一組方法為主隊列打上標記:

+ (BOOL)isMainQueue {
    static const void* mainQueueKey = @"mainQueue";
    static void* mainQueueContext = @"mainQueue";
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        dispatch_queue_set_specific(dispatch_get_main_queue(), mainQueueKey, mainQueueContext, nil);
    });
    return dispatch_get_specific(mainQueueKey) == mainQueueContext;
}
+ (BOOL)isMainQueue {
    static const void* mainQueueKey = @"mainQueue";
    static void* mainQueueContext = @"mainQueue";
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        dispatch_queue_set_specific(dispatch_get_main_queue(), mainQueueKey, mainQueueContext, nil);
    });
    return dispatch_get_specific(mainQueueKey) == mainQueueContext;
}

用 isMainQueue 方法代替 [NSThread isMainThread] 即可獲得更好的安全性。

參考資料

Community bug reports about MapKit

GCD’s Main Queue vs Main Thread

ReactiveCocoa 中遇到類似的坑

Why can’t we use a dispatch_sync on the current queue?

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