『ios』主線程 和 主隊列的關系,絕對安全的UI操作,主線程中一定是主隊列?

image.png

一直想搞清楚主線程和主隊列的關系。

其實我一直帶著這么幾個問題。
1.主線程中的任務一定在主隊列中執行嗎?
2.如何保證一定在主線程中執行?
3.如何保證既在主線程中執行又在主隊列中執行?

下面我們帶著這幾個問題來看下面的文章。

先來認識這幾個方法

//給指定的隊列設置標識
dispatch_queue_set_specific(dispatch_queue_t queue, const void *key,
        void *_Nullable context, dispatch_function_t _Nullable destructor);
queue:需要關聯的queue,不允許傳入NULL。
key:唯一的關鍵字。
context:要關聯的內容,可以為NULL。
destructor:釋放context的函數,當新的context被設置時,destructor會被調用

//獲取當前所在隊列的標識,根據唯一的key取出當前queue的context,如果當前queue沒有key對應的context,則去queue的target queue取,取不著返回NULL,如果對全局隊列取,也會返回NULL。
dispatch_get_specific(key)
//獲取指定隊列的標識
dispatch_queue_get_specific(dispatch_queue_t queue, const void *key);

第一種情況

//給主隊列設置標識
    dispatch_queue_set_specific(dispatch_get_main_queue(), key, @"main", NULL);
//放到同步隊列中 全局并發隊列中
    dispatch_sync(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"main thread: %d", [NSThread isMainThread]);
        // 判斷是否是主隊列
        void *value = dispatch_get_specific(key);//返回與當前分派隊列關聯的鍵的值。
        NSLog(@"main queue: %d", value != NULL);
    });

打印的結果:

main thread: 1 //是主線程
main queue: 0 //不是主隊列

分析:不是主隊列是因為 在全局并發隊列中,但是在全局并發隊列中,為何又在主線程執行呢?經過查閱資料發現,蘋果是為了性能,所以在主線程執行,線程切換是耗性能的。

第二種情況

//異步加入到全局并發隊列中
 dispatch_async(dispatch_get_global_queue(0, 0), ^{
//異步加入到主隊列中
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"main thread: %d", [NSThread isMainThread]);
            NSLog(@"%@",[NSThread currentThread]);
            // 判斷是否是主隊列
            void *value = dispatch_get_specific(key);//返回與當前分派隊列關聯的鍵的值。
            NSLog(@"main queue: %d", value != NULL);
        });
    });
    NSLog(@"dispatch_main會堵塞主線程");
    dispatch_main();
    NSLog(@"查看是否堵塞主線程");

打印結果:

dispatch_main會堵塞主線程
main thread: 0  //不是主線程
<NSThread: 0x600000b73b80>{number = 3, name = (null)}//不是主線程
main queue: 1   //是主隊列

分析:明明是在dispatch_get_main_queue()中,為何不是在主線程執行呢?是不是很顛覆三觀?原因再 dispatch_main()這個函數。這個函數的作用,經過查閱資料和讀文檔獲取:

/*!
 * @function dispatch_main
 *
 * @abstract
 * Execute blocks submitted to the main queue.
 * 執行提交給主隊列的任務blocks
 *
 * @discussion
 * This function "parks" the main thread and waits for blocks to be submitted
 * 
 * to the main queue. This function never returns.
 * 
 * Applications that call NSApplicationMain() or CFRunLoopRun() on the
 * main thread do not need to call dispatch_main().
 *
 */
API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_EXPORT DISPATCH_NOTHROW DISPATCH_NORETURN
void
dispatch_main(void);
這個函數會阻塞主線程并且等待提交給主隊列的任務blocks完成,這個函數永遠不會返回.
這個方法會阻塞主線程,然后在其它線程中執行主隊列中的任務,這個方法永遠不會返回(意思會卡住主線程).

也就是說,把主隊列中的任務在其他線程中執行。所以用了dispatch_get_main_queue也不一定是主線程的。
加上這個

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"1");
}

我們會發現打印這句話

Attempting to wake up main runloop, but the main thread as exited. This message will only log once. Break on _CFRunLoopError_MainThreadHasExited to debug.

這就完全驗證了dispatch_main()的作用。退出主線程,讓其他線程來執行主線程的任務。

當我們把dispatch_main()注釋掉之后。上面那段代碼的打印

dispatch_main會堵塞主線程
查看是否堵塞主線程
 main thread: 1 //是主線程
 <NSThread: 0x600001596b80>{number = 1, name = main}//是主線程
main queue: 1//是主隊列

經過上面幾種情況的分析,到底我們需要怎么搞才能確定是保證線程安全的呢?

我查閱了sdwebimage 3.8版本和 4.4.2版本,發現了兩種不同的寫法

3.8版本

#define dispatch_main_sync_safe(block)\
    if ([NSThread isMainThread]) {\
        block();\
    } else {\
        dispatch_sync(dispatch_get_main_queue(), block);\
    }

#define dispatch_main_async_safe(block)\
    if ([NSThread isMainThread]) {\
        block();\
    } else {\
        dispatch_async(dispatch_get_main_queue(), block);\
    }

4.4.2版本

#ifndef dispatch_queue_async_safe
#define dispatch_queue_async_safe(queue, block)\
    if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(queue)) {\
        block();\
    } else {\
        dispatch_async(queue, block);\
    }
#endif

#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block) dispatch_queue_async_safe(dispatch_get_main_queue(), block)
#endif

那么到底上面兩個版本哪個版本才是最安全的呢?
既然sdwebImage最新版本換了方式,那么肯定,4.2.2是最安全的。

3.8版本是我們平時通常使用的版本,那么對于4.2.2又如何解釋呢?
iOS UI 操作在主線程不一定安全?
通過這篇文章我了解了不少東西。

第一種方案

      static void *mainQueueKey = "mainQueueKey";
        dispatch_queue_set_specific(dispatch_get_main_queue(), mainQueueKey, &mainQueueKey, NULL);
        if (dispatch_get_specific(mainQueueKey)) {
            // do something in main queue
            //通過這樣判斷,就可以真正保證(我們在不主動搞事的情況下),任務一定是放在主隊列中的
        } else {
            // do something in other queue
        }

第二種方案 ,sdwebImage的方案

//獲取主隊列名
    const char *main_queue_name = dispatch_queue_get_label(dispatch_get_main_queue());
    const char *other_queue_name = "other_queue_name";
    NSLog(@"\nmain_queue_name====%s", main_queue_name);
    //創建一個和主隊列名字一樣的串行隊列
    dispatch_queue_t customSerialQueue = dispatch_queue_create(other_queue_name, DISPATCH_QUEUE_SERIAL);
    if (strcmp(dispatch_queue_get_label(customSerialQueue), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {
        //名字一樣
        NSLog(@"\ncutomSerialQueue is main queue");
        dispatch_async(customSerialQueue, ^{
            //將更新UI的操作放到這個隊列
            if ([NSThread isMainThread]) {
                NSLog(@"i am mainThread ");
            }
      
            NSLog(@"\nUI Action Finished");
        });
        
    } else {
        //名字不一樣
        NSLog(@"cutomSerialQueue is main queue");
                NSLog(@"main thread: %d", [NSThread isMainThread]);
                // 判斷是否是主隊列
                void *value = dispatch_get_specific(key);//返回與當前分派隊列關聯的鍵的值。
                NSLog(@"main queue: %d", value != NULL);
        
    }

總結:我們都知道主隊列是串行隊列,所以串行隊列肯定不會開辟新的線程,也就是說主隊列一定會是在主線程執行。
對于更新UI這種操作,要保證在主線程執行,也就是要保證在主隊列執行。
1.主線程中的任務一定在主隊列中執行嗎?
不是。
2.如何保證一定在主線程中執行?
只要保證在主隊列中執行就可以了。
3.如何保證既在主線程中執行又在主隊列中執行?
保證在主隊列中就會及在主線程又在主隊列。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。