Swift GCD 和 DispatchQueue 使用解析

iOS 中处理多核并发的技术主要使用以下两种:

  • Grand Central Dispatch(以下简称 GCD
  • NSOperationQueue

在 iOS 程序开发中处理多个任务同时执行的时候,老司机们一定都会使用到这两个框架,而且 GCD 依靠它简洁的语法和对 block 的运用一直很受大家的青睐。iOS 开发中你一定明白这样一条原则:”任何用于界面 UI 刷新和用户交互的操作都要放在主线程来操作,任何耗时或者消耗 CPU 的任务必须在异步线程去操作“。

小白都会问为什要这样,老司机都说记住就好。这里就简单解释下:

一、“任何用于界面 UI 刷新和用户交互的操作都要放在主线程来操作”,要明白这句话只要明白以下几点:

1、主线程是线程安全的,把所有 UI 刷新以及用户的交互放在主线程操作会避免很多意外情况的发生,保证在获取服务端返回的数据时,UI 界面可以及时安全的刷新数据,给用户带来良好体验 。

2、iOS 中只有主线程才可以立刻刷新 UI 界面,如果放在异步线程去操作,就会造成线程阻塞和延迟的问题。

二、“任何耗时或者消耗 CPU 的任务必须在异步线程去操作”。如果你很好地明白了前半句,那么这句话的意思就很好理解了,把耗时或者消耗 CPU 的操作放在异步线程,也就是为了防止线程的阻塞延迟,防止主线程上的 UI 刷新和用户操作的一系列动作出现卡顿,死锁,延迟等问题。

Swift 几个概念

如果你对 iOS 中的 GCD 和 DispatchQueue 使用很熟练的话,那么 Swift 的语法和使用应该就是轻车熟路,如果你还不是特别明白 GCD 是什么鬼?没关系,这里先给大家来点山里的干货:

  1. dispatch queue:一堆在主线程(或后台线程)上同步(或异步)来执行的代码,一旦被创建出来,操作系统就开始接手管理,在 CPU 上分配时间片来执行队列内的代码。开发者没法参与queue的管理。队列采用FIFO模式(先进先出),意味着先加入的也会被先完成,这和超市排队买单,队伍前的总是最先买单出去的道理是一样一样的。

  2. work item:一段代码块,可以在 queue 创建的时候添加,也可以单独创建方便之后复用,你可以把它当成将要在queue上运行的一个代码块。work items也遵循着FIFO模式,也可以同步(或异步)执行,如果选择同步的方式,运行中的程序直到代码块完成才会继续之后的工作,相对地,如果选择异步,运行中的程序在触发了代码块就立刻返回了。

  3. serial(串行) vs concurrent(并行):serial将会执行完一个任务才会开始下一个,concurrent触发完一个就立即进入下一个,而不管它是否已完成。

Swift GCD 和 DispatchQueue 使用

1、'serial'(串行) vs 'concurrent'(并行)

  • 1.1 创建一个DispatchQueue 的方法:
let queue = DispatchQueue(label: "com.omg.td")

label:后面是一个标识,可以随便写,一般建议写成你的工程的 dns 的反序比较好。

  • 1.2 创建一个串行的 queue :

创建一个串行的 queue,和在主线程中执行的代码进行对比,看看串行队列和主线程的区别?

func createSyncQueues() {
    let queue = DispatchQueue(label: "com.omg.td")
    queue.sync {
        for j in 0..<10 {
            print("同步队列执行--😊 \(j)")
        }
    }
    
    for k in 0..<10 {
        print("同步主队列执行--🐤 \(k)")
    }
}

运行,看下控制台打印的结果:

从结果中可以看到两个循环是一个一个按顺序来执行的,也就是说串行队列和主线程一样都是串行输出的。换句话说也就是:主线程也是一个串行队列。

那异步(并行)队列执行会是怎么样呢?

func createAsyncQueues() {
    let queue = DispatchQueue(label: "com.omg.td")
    queue.async {
        for j in 0..<10 {
            print("异步队列执行--😊 \(j)")
        }
    }
    
    for k in 0..<10 {
        print("同步主队列执行--🐤 \(k)")
    }
}

看下输出结果:

这次惊喜地发现和上次不同,并且主线程的 for 循环和异步队列的 for 循环是交替执行,也就是说二者同步的输出,这是因为:异步队列不会阻塞当前线程,而是会另开一个线程来执行当前的任务,而主线程上的任务也就不会被阻塞,所以二者是同步输出的。

  • 1.3 总结

通过上面的对比,我们至少可以明白两个件事:

  1. 主线程中使用 async 可以和后台线程一起并行执行任务。
  2. 主线程中使用 sync 只能串行执行任务,当前线程被卡住,直到串行任务完成才能继续。

2、GCD 服务等级 Qos 队列

弄明白 'serial' (串行) 和 'concurrent' (并行) 的关系,我们继续来看下 GCD 的 Qos 队列。

GCD 服务等级 (GCD QoS) :确定任务重要和优先级的属性。QoS 是个基于具体场景的枚举类型,在初始队列时,可以提供合适的 QoS 参数来获得到相应的权限,如果没有指定 QoS,那么初始方法会使用队列提供的默认的 QoS 值。

QoS 等级 (QoS Classes),从前到后,优先级从高到低:

  • userInteractive - 与用户进行交互的动作,例如:”在主线程操作,刷新用户界面,或者执行动画。如果动作没有很快的发生,用户界面就会明显的卡顿“。重点在于响应性和性能,在瞬间执行。

  • userInitiated - 用户发起的动作需要立即得到结果,例如:”正在打开一个保存的文档或执行一个用户点击用户界面的动作。为了用户后续的工作,需要进行这个工作“。重点在于响应性和性能,几乎是在瞬间执行,例如几秒钟或者更少。

  • default - 它的优先级在 userInitiated 和 utility 之间,这个类别并不打算给开发人员进行分类工作。没有被赋值 QoS 的都会被默认为这个级别,并且 GCD 全局队列就是运行在这个类别下。

  • utility - 可能需要一些时间才能完成的操作,不是立即需要结果的,例如:”下载或者导入数据。utility 任务一般有一个进度条让用户能看到“。重点是在响应性和性能以及能源效率之间提供一个平衡,可能需要几秒或者几分钟。

  • background - 在后台操作并且不需要用户看见的工作,例如:”索引,同步操作和备份等“。重点在能源效率,工作要很长的时间,例如多少分钟或多少小时。

  • unspecified - 它表示当前没有 QoS 信息,系统应该根据环境自动推断 QoS 信息。如果使用遗弃的 API,线程有可能会脱离 QoS,这个时候,线程就是 unspecified QoS 类别的。

(注:在 Low Power Mode 即低电量模式开启时,后台操作包括网络请求将被暂停)

其中两种特殊的优先级分别是 default 和 unspecified,在大部分情况下我们都不会接触或使用的这2种优先级,但是它们的存在还是有意义的。

最后做个最直观的优先级排序:

userInteractive > userInitiated > default > utility > background > unspecified

从上面的介绍分析,等级越高的队列越先被执行,同一等级下的队列中串行队列肯定是一个一个执行,异步队列肯定是分线程并行执行,下面我们就来验证下:

  • 2.1 创建一个 Qos 队列:
let queue = DispatchQueue(label: "com.omg.td", qos: .unspecified)

qos:需要传一个 DispatchQoS 的枚举类型。

  • 2.2 同个队列同等级并行输出对比
func qosConcurrentQueues() {
    let queue = DispatchQueue(label: "com.omg.td", qos: .unspecified)
    
    queue.async {
        for j in 0..<10 {
            print("😊 \(j)")
        }
    }
    
    queue.async {
        for k in 100..<110 {
            print("😢 \(k)")
        }
    }
}

看下输出结果:

在仔细观察后,我们发现结果并没有像我们预想的那样(同等级下的异步队列并行输出),这是怎么回事呢?因为 Qos 队列默认是串行执行的,所以即使 qos 队列中的方法是异步的,也会被顺序串行执行。那么怎样才可以并行执行呢?这就要用到 Qos 队列的另外一个属性:attributes:.concurrent

具体写法:

let queue = DispatchQueue(label: "com.omg.td", qos: .utility, attributes: .concurrent)

同个队列同等级并行输出完整示例:

func qosConcurrentQueues1() {
    let queue = DispatchQueue(label: "com.omg.td", qos: .utility, attributes: .concurrent)
    
    queue.async {
        for j in 0..<10 {
            print("😊 \(j)")
        }
    }
    
    queue.async {
        for k in 100..<110 {
            print("😢 \(k)")
        }
    }
}

看下运行结果:

log-1
log-2

通过输出结果,我们发现队列是并行输出了,但是细心的你是不是发现了结果中的一丝丝不同,附上的两张结果图,是同一个函数运行多次显示的不同结果,为什么会不同呢?难道程序有问题,一个函数怎么可能有两个结果呢?是不是顿时一脸懵逼,一万个草泥马在心中奔腾😢😢😢😢😢😢......,施主息怒,待我给你分析:

我们要明白系统所谓的并行执行,并不全是我们想象那的样是固定的像 log-1 中的一样十分规矩的输出,内部是会发生资源的倾斜或者顺序的不确定性的。继续看下面的例子,你会彻底明白。

新建一个不同等级的 Qos 队列来看下结果:

func qosQueues1() {
    let queue1 = DispatchQueue(label: "com.omg.td1", qos: .userInteractive)
    let queue2 = DispatchQueue(label: "com.omg.td2", qos: .utility)
    
    queue1.async {
        for j in 0..<10 {
            print("queue1异步队列执行--😊 \(j)")
        }
    }
    
    queue2.async {
        for k in 0..<10 {
            print("queue2异步队列执行--😢 \(k)")
        }
    }
}

输出结果:

log-3

观察结果,我们会发现等级越高的队列不是我们之前预测的那样会快速先执行完,而是高等级和低等级的交叉进行输出,仔细想下,其实这就是上面那个问题的完美诠释,系统会使优先级更高的 queue1queue2 更快被执行,虽然在 queue1 运行的时候 queue2 得到一个运行的机会,系统还是将资源倾斜给了被标记为更重要的 queue1,等 queue1 内的任务全部被执行完成,系统才开始全心全意服务于 queue2 。看到这里应该明白了吧。

大家可以在思考个问题,在这些等级中 main queue 主线程队列是排在哪个等级呢?下面我们来看个例子:

func qosQueues2() {
    let queue1 = DispatchQueue(label: "com.omg.td1", qos: .userInteractive)
    let queue2 = DispatchQueue(label: "com.omg.td2", qos: .utility)
    
    queue1.async {
        for j in 0..<10 {
            print("queue1异步队列执行--😊 \(j)")
        }
    }
    
    queue2.async {
        for k in 0..<10 {
            print("queue2异步队列执行--😢 \(k)")
        }
    }
    
    // 实践证明:`main queue`默认就有一个很高的权限
    for n in 0..<10 {
        print("main queue--🐤 \(n)")
    }
}

运行结果:

从中我们可以清楚地得出结论:main queue 默认就有一个很高的权限。

接下来我们在来看下 Qos 队列 attributes 的另外一个属性 initiallyInactive (不活跃的),我们可以创建一个 qos 的不活跃队列,这个队列的特点是需要调用 DispatchQueue 类的 activate() 让任务执行。看下具体代码:

// Qos队列 attributes 的另外一个属性: initiallyInactive
// 同个队列同等级不活跃输出,attributes: 设置了 initiallyInactive,
// 需要调用 DispatchQueue 类的 activate() 让任务执行
func qosConcurrentQueues2() -> DispatchQueue {
    let queue = DispatchQueue(label: "com.omg.td", qos: .userInteractive, attributes: .initiallyInactive)
    
    queue.async {
        for j in 0..<10 {
            print("😊 \(j)")
        }
    }
    
    queue.async {
        for k in 100..<110 {
            print("😢 \(k)")
        }
    }
    
    return queue
}

重点在下面,viewDidLoad() 中调用:

let queue = qosConcurrentQueues2()
queue.activate() 

看下输出结果:

initiallyInactive queue - log

我们发现队列是串行输出的,那么怎样创建一个并行的 initiallyInactive 队列呢?查看 API 我们会发现 Qos 队列的 attributes 接收的是一个数组,所以聪明的你肯定知道怎么办吧。😊

func qosConcurrentQueues3() -> DispatchQueue {
    let queue = DispatchQueue(label: "com.omg.td", qos: .userInteractive, attributes: [.initiallyInactive, .concurrent])
    
    queue.async {
        for j in 0..<10 {
            print("😊 \(j)")
        }
    }
    
    queue.async {
        for k in 100..<110 {
            print("😢 \(k)")
        }
    }
    
    return queue
}

3、延迟执行

当你的应用的某个流程中的某项任务延迟执行,GCD 允许你执行某个方法来达到特定时间后运行你指定任务的目的。直接上代码:

func delayToPerformTask() {
    let queue = DispatchQueue(label: "com.omg.td", qos: .userInteractive)
    
    // seconds(Int),      秒
    // milliseconds(Int), 毫秒
    // microseconds(Int), 微秒
    // nanoseconds(Int),  纳秒
    let time: DispatchTimeInterval = .seconds(5)
    print("😊 \(Date())")
    
    queue.asyncAfter(deadline: .now() + time) {
        print("😊 \(Date())")
    }
}

这里解释下:.now() 方法是获取当前时间。

4、DispatchWorkItem

DispatchWorkItem 是一个代码块,它可以被分到任何的队列,包含的代码可以在后台或主线程中被执行,简单来说,它被用于替换我们前面写的代码block来调用。使用起来也很简单,看代码:

func workItemSample() {
    var num = 10
    
    let workItem = DispatchWorkItem {
        num += 2
    }
    
    workItem.perform() // 激活 workItem 代码块
    
    let queue = DispatchQueue.global(qos: .utility)
    queue.async(execute: workItem)
    workItem.notify(queue: .main) {
        print("workItem完成后的通知:", num)
    }
}

5、主线程更新 UI

最后给大家分享一个开篇说到的主线程刷新 UI 的例子,先看代码:

class IMViewController: UIViewController {
    
    lazy var imageView: UIImageView = {
        let iv = UIImageView()
        iv.frame = CGRect(x: 0, y: 160, width: 300, height: 300)
        return iv
    }()
    
    func loadImage() {
        let url = URL(string: "https://upload.jianshu.io/users/upload_avatars/1776763/c7960fe8-35a8-4d36-8a15-a2289d9f2911.png?imageMogr2/auto-orient/strip|imageView2/1/w/240/h/240")
        
        guard let imgUrl = url else {
            return
        }
        
        let urlSession = URLSession(configuration: .default)
        let dataTask = urlSession.dataTask(with: imgUrl) { (data, response, error) in
            
            if let imageData = data {
                
                DispatchQueue.main.async {
                    self.imageView.image = UIImage(data: imageData)
                }
            }
        }
        
        dataTask.resume()
    }
    
    func setupImageView() {
        view.addSubview(self.imageView)
        loadImage()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupImageView()
    }
    
}

使用的时候记得在 viewDidLoad() 中调用setupImageView() 这个方法。

这里想和大家介绍下 Swift 中的懒加载和 ObjC 中的异同,个人总结供大家可以参阅:

  • 1、{} 用来包装代码,() 用来执行代码。
  • 2、日常开发:
    闭包中智能提示不灵敏,出现 self. 需要注意循环引用。
  • 3、Swift 和 OC 懒加载不同:
    Swift 懒加载的代码只会在第一次调用时执行闭包,将闭包的结果保存在属性中,Lazy 修饰的是一个存储属性,存储的是闭包,并且属性的代码块只会调用一次。

总结:

上面对 GCD 的分享肯定还不够全面,若熟练,则足以满足基本的开发运用。欢迎常来转一转......


点赞+关注,第一时间获取技术干货和最新知识点,谢谢你的支持!

最后祝大家生活愉快~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335