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
目录
启动.................................................................................................1.0.0
前言
笔者本着一个从业多年 OC 开发的码农, 最近兴致好, 想学习一下 Swift 并写点东西当作笔记. 原本以为这是个很简单的事情, 不就是把 OC 代码翻译一遍吗? 有什么难的, 写的时候才发现, 把话讲明白似乎没那么容易.
内容概述
进程和线程概念复习
C&OC main 启动
主线程 & 子线程
主队列
进程和线程概念复习
巴哥去请教了公司 C 语言的同事, 同时也查了一些资料, 先引入几个概念的东西 线程进程简单描述 https://www.cnblogs.com/dreamroute/p/5207813.html
进程
是具有一定独立功能的程序, 相对操作系统来说, 操作系统分配资源给进程, 所以进程作为系统资源分配和调度的基本单位, 进程是可以独 立运行的一段程序.
线程
线程进程的一个实体, 是 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 的代码块一定是在主队列中
总结 iOS 主线程和主队列的区别
主队列到底是不是在主线程中执行, 巴哥的解释是 一定会在主线程执行, 关键是你有没有把它分发出去, 只要你分发就一定是在主线程中执行. DispatchQueue.main.async 系统默认会把队列派发到主线程中, 这篇文章的例子中派发不是 main queue 而是 global queue , 这两者是有区别的
小结
进程是拥有资源的最小单位. 线程是执行的最小单位.
线程之间是平等的, 主线程只不过是 number = 1 的线程, 应用程序的主线程, 也只不过是在 number = 1 的线程上启动了 Runloop, 让 number = 1 的线程常驻, 使得进程不会被销毁
主线程是系统帮我们创建的, 而子线程是需要依赖主线程来创建
主队列一定是在主线程中执行的
队列之间也平等的, 系统默认会帮助我们分配两个队列 dispatchMain()和 DispatchQueue.global()
线程和队列没有可比性, 两者是完全不同的两个概念
来源: http://www.jianshu.com/p/75490b41cde3