Runloop & 方法呼叫

d_d發表於2018-12-04

問題來源

首先來看一段程式碼,然後猜測輸出結果,注意輸出內容的順序:

class Dog: NSObject {
    @objc
    func getDogName() {
        debugPrint("ハチ公")
    }
}

var runloop: CFRunLoop!

let sem = DispatchSemaphore(value: 0)

let thread = Thread {
    RunLoop.current.add(NSMachPort(), forMode: .common)
    runloop = CFRunLoopGetCurrent()
    sem.signal()
    CFRunLoopRun()
}

thread.start()

sem.wait()

DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
    CFRunLoopPerformBlock(runloop, CFRunLoopMode.defaultMode.rawValue) {
        debugPrint("2")
    }
    
    DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
        debugPrint("1")
        let dog = Dog()
        dog.perform(#selector(dog.getDogName), with: nil)
    })

}

RunLoop.current.run()
複製程式碼

輸出結果為1ハチ公

原因分析

為什麼沒有輸出2

我們其實已經將block任務加到了thread對應的runloop中了,但是此時的runloop已經處於休眠狀態,一直沒有被喚醒,所以block內任務一直得不到執行。

如何執行到block內方法

要想執行到block內方法我們需要喚醒runloop,可以使用

let dog = Dog()
dog.perform(#selector(dog.getDogName), with: nil)
CFRunLoopWakeUp(runloop)
複製程式碼

這樣runloop被喚醒,block方法得到了執行,輸出結果為1ハチ公2

新增timer的方式

喚醒runloop的方式有多種,有一張圖描述的很不錯:

Runloop & 方法呼叫
從這張圖中我們不難看出可以通過新增timer的方式,喚醒runloop:

let dog = Dog()
dog.perform(#selector(dog.getDogName), on: thread, with: nil, waitUntilDone: false)
複製程式碼

這個方法會在指定執行緒新增一個定時器,然後執行相應方法。這樣通過新增定時器方法成功喚醒runloop,又因為block新增到任務佇列較早所以輸出順序是12ハチ公

關於perform的幾個方法

public func perform(_ aSelector: Selector!) -> Unmanaged<AnyObject>!
複製程式碼

這個方法是NSObjectProtocol內的方法,會在當前執行緒通過訊息傳送機制呼叫方法。

open func perform(_ aSelector: Selector, on thr: Thread, with arg: Any?, waitUntilDone wait: Bool)
複製程式碼

這個方法是NSObjectThread檔案下的一個extension,他可以指定執行緒並且新增一個timer去執行這個任務,這就是為什麼可以喚醒runloop的原因。waitUntilDone這個引數可以設定是否等到selector方法執行完畢後,執行後面的程式碼。

open func perform(_ aSelector: Selector, with anArgument: Any?, afterDelay delay: TimeInterval)
複製程式碼

這個方法是NSObjectRunloop檔案下的一個extension。與上一個方法類似,只不過預設執行在當前執行緒。並且可以設定delay時間。

修改runloop

如果我們將程式碼改為:

let thread = Thread {
//    RunLoop.current.add(NSMachPort(), forMode: .common)
    runloop = CFRunLoopGetCurrent()
    sem.signal()
    CFRunLoopRun()
}

let dog = Dog()
dog.perform(#selector(dog.getDogName), on: thread, with: nil, waitUntilDone: false)
複製程式碼

這樣只會列印1。因為雖然我們獲取到了當前執行緒的runloop,但是由於runloop沒有新增timersource,所以會馬上釋放掉,這樣執行緒也被釋放掉了,所以再去操作這個執行緒就得不到任何結果了。

問題來源

相關文章