美文网首页程序员面试题精选iOS基础篇
IOS 进程、主线程、主队列 (巴哥莫出品)

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

作者: 巴哥莫 | 来源:发表于2018-12-05 17:48 被阅读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()
    • 线程和队列没有可比性,两者是完全不同的两个概念

    相关文章

      网友评论

        本文标题:IOS 进程、主线程、主队列 (巴哥莫出品)

        本文链接:https://www.haomeiwen.com/subject/ivabcqtx.html