MacOS和iOS开发中异步调用与多线程的区别

时间:2024-10-19 08:35:32

很多童鞋可能对Apple开发中的异步调用和多线程的区别不是太清楚,这里本猫将用一些简单的示例来展示一下它们到底直观上有神马不同.

首先异步调用可以在同一个线程中,也可以在多个不同的线程中.每个线程都有一个run loop,主线程的运行环称为main run loop,所有和UI界面有关的操作必须在主运行环中完成.在run loop中线程将会轮询消息源发出的消息.比如在MacOS中鼠标就是一个消息源,鼠标按下就会给系统中相关线程的消息环发送消息;而在iOS中手势操作也是一个消息源,当对应的手势出现时,也会给有关线程的run loop发送消息.

run loop接收到消息源发送的消息后会发生什么呢?它会调用之前设置好的消息回调方法,比如手势操作对象会在创建时设置一个回调闭包.在手势消息送达运行环时,就会调用这个闭包.这就是异步调用的一种典型的使用方法.

异步调用不立即返回值,而是等到某个特定的时机再完成特定的调用.值得注意的是,正如前面强调过的那样,如果你在多线程中使用异步调用视图更新用户界面,你必须将实际updateUI的代码放到main run loop中执行,否则没有效果.

下面先看一下异步调用的示例代码,必须在Xcode8.0beta的playground中测试,因为是Swift 3.0的语法:

import UIKit

@objc class Time:NSObject{
    @objc(one:)
    func one(timer:Timer!){
        print(#function)
    }

    @objc(two:)
    func two(timer:Timer!){
        print(#function)
    }
}

let t = Time()
let timer1 = Timer(timeInterval: 1.0, target: t, selector: #selector(Time.one(timer:)), userInfo: nil, repeats: true)
let timer2 = Timer(timeInterval: 1.0, target: t, selector: #selector(Time.two(timer:)), userInfo: nil, repeats: true)
RunLoop.current.add(timer1, forMode: .defaultRunLoopMode)
RunLoop.current.add(timer2, forMode: .defaultRunLoopMode)
RunLoop.current.run()

大家注意最后一行,实际在graphic类型的App中这一行是不需要加的,因为UI App自身会驱动消息环使其中的任何Timer自动触发,但是在console类型的App中(比如MacOS)必须要手动run一次.

以上代码运行结果会反复打印出以下内容:

one(timer:)
two(timer:)
one(timer:)
two(timer:)
one(timer:)
two(timer:)

下面我们马上来验证异步调用默认都是在同一线程这一特性,因为在线程的run loop中所有触发和处理都是同步的,如果某一个事件的处理耗时很久或挂起了线程,则该线程中其他的操作都不会响应,即便有新的事件源被触发.

我们只需要简单的将代码中one方法修改为如下内容即可观察这一有趣的现象:

func one(timer:Timer!){
        print(#function)
        for _ in 0...100000000{
            //Do Nothing...
        }
    }

运行代码片段,你可以很清楚的看到,在one方法执行完成之前two方法不可能被执行,即使前面two方法被设置为1.0秒执行一次.这是因为one方法挂起了run loop,所有其他消息源都不会被处理.

最后让本猫带大家看一下异步调用在多线程中带来的变化,我们在playground中新建一个helper方法:

func invokeTimerInNewThread(methodName:String){
    let thread = Thread() {
        let timer = Timer(timeInterval: 1.0, target: t, selector: NSSelectorFromString(methodName), userInfo: nil, repeats: true)
        RunLoop.current.add(timer, forMode: .defaultRunLoopMode)
        RunLoop.current.run()
        while true{
            Thread.sleep(forTimeInterval: 1.0)
        }
    }
    thread.start()
}

以上代码有2点要注意:

  1. 如果不做任何操作线程在执行完闭包方法中的代码后会很快终止,所以你必须添加一个无限循环,尽管你可以将死循环可以写的更优雅一些 ;]
  2. 线程对象必须用start方法启动后才会运行

哦鸟!就是这么简单,下面把原来playground中的Timer创建代码统统删除,修改为如下代码:

invokeTimerInNewThread(methodName: "one:")
invokeTimerInNewThread(methodName: "two:")
while true{
    Thread.sleep(forTimeInterval: 1.0)
}

没错,playground中的主线程也必须最后添加一个无限循环,否则你看不到任何东西.运行playground,你将看到如下输出:

two(timer:)
one(timer:)
two(timer:)
two(timer:)
two(timer:)
two(timer:)
two(timer:)

这正是异步调用和多线程的一本质区别!!!one方法在循环结束前会一直挂起,之后的one方法回调将不会得到任何机会执行,原因前面说过了,one方法所在的线程被挂起了!但是这丝毫不会影响two方法的执行,因为two方法在另一个线程中啊!