GCD的简单使用详解

1,516 阅读7分钟

在iOS中,苹果提供了两种多任务处理方式:GCD和NSOperationQueue框架。当它们分配任务到不同的线程,或主要的不同的队列时,他们都能完美地工作。 Swift3.0开始 GCD 的写法发生了巨变,相对于我之前写 OC,那简直美好了无数倍。 GCD 中发挥决定重要作用的肯定就是 DispatchQueue 了,DispatchQueue 是 iOS 的 API文档中System部分中 Dispatch 框架下的一个类。 队列实际上是一个代码块,可以在主程序或后台线程上同步或异步执行。一旦创建了队列,操作系统就是管理它,并给予它在CPU的任何核心上被处理的时间。相应地管理多个队列,并且该管理是开发人员不必处理的。队列遵循FIFO模式(First In,First Out),这意味着首先执行的队列也将首先完成。 接下来,另一个重要的概念是DispatchWorkItem。一个工作项是字面上的一个代码块,它与队列创建一起写入,或者它被分配到一个队列,并且可以被重用。 DispatchWorkItem是:它是运行队列的代码。队列中的工作项的执行也遵循FIFO模式。此执行可以是同步的或异步的。 在同步情况下,运行的应用程序不会退出该项目的代码块,直到执行完成。 当队列计划异步运行时,正在运行的应用程序将调用工作项块并立即返回。

DispatchQueue

DispatchQueue的初始化方法如下:

    public convenience init(label: String, qos: DispatchQoS = default, attributes: DispatchQueue.Attributes = default, autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency = default, target: DispatchQueue? = default)

除了参数 label 其他都有默认值,每个参数的用法接下来会讲一部分。

那么在Swift 3中,创建新的DispatchQueue的最简单的方法如下:

  // 需要提供唯一的标签来创建队列
  let queue = DispatchQueue(label: "com.YaHoYi")

队列创建完成之后,我们就可以使用它来同步(sync)执行代码,或者异步执行代码(async)。 现在我们做演示,我们把一段代码块分配到一个队列来执行。其实按照合理的写法应该是使用(DispatchWorkItem)对象而不是一段代码块。

同步异步执行代码:

	// 串行队列同步执行代码
    func simpleSyncQueues() {
      // 这样创建出来的队列是一个串行队列
         print("=============同步=============")
         let queue = DispatchQueue(label: "com.YaHoYi")
			queue.sync {
				for i in 0..<10 {
					print("😑",i)
				}
			}
			
			for i in 100..<110 {
				print("👻",i)
			}    
  }
	// 串行队列异步执行代码
	func simpleAsyncQueues() {
		
	print("============异步=============")
		
		let queue = DispatchQueue(label: "com.YaHoYii")
		
		queue.async {
			
			for i in 0..<10 {
				print("💣",i)
			}
		}

		for i in 100..<110 {
			
			print("🚀",i)
		}
		
	}
	

同步
异步

从上边的图可以看出同步和异步的执行过程。同步:等待队列执行完毕才去执行第二个队列的任务 异步: 开始一个队列的任务之后,立刻跳出队列的执行,去开始别的队列任务

队列优先级

在主线程上运行的任务一直是最高优先级,因为主队列还处理UI并保持应用程序响应。无论如何,通过将该信息提供给系统,iOS将根据您指定的内容对队列进行优先排序并提供所需的资源(例如CPU上的执行时间)。不用说,所有的任务最终都会完成。但是,区别在于哪些任务会更快完成,哪些任务稍后。

构造一个具有优先级的队列,需要在初始话DispatchQueue的时候,给定 qos 参数,qos 参数是一个枚举类型,不同的枚举值对应不同的优先级。

示例代码:

	// qos参数影响系统给予的优先级
    func queuesWithQoS() {
		
		// 高优先级队列
		let queue1 = DispatchQueue(label: "com.YaHoYi1", qos: .userInitiated)
		// 低优先级队列
		let queue2 = DispatchQueue(label: "com.YaHoYi2", qos: .utility)
		// 异步执行
		queue1.async {
			for i in 0..<10 {
				print("🙂",i)
			}
		}
		// 异步执行
		queue2.async {
			for i in 100..<110 {
				print("😂",i)
			}
		}
		
		// 主线程队列,优先级最高
		for i in 1000..<1010 {
			print("Ⓜ️", i)
		}
		
    }

优先级

很显然执行结果是主队列先输出,然后是 queue1,最后是 queue2。因为他们是异步执行的所以不需要等一个队列执行完就可以继续执行下一个队列了。输出结果没有一定的顺序,运行每一次都会有不同的结果,但是大的规律绝对是主队列先输出,然后是 queue1,最后是 queue2。

并行队列

之前示例的共同之处在于我们创建的队列都是串行的。这意味着如果我们将多个任务分配给任何队列,那么这些任务将一个接一个执行,而不是全部同时执行。 接下来,我们将看到如何使多个任务同时运行,换句话说,我们将看到如何进行并发队列。 创建并发队列需要增加一个新的参数:attributes 是一个结构体。

   public struct Attributes : OptionSet {

        public let rawValue: UInt64

        public init(rawValue: UInt64)
        
        public static let concurrent: DispatchQueue.Attributes

        public static let initiallyInactive: DispatchQueue.Attributes
    }

attributes最后两个静态属性的解释: 1.参数值concurrent,是并发队列的标记。如果不使用此参数,则队列是串行的。 2.参数值initiallyInactive,队列需要手动启动任务,不然会发生运行错误。手动启动队列执行任务使用activate()方法。 此外,QoS参数(决定优先级)不是必需的,我们可以在初始化中省略它是没有任何问题的。

示例代码:

 
    var inactiveQueue: DispatchQueue!
    func concurrentAsyncQueues() {
		// 并发队列
		//	let anotherQueue = DispatchQueue(label: "com.YaHoYi3", qos: .utility,attributes: .concurrent)
		
		// 使用attributes的参数值 initiallyInactive,任务不会自动启动,必须手动触发,不然会发生运行错误
		// let anotherQueue = DispatchQueue(label: "com.YaHoYi3", qos: .utility,attributes: .initiallyInactive)

		// 也是并发队列,只不过必须手动启动任务,因为有属性initiallyInactive
		let anotherQueue = DispatchQueue(label: "com.YaHoYi3", qos: .utility,attributes: [.initiallyInactive,.concurrent])

      // inactiveQueue是一个 DispatchQueue 
		inactiveQueue = anotherQueue
		
		inactiveQueue.async {
			for i in 0..<10 {
				print("⚽️",i)
			}
		}
		
		inactiveQueue.async {
			for i in 100..<110 {
				print("🏀",i)
			}
		}
		
		inactiveQueue.async {
			for i in 1000..<1010 {
				print("🏐",i)
			}
		}
		
    }

放开第一个anotherQueue注释代码,注释掉第三个anotherQueue代码,创建的是一个并发队列,这时不需要手动开启任务,结果如图:

并发

然后注释第一个anotherQueue,放开第二个anotherQueue,创建的是一个串行队列,这时需要下边的手动开启任务。结果如图)

串行,手动开启任务

可以看到队列的任务是顺序执行的。

注释第二个,放开第三个,是并行队列,需要手动开启任务,结果如图:

并发,手动开启任务

可以看到队列中的任务是同时执行的

手动启动任务


if let queue = inactiveQueue {
    queue.activate()
}

延迟执行

应用程序的有时候需要延迟任务的执行。CGD可以通过调用方法来设置任务延迟多久后执行。

    func queueWithDelay() {
       //创建一个并发队列
		let delayQueue = DispatchQueue(label: "com.YaHoYie", qos: .utility, attributes: .concurrent)
		// 输出当前时间,用于延迟对比
		print("1",Date())
		// 设置延迟时间
		let additionalTime: DispatchTimeInterval = .seconds(5)
		// 队列延迟执行5秒
		delayQueue.asyncAfter(deadline: .now() + additionalTime) { 
		    print("2",Date())
		}
		// 队列延迟执行10秒
		delayQueue.asyncAfter(deadline: .now() + 10) {
			print("3",Date())
		}
		
    }
    

结果如图:

延迟执行

输出时间分别等了5秒和10秒,还是自己运行一下代码更直观。

全局队列

之前的例子中,我们手动创建了我们使用的 DispatchQueue。但是,并不总是需要这样做,特别是如果不希望更改DispatchQueue的属性的时候。系统创建了一个后台DispatchQueue的集合,也称为全局队列。可以像使用自定义队列一样自由使用它们,但请注意,不要滥用系统,尝试尽可能多地使用全局队列。

访问全局队列就如此简单:

let globalQueue = DispatchQueue.global()

使用全局队列时,可以更改的属性不多。但是,可以指定要使用的qos 优先级:

let globalQueue = DispatchQueue.global(qos: .userInitiated)

主队列

在更新 UI的时候,必须在主队列中更新,不然会发生运行错误 接下来看一个获取图片的示例:

    func fetchImage() {
		
		let imageURL: URL = URL(string: "http://img06.tooopen.com/images/20160921/tooopen_sy_179583447187.jpg")!
		
		(URLSession(configuration: URLSessionConfiguration.default)).dataTask(with: imageURL, completionHandler: { (imageData, response, error) in
			
			if let data = imageData {
				print("下载图片")
				// 更新 imageView 展示图片。切记更新 UI 要在主队列中
				DispatchQueue.main.async {
					self.imageView.image = UIImage(data: data)
				}
				
			}
		}).resume()
		
    }

GCD另一个主要的类DispatchWorkItem

文章开头说过按照合理的DispatchQueue的调用任务的写法应该是使用(DispatchWorkItem)对象而不是像示例中那样写一段代码。那么如何使用DispatchWorkItem呢。

func useWorkItem() {
		var value = 10
		let workItem = DispatchWorkItem {
			value += 5
			print(value)
		}
		// 执行 workItem
		workItem.perform()
		// 全局队列
		let queue = DispatchQueue.global(qos: .utility)
		queue.async {
         // 执行 workItem
			workItem.perform()
		}
		
		// 通知一个队列,可以是主队列也可以是其他的队列
		// workItem.notify(queue: DispatchQueue.main) { 
		//  	print("value = ", value)
		// }
		
    }

结果:

WorkItem

因为执行了两次嘛。 你也可以注释第一个 print ,放开最后的通知看下结果是否一样。

Demo 下载