IOS 程序、主執行緒、主佇列 (巴哥莫出品)
- author :巴哥莫 https://www.jianshu.com/u/a64ad02ab496
- 除錯環境 :Mac mojave 10.14.1 + Xcode10 + Swift4.0
- 關鍵字 :程序、執行緒 、主執行緒 、子執行緒 、佇列 、、主佇列 、global佇列 、main函式、Runloop
- 文集 :IOS- Swift-啟動篇
- 章節 :啟動
- 編號 :1.0.0
- 寫作時間 :2018-12-05
- 本文地址 : https://www.jianshu.com/p/75490b41cde3
目錄
前言
筆者本著一個從業多年OC開發的碼農,最近興致好,想學習一下Swift並寫點東西當作筆記。原本以為這是個很簡單的事情,不就是把OC程式碼翻譯一遍嗎?有什麼難的,寫的時候才發現,把話講明白似乎沒那麼容易。
內容概述
- 程序和執行緒概念複習
- C&OC main啟動
- 主執行緒&子執行緒
- 主佇列
程序和執行緒概念複習
巴哥去請教了公司C語言的同事,同時也查了一些資料,先引入幾個概念的東西 執行緒程序簡單描述
程序
是具有一定獨立功能的程式,相對作業系統來說,作業系統分配資源給程序,所以程序作為系統資源分配和排程的基本單位,程序是可以獨 立執行的一段程式。
執行緒
執行緒程序的一個實體,是CPU排程和分派的基本單位,他是比程序更小的能獨立執行的基本單位。相對於程序,執行緒擁有系統資源比較少,而且執行緒的生命週期是程序這個程式來控制的。在執行時,只是暫用一些計數器、暫存器和棧 。同時一個程序至少要有一個主執行緒。所以真正執行任務的是執行緒。
關於程序和執行緒,網上的說法五花八門,巴哥複製了一段,供大家參考,下面這段話是我的一個同事告訴我的,他讓我深刻理解,我也貼出來供大家參考
程序是擁有資源的最小單位。執行緒是執行的最小單位。--- 某C語言同事
main 啟動
C 語言程式碼 main
int main(){ while (1) { printf("pid is %d \n",getpid()); } return 0; }
result:
pid is 78743 pid is 78743 pid is 78743 ……

1543979065578.jpg
在進入main的時候程序和主執行緒就已經存在了
應用程式程序啟動的時候主執行緒被系統建立了,應用程式是需要不斷的和使用者進行互動的,即主執行緒是需要一直存在。main函式在執行到return 的時候程序就會被銷燬,執行緒也會被釋放。很顯然,應用程式的main函式是不會主動返回的。
- 主執行緒常駐
應用程式是需要一個長久存在的執行緒,這個執行緒通常被我們稱作為主執行緒,主執行緒如何常駐?(上一小節中通過while迴圈可以讓主執行緒長期存活,但是這種方式不可取)應用程式都是響應式的機制,例如, 點選,觸控,外部程序搶佔資源 …… ,應用程式需要快速的對這些事件進行相應。
oc main函式程式碼
int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
swift沒有暴露main方法 他是使用了@UIApplicationMain ,兩者內部實現是一樣的。(OC的比較直觀,直接用OC程式碼做例子)
UIApplicationMain 函式做了什麼,我們從他的函式申明來分析一下?
UIKIT_EXTERN int UIApplicationMain(int argc, char * _Nullable argv[_Nonnull], NSString * _Nullable principalClassName, NSString * _Nullable delegateClassName);
四個引數
@param argc C main函式引數 不再複述
@param argv C main函式引數 不再複述
@param principalClassName 一個字串型別的引數principalClassName,直譯為主要類,必須為UIApplication或者其子類,代表著當前app自身。並且如果此引數為nil的話,則預設為@"UIApplication"
@param delegateClassName 代理類。在UIApplication中有個delegate的變數,delegate遵守UIApplicationDelegate協議負責程式的生命週期。UIApplication 接收到所有的系統事件和生命週期事件時,都會把事件傳遞給UIApplicationDelegate進行處理
綜上所述:UIApplicationMain做了如下3件事
- 建立了UIApplication或其子類的物件
- 根據傳入第四個引數建立了代理物件並設定UIApplication的代理為剛剛創建出來的代理物件
- 應用程式需要main執行緒常駐,並且還要是響應式 的,所以UIApplication在建立的時候應該還做了一件事,啟動了Runloop。(Runloop是怎麼做到不堵塞主執行緒並且快速響應,下一章節將會作介紹)
主執行緒VS子執行緒
按照某C語言同事的說法( 程序是擁有資源的最小單位。執行緒是執行的最小單位 )程序應該是資源空間以及資源空間下執行緒的總和。傳統意義上的主執行緒應該叫第一個執行緒,只不過其他執行緒的建立是依賴於第一個執行緒,既然執行緒是執行的最小單位,CPU在分配時間片段給執行緒的時候應該是雨露均沾的,而不是分主次的。雖然第一個執行緒在建立時先於其他的執行緒,但是他們一旦建立了,CPU在排程的時候應該平等。
思考:??對於整個作業系統而言,執行緒之間是否的平等的
主執行緒
主執行緒通常又叫UI執行緒。為什麼和UI相關的東西都要放在主執行緒中執行?為什麼在子執行緒中去做UI的操作會經常出現Bug?
@IBAction func doDownLoadAction(_ sender: Any) { DispatchQueue.global().async { [unowned self] in self.imageView.image = UIImage(named: "image1.png"); } }
result: 雖然程式沒有崩潰,但是在控制檯上還是報了警告資訊,並且這個圖片載入的很慢或根本就載入不出來。(子執行緒在給UI賦值的時候其實值已經傳成功,只是沒有機制去觸發UI的更新)
Main Thread Checker: UI API called on a background thread: -[UIImageView setImage:]
從OCmain的啟動函式中可以看出,子執行緒和主執行緒巨集觀上的區別就是主線中添加了Runloop ,Runloop使得主執行緒一致存在,不會退出。假如我們在子執行緒中也新增一個Runloop,情況會是什麼樣的呢?
@IBOutlet weak var imageView: UIImageView! var subThread:Thread!; override func viewDidLoad() { super.viewDidLoad(); subThread = Thread.init(target: self, selector: #selector(makeSubThreadAlive), object: nil) subThread.name = "subThread"; subThread.start(); } @IBAction func doSubThreadTask(_ sender: Any) { self .perform(#selector(refreashImage), on: subThread, with: nil, waitUntilDone: false) } @objc func refreashImage(){ self.imageView.image = UIImage(named: "image1.png"); //Thread.sleep(forTimeInterval: 5); } @objc func makeSubThreadAlive(){ print("runloop start"); let machPort = NSMachPort(); RunLoop.current.add(machPort, forMode: RunLoop.Mode.default); RunLoop.current.run(); print("runloop end") }
result:雖然依然會有警告,但是圖片重新整理正常了,很顯然是Runloop觸發了圖片控制元件的更新
Main Thread Checker: UI API called on a background thread: -[UIImageView setImage:]
思考:??是不是表示如果開啟一個常駐的子執行緒,就可以通過該子執行緒來更新UI?
當然還是在主執行緒中去更新UI比較好。巴哥通過這個例子告訴大家,其實主執行緒沒有那麼神祕,它和其他的執行緒之間是平等的,只不過UIApplication在初始化的時候在主執行緒中啟動一個Runloop(Runloop 巴哥計劃也開個章節寫一寫,因為這個很重要)
主佇列
- 佇列概念 佇列像棧一樣,是一種線性表,它的特性是先進先出,插入在一端,刪除在另一端。就像排隊一樣,剛來的人入隊(push)要排在隊尾(rear),每次出隊(pop)的都是隊首(front)的人
- 佇列特點 佇列中的資料元素遵循“先進先出”(First In First Out)的原則,簡稱FIFO結構。
在隊尾新增元素,在隊頭刪除元素。
主佇列的獲取
DispatchQueue.main OperationQueue.main;
主佇列的獲取是通過類變數獲取的,這說明主佇列是事先分配好的
Question:佇列就是佇列,為什麼要強行提個概念叫主佇列呢?
佇列和執行緒
主佇列執行在主執行緒中嗎 ? iOS主執行緒和主佇列的區別
備註:執行緒就是執行緒,佇列就是佇列,這兩者概念上應該是清晰的(沒有可比性,叫做聯絡或則關係比較準確),只不過佇列是執行線上程上的
DispatchQueue.main.async { print("2222 \(Thread.main)"); }
result: 主佇列執行在主執行緒中,它只是被預設取了個名字叫 main 真實的名字叫做 com.apple.main-thread
2222 <NSThread: 0x282678b00>{number = 1, name = main}
func doSomeThingInCommonQueue(){ let queue = DispatchQueue.init(label: "aaa"); queue.sync { print("queue run in \(Thread.current)"); self.imageView.image = UIImage(named: "image1.png"); } }
result: 啟動一個佇列使用同步方式,它執行的執行緒是主執行緒,並且介面重新整理正常。
queue run in <NSThread: 0x28233edc0>{number = 1, name = main}
佇列是平等的,自定義的佇列在主執行緒中執行也能起到主佇列的作用,主佇列只不過是一種約定俗成的概念,預設將系統幫我們建立的這個取名為 main 的佇列叫做主佇列
巴哥淺見 iOS主執行緒和主佇列的區別
這篇文章中的例子很好,巴哥談談對這幾個例子的理解。
import Foundation print("Hello, World!") debugPrint("current thread out: \(Thread.current)") //主執行緒在這裡呢 看我的number編號 let key = DispatchSpecificKey<String>() let queueGlobal =DispatchQueue.global(); let globalKey = DispatchSpecificKey<String>(); DispatchQueue.main.setSpecific(key: key, value: "main") DispatchQueue.global().setSpecific(key: globalKey, value: "global"); func log() { debugPrint("main thread: \(Thread.isMainThread)") debugPrint("current thread in: \(Thread.current)") let valueglobal = DispatchQueue.global().getSpecific(key: key); print("從globalqueue中取queue key 為main 是否能取到\(String(describing: valueglobal))"); let valuemain = DispatchQueue.main.getSpecific(key: key); print("從main queue中取queue key 為main \(String(describing: valuemain))"); let value = DispatchQueue.getSpecific(key: key) print("\(String(describing: value))"); debugPrint("main queue: \(value != nil)") let globalValue = DispatchQueue.getSpecific(key: globalKey); debugPrint("global queue: \(globalValue != nil)") } DispatchQueue.global().sync(execute: log) RunLoop.current.run()// result2 會遮蔽它
result1: 啟動RunLoop (getSpecific這個函式的解釋巴哥會在GCD章節中介紹)
Hello, World! "current thread out: <NSThread: 0x100f0bbc0>{number = 1, name = main}" "main thread: true" "current thread in: <NSThread: 0x100f0bbc0>{number = 1, name = main}" 從globalqueue中取queue key 為main 是否能取到nil 從main queue中取queue key 為main Optional("main") nil "main queue: false" "global queue: true"
result2: 不啟動RunLoop, Program ended with exit code: 0 程序退出了
Hello, World! "current thread out: <NSThread: 0x100f0bbc0>{number = 1, name = main}" "main thread: true" "current thread in: <NSThread: 0x100f0bbc0>{number = 1, name = main}" 從globalqueue中取queue key 為main 是否能取到nil 從main queue中取queue key 為main Optional("main") nil "main queue: false" "global queue: true" Program ended with exit code: 0
- 上文中巴哥提到,不要把主執行緒看的很特殊,應用程式中主執行緒只不過是線上程 number = 1 的執行緒上啟動了RunLoop ,讓這個 number = 1 的執行緒常駐,RunLoop啟動後會讓這個 number = 1 的執行緒有響應機制。
- sync 不會啟動新的執行緒,所以結果中當前執行緒是在 number = 1 的執行緒上執行
- DispatchQueue.getSpecific(key: key) 這個類方法不是表示從全域性獲取名字叫做key的佇列
如果global queue使用非同步執行
import Foundation print("Hello, World!") debugPrint("current thread out: \(Thread.current)") //主執行緒在這裡呢 看我的number編號 let key = DispatchSpecificKey<String>() let queueGlobal =DispatchQueue.global(); let globalKey = DispatchSpecificKey<String>(); DispatchQueue.main.setSpecific(key: key, value: "main") DispatchQueue.global().setSpecific(key: globalKey, value: "global"); func log() { debugPrint("main thread: \(Thread.isMainThread)") debugPrint("current thread in: \(Thread.current)") let valueglobal = DispatchQueue.global().getSpecific(key: key); print("從globalqueue中取queue key 為main 是否能取到\(String(describing: valueglobal))"); let valuemain = DispatchQueue.main.getSpecific(key: key); print("從main queue中取queue key 為main \(String(describing: valuemain))"); let value = DispatchQueue.getSpecific(key: key) print("\(String(describing: value))"); debugPrint("main queue: \(value != nil)") let globalValue = DispatchQueue.getSpecific(key: globalKey); debugPrint("global queue: \(globalValue != nil)") } DispatchQueue.global().async(execute: log)//切換成非同步 RunLoop.current.run()
result
Hello, World! "current thread out: <NSThread: 0x100f0dd20>{number = 1, name = main}" "main thread: false" "current thread in: <NSThread: 0x100f92dd0>{number = 2, name = (null)}" 從globalqueue中取queue key 為main 是否能取到nil 從main queue中取queue key 為main Optional("main") nil "main queue: false" "global queue: true
- DispatchQueue.main & DispatchQueue.global() 是通過類屬性取到的,這兩個queue系統早就幫我們分配好了
- 使用DispatchQueue.getSpecific方法去取佇列的value的時候,是看你有沒有把這個佇列分發出去,上面的例子中分發的是 DispatchQueue.global() ,所以你去取key為 main 的佇列的value時候取不到
上個例子中把最有一個RunLoop.current.run()遮蔽
result
Hello, World! "current thread out: <NSThread: 0x10180b7f0>{number = 1, name = main}" Program ended with exit code: 0
執行緒都來不及切換程序就已經退出了
再看例子2 使用dispatchMain()
import Foundation print("Hello, World!") debugPrint("current thread out: \(Thread.current)") //主執行緒在這裡呢 看我的number編號 let key = DispatchSpecificKey<String>() let queueGlobal =DispatchQueue.global(); let globalKey = DispatchSpecificKey<String>(); DispatchQueue.main.setSpecific(key: key, value: "main") DispatchQueue.global().setSpecific(key: globalKey, value: "global"); func log() { debugPrint("main thread: \(Thread.isMainThread)") debugPrint("current thread in: \(Thread.current)") let valueglobal = DispatchQueue.global().getSpecific(key: key); print("從globalqueue中取queue key 為main 是否能取到\(String(describing: valueglobal))"); let valuemain = DispatchQueue.main.getSpecific(key: key); print("從main queue中取queue key 為main \(String(describing: valuemain))"); let value = DispatchQueue.getSpecific(key: key) print("\(String(describing: value))"); debugPrint("main queue: \(value != nil)") let globalValue = DispatchQueue.getSpecific(key: globalKey); debugPrint("global queue: \(globalValue != nil)") } DispatchQueue.global().async(execute: log) dispatchMain();
result: DispatchQueue.global() 的global方法被執行了,dispatchMain() 只有無法停止這個程序,只能強制退出,dispatchMain() 沒有返回值,使得程序永遠存在
Hello, World! "current thread out: <NSThread: 0x102104100>{number = 1, name = main}" "main thread: false" "current thread in: <NSThread: 0x1022134f0>{number = 2, name = (null)}" 從globalqueue中取queue key 為main 是否能取到nil 從main queue中取queue key 為main Optional("main") nil "main queue: false" "global queue: true"
-
dispatchMain() 官方解釋是 Executes blocks submitted to the main queue. 字面翻譯就是 "把main queue 的程式碼塊都執行了"
思考:dispatchMain() 為什麼把 DispatchQueue.global() 也執行了呢?
巴哥的猜想是 DispatchQueue.global().async async的程式碼塊一定是在主佇列中
- 主佇列到底是不是在主執行緒中執行,巴哥的解釋是 一定會在主執行緒執行 ,關鍵是你有沒有把它分發出去,只要你分發就一定是在主執行緒中執行。 DispatchQueue.main.async 系統預設會把佇列派發到主執行緒中,這篇文章的例子中派發不是main queue 而是 global queue ,這兩者是有區別的
小結
- 程序是擁有資源的最小單位。執行緒是執行的最小單位。
- 執行緒之間是平等的,主執行緒只不過是 number = 1 的執行緒,應用程式的主執行緒,也只不過是在 number = 1 的執行緒上啟動了Runloop,讓 number = 1 的執行緒常駐,使得程序不會被銷燬
- 主執行緒是系統幫我們建立的,而子執行緒是需要依賴主執行緒來建立
- 主佇列一定是在主執行緒中執行的
- 佇列之間也平等的,系統預設會幫助我們分配兩個佇列 dispatchMain() 和 DispatchQueue.global()
- 執行緒和佇列沒有可比性,兩者是完全不同的兩個概念