iOS 多线程概览

1、为什么所有的UI操作都在主线程中

不仅是iOS系统,包括Android等,所有的UI渲染、操作都在主线程中来完成。那为什么不采用多线程的方式呢?
使用多线程渲染UI更快,操作更流畅。但是系统设计者和开发者来说,需要解决线程问题的成本就更高了,也就是说成本远大于收益了。所以工程师们把所有的UI渲染和操作全都放在了主线程中。参考为什么必须在主线程操作UI

2、为什么要使用多线程

即然所有的UI操作都是单线程的,那么为何还需要多线程呢?在App开发中,所遇到不仅有UI操作,还有一些其他的费时操作,如网络请求、文件读取操作、AR模型下载等。此时就需要开发者把费时的操作放到子线程中去,完成后再返回住线程执行一些UI操作。


11.png

如上图所示,当进入这个Controller时开始下载图片,由于下载的图片较多需要几秒甚至十几秒,在下载时我同步滑动底部的UISlider,此时UISlider并没有滑动,直到所有的图片全部下载完成后才滑动。

试想如果我们的使用的每个App在费时操作时都需要等待很长时间用户才能操作,这样势必会给用户很不好的体验。此时的解决方案是创建一个新的线程来下载图片,这样既不影响用户的UI操作也不影响图片下载。

3、多线程的实现方式

Apple为开发者提供的三种多线程实现方式:

(1)、 Thread

  • 轻量级
  • 需要开发者手动管理线程生命周期和线程同步

(2)、 GCD(Grand Central Dispatch)

  • 相对Thread而言,不需要管理线程生命周期,操作更简单
  • 本身维护了一个线程池,会自动根据当前手机系统的情况来动态管理线程,不需要开发者来管理线程池和线程并发情况
  • 底层源码是开源的,点击Apple Open Spurce查看源码

(3)、 Cocoa Operation

  • 面向对象的API
  • 可以取消、依赖、任务优先级、可以子类化

4、多线程常用队列

多线程可以根据任务执行的队列方式分为三种队列:

  • 主队列: 在主线程中执行的任务
  • 串行队列(Serial Queue): 任务按照先后顺序执行,同一时刻只会执行一个任务
  • 并行队列(Concurrent Queue): 多个任务同时执行,完成的顺序不一定

参考 Apple Developer About Dispatch Queues

5、Serial Queue

  • (1)、串行队列处理并发任务

前面说过,对于一些耗时操作,一般将其放到一个子线程中执行,待完成后再返回主线程中刷新UI。核心代码如下:

// 图片下载管理类`DownloaderManager`
public class DownloaderManager: NSObject {
    public class func downloadImageWithURL(_ url: String) -> UIImage? {
        guard let data = try? Data(contentsOf: URL(string: url)!) else { return nil }
        return UIImage(data: data)
    }
}

// 创建一个队列
let serialQueue = DispatchQueue(label: "TSN.RPChat.io")
for (index, imgurl) in DownloaderManager.imageArray.enumerated() {
   // 把下载任务加载到队列中
   serialQueue.async { 
      let image = DownloaderManager.downloadImageWithURL(imgurl)
      DispatchQueue.main.async {
         girlsImg.image = image
      }
   }
}

此时再次运行代码,发现在下载图片的同时也能滑动UISlider,同时发现图片的加载顺序是按照从上到下的顺序加载的。默认情况下,系统会创建一个串行队列,也就是下载完成第一张图片之后再去下载第二张。此时对我来说,我的目的是下载并把图片全部显示出来,我并不关心图片的下载和加载顺序。这样我就需要在创建队列时设置这个队列为一个并行队列。

串行队列

6、Concurrent Queue

作为队列,concurrent queue中的任务虽然是按照进入队列的顺序启动,但不用等待之前的任务完成,iOS会根据当前系统情况启动多个线程并行执行队列中的任务。

在创建队列时设置attributes属性为concurrent就创建了一个并行队列。

let concurrentQueue = DispatchQueue(label: "TSN.RPChat.io", attributes: .concurrent)

此时运行工程,可以看到图片并不是按照先后顺序加载的,说明同一个concurrent queue中的所有任务在并行执行。

Concurrent Queue

7、面向对象的Cocoa Operation

上面创建的队列我使用的GCD(Grand Central Dispatch)方式,尽管GCD对线程管理进行了封装并加入了面向对象管理模式。但是如果我要对一个队列中的任务做更多的操作,如(查看状态、取消任务,控制任务的执行顺序等)仍然不太方便。考虑到这些问题,苹果为开发这提供了一个面向对象方式的多任务执行机制OperationOperation是基于GCD的对象封装。

(1)、Operation概览

Operation的一些使用状态:

  • isReady是否可执行,一般用于异步的情况下
  • isExexuting标记Operation是否正在执行中
  • isFinished标记Operation是否已经执行完成了,一般用于异步
  • isCancelled标记Operation是否已经cancel

更多状态请参考Apple Developer: Maintaining Operation Object States

(2)、OperationQueue

  • OperationQueue可以加入多个Operation
let ope1 = Operation()
let ope2 = Operation()
       
let que = OperationQueue()
que.addOperation(ope1)
que.addOperation(ope2)
  • maxConcurrentOperationCount可设置最大并发数当前,默认情况下,系统会根据当前情况动态确定最大并发数
que.maxConcurrentOperationCount = 5

此处需要注意的是最大并发数并不是线程数,最大并发数表示的是当前队列最多可同时执行的的任务(或线程)数量。

  • 可取消所有Operation,但当前正在执行的Operation不会取消
  • 所有的Operation执行完毕后退出销毁

(3)、BlockOperation

let queblock = BlockOperation.init(block: { [weak self] in
            
})
let que = OperationQueue.init()
que.maxConcurrentOperationCount = 3
que.addOperation(queblock)

(4)、completionBlock

当执行完一个任务时的回调.
我们可以通过创建Operation的方法,首先创建一个Operation对象,然后将其添加到队列中,这样做就可以通过设置completionBlock,在任务完成时得到通知。

(5)、默认优先级

苹果为Operation提供了优先级,Operation通过qualityOfService属性来控制其优先级,来看源码:

public enum QualityOfService : Int {
    
    case userInteractive = 33

    case userInitiated = 25

    case utility = 17

    case background = 9

    case `default` = -1
}
  • userInteractive: 最高优先级,用于用户交互事件
  • userInitiated:次高优先级,用于用户需要马上执行的事件
  • utility:普通优先级,用于普通任务
  • background:最低优先级,用于不重要的任务
  • default:默认优先级,主线程和没有设置优先级的线程都默认为这个优先级
operation.qualityOfService = .default

通过queuePriority属性来控制在OperationQueue中的优先级:

public enum QueuePriority : Int {

        case veryLow = -8

        case low = -4

        case normal = 0

        case high = 4

        case veryHigh = 8
    }
operation.queuePriority = .high

(6)、使用OperationQueue下载图片

// 创建一个队列
let queue = OperationQueue()

// 创建一个Operation
queue.addOperation {
et image = DownloaderManager.downloadImageWithURL(imgurl)
OperationQueue.main.addOperation {
     girlsImg.image = image 
   }
}

这里需要注意的是:更新UI的代码要放到主线程中完成。使用OperationQueue.main获取主线程队列,然后添加addOperation把更新UI的任务放到主线程。
如果需要在下载完成时做一些相关操作,可以使用completionBlock,

let operation = BlockOperation(block: {
     // 要执行的任务,如下载图片等
     let image = DownloaderManager.downloadImageWithURL(imgurl)
     // 下载完成后,返回主线程渲染图片
     OperationQueue.main.addOperation {
         girlsImg.image = image
     }
})
operation.completionBlock = {
     // 执行完成后
}
// 设置最大并发数 不设置时系统回根据当前情况动态设置最大并发数 设置为1时为串行队列
queue.maxConcurrentOperationCount = 5
// 将Operation添加到queue队列中
queue.addOperation(operation)

(7)、设置任务之间的关联性

设置任务之间的关联性

如图所示,当点击download按钮的时候开始下载图片,但是客户要求按照432的顺序加载,但是图片1不影响,此处需要用到addDependency方法,让图片按照432的顺序下载,图片1并行下载,核心代码如下:

let queue = OperationQueue()

let operation1 = BlockOperation(block: {
       let image = DownloaderManager.downloadImageWithURL(imgArray[0])
       OperationQueue.main.addOperation {
           self.girlsImg1.image = image
    }
})
operation1.completionBlock = {
    print("-------operation1")
 }
        
let operation2 = BlockOperation(block: {
        let image = DownloaderManager.downloadImageWithURL(imgArray[1])
        OperationQueue.main.addOperation {
           self.girlsImg2.image = image
     }
})
operation2.completionBlock = {
     print("-------operation2")
}
        
let operation3 = BlockOperation(block: {
    let image = DownloaderManager.downloadImageWithURL(imgArray[2])
        OperationQueue.main.addOperation {
          self.girlsImg3.image = image
    }
})
operation3.completionBlock = {
   print("-------operation3")
}
        
let operation4 = BlockOperation(block: {
    let image = DownloaderManager.downloadImageWithURL(imgArray[7])
    OperationQueue.main.addOperation {
         self.girlsImg4.image = image
    }
})
operation4.completionBlock = {
    print("-------operation4")
}

operation3.addDependency(operation4)
operation2.addDependency(operation3)
        
queue.addOperation(operation1)
queue.addOperation(operation4)
queue.addOperation(operation3)
queue.addOperation(operation2)

此处把添加OperationQueue的操作,放到了addDependency之后,确保执行前有正确的依赖关系。多次运行代码可以看到,图片的下载顺序依然是4->3->2的顺序。

-------operation1
-------operation4
-------operation3
-------operation2

(8)、取消执行的任务

除了设置一个队列中任务关联性之外,还可以控制取消队列中的任务,但是取消的结果会根据任务的状态而不同:

  • 已经完成的任务,取消不影响其结果
  • 当一个任务被取消时所有与其关联的任务也会被取消
  • 任务被取消后,completionBlock依旧会执行

取消图片下载

如图所示,当我点击download按钮后快速点击cancel按钮,可以看到图片一的下载任务被cancel了。

let cancelItem = UIBarButtonItem()
cancelItem.title = "cancel"
cancelItem.rx.tap.subscribe(onNext: {
   self.queue.cancelAllOperations()
}).disposed(by: disposeBag)

此处可以通过OperationisCancelled属性来判断任务是否被cancel。当isCancelled返回true表示该任务被cancel了。

-------operation4,false
-------operation3,false
-------operation2,false
-------operation1,true

本文主要简单介绍了Operation的一些简单应用,正确的理解和应用这些多线程技术是构建复杂App的基础,关于更多多线程的应用可参考官方多线程的文档:

Apple Developer Threading Programming Guide

Apple Developer About Dispatch Queues

本文demo

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