程序员面试题精选iOS基础篇

IOS 进程、主线程、主队列 (巴哥莫出品)

2018-12-05  本文已影响268人  巴哥莫


目录


前言

笔者本着一个从业多年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件事

  1. 创建了UIApplication或其子类的对象
  2. 根据传入第四个参数创建了代理对象并设置UIApplication的代理为刚刚创建出来的代理对象
  3. 应用程序需要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()
  • 线程和队列没有可比性,两者是完全不同的两个概念
上一篇下一篇

猜你喜欢

热点阅读