ios多线程

来源:互联网 发布:python 首字母小写 编辑:程序博客网 时间:2024/06/08 14:04

关于多线程处理,很多人不敢用,因为多线程常常会导致一些奇葩的问题出现(大多都是资源共享导致的)。但存在肯定是有其道理的,如果你对它足够了解,当然也不一定就不出问题。起码是出了问题很快就可以定位到。至于多线程的优势,我想不说大家也都知道,肯定是比单线程要快的。这里就讲讲iOS的多线程并发处理。现在的设备很少是单核处理器了,如果你还用单线程,那可真是暴殄天物啊。那么多资源就被你这样浪费了。在iOS中主线程是处理UI的,你如果什么都在主线程里操作,这样用户的体验会非常的差,因为操作一个事件就要等其结束才能继续。所以用户体验很差,今天就来谈谈iOS的多线程处理。

第一部分:GCD

GCD是管理多线程最常用的api,GCD提供并管理任务队列。当然首先要了解队列是什么东西。


队列是管理对象的先进先出(FIFO)的一种数据结构,就像排队买票,排在前面的先买,后面的后买一样。



队列分为两种队列,一种就是分发队列(Dispatch queues)一种是顺序队列(Serial queues
分发队列是以一种比较简单的方式在应用中来执行的异步并发任务,分发队列的任务是以block的形式提交的,无论是分发队列还是顺序队列,都是在主线程提交任务,其他线程来执行的队列。分发队列是以平行的并发顺序执行任务的。顺序队列是有先后顺序的(避免资源被竞争)。
在iOS中系统提供给每个应用一个顺序的队列和四个并发队列。其中主线程(顺序队列)来执行UI操作,所以如果大量的任务都放到主线程,那么主线程就会被阻塞掉。除了主线程队列,系统还提供了四个并发队列,我们称之为全局分发队列。这些队列是全局的队列,他们仅仅是优先级的区别。为了使用这些队列,你需要使用这个functiondispatch_get_global_queue函数,并设置第一个参数(参数如下)来得到这个队列的引用。
  • DISPATCH_QUEUE_PRIORITY_HIGH
  • DISPATCH_QUEUE_PRIORITY_DEFAULT
  • DISPATCH_QUEUE_PRIORITY_LOW
  • DISPATCH_QUEUE_PRIORITY_BACKGROUND
  • 当然,他们的优先级从上到下越来越低的是。你可以使用任意一个你想用的队列,但是请注意,这些队列是系统提供给你的,所以队列里面的任务不一定都是你的任务(可能有其他的任务)。
GCD快速预览
了解了队列的基础知识之后,我们来看看什么是GCD

接下来我们来个demo。
Demo Project
我们的项目比较简单,仅仅展示四幅图片,每张图片都根据url从网上请求,图片请求在主线程中,为了向你展示这个会影响UI的响应,我们在图片下面加了个组件slider。demo 在这里下载。点击开始下载图片的start按钮,同时拖拽下面的slider。你会发现,你根本拖动不了它。



一旦你点击开始按钮,图片就开始下载了在主线程中,很显然,这个方法很糟糕,它使得UI无法响应其他任何事件,不幸的是,至今还有一些应用依然在主线程内执行大量的任务。那么现在我们开始定位这个问题,并用并发队列来解决它。
我们首先用并发队列来实现它,然后再用顺序队列来实现它。(两种方法而已)

运用并发队列
首先打开 ViewController.swift这个文件在Xcode中,看下代码,你就会找到这个方法didClickOnStart,它就是下载图片的方法。
<span style="font-size:14px;">@IBActionfuncdidClickOnStart(sender:AnyObject){    letimg1=Downloader.downloadImageWithURL(imageURLs[0])    self.imageView1.image=img1        letimg2=Downloader.downloadImageWithURL(imageURLs[1])    self.imageView2.image=img2        letimg3=Downloader.downloadImageWithURL(imageURLs[2])    self.imageView3.image=img3        letimg4=Downloader.downloadImageWithURL(imageURLs[3])    self.imageView4.image=img4    }</span>


每个下载方法被看作是一个任务,并且所以任务都在主线程内顺序执行,那么现在我们要获取一个全局并发队列的一个引用(默认优先级的就可以了)。
<span style="font-size:14px;">letqueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0)        dispatch_async(queue){()->Voidin                        letimg1=Downloader.downloadImageWithURL(imageURLs[0])            dispatch_async(dispatch_get_main_queue(),{                                self.imageView1.image=img1            })                    }</span>


我们得到了一个默认的并发队列通过这个方法dispatch_get_global_queue,并且在内部的block内我们提交了一个下载图片的任务,一旦图片下载完成,我们提交另一个任务给主线程,让其更新下载好的图片,换句话来讲,就是我们把下载图片的方法放到了后台去做,但是执行UI更新还是在主线程内。其他部分也如此,修改后的代码就如下所示:
<span style="font-size:14px;">@IBActionfuncdidClickOnStart(sender:AnyObject){        letqueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0)    dispatch_async(queue){()->Voidin                letimg1=Downloader.downloadImageWithURL(imageURLs[0])        dispatch_async(dispatch_get_main_queue(),{                        self.imageView1.image=img1        })            }    dispatch_async(queue){()->Voidin                letimg2=Downloader.downloadImageWithURL(imageURLs[1])                dispatch_async(dispatch_get_main_queue(),{                        self.imageView2.image=img2        })            }    dispatch_async(queue){()->Voidin                letimg3=Downloader.downloadImageWithURL(imageURLs[2])                dispatch_async(dispatch_get_main_queue(),{                        self.imageView3.image=img3        })            }    dispatch_async(queue){()->Voidin                letimg4=Downloader.downloadImageWithURL(imageURLs[3])                dispatch_async(dispatch_get_main_queue(),{                        self.imageView4.image=img4        })    }    }</span>



你提交了四个图片的下载任务(并发的)到默认的队列,重新运行应用时你会发现速度快了很多,并且你也可以拖动那个slider了。

运用顺序队列
另一个可选的方法是使用顺序队列来处理这个问题,同样修改这个ViewController.swift文件的didClickOnStart()方法,这次我们用顺序队列来下载图片。当使用顺序队列的时候,需要注意的是,每个应用有个默认的顺序队列,那就是主队列,用来处理UI的。所以记住,使用顺序队列,你必须自己新建顺序队列来使用,否则你还是在主队列里面操作。并且会导致你要在主队列执行任务,同时试着去更新UI ,可能会报错。你可以使用函数dispatch_queue_create来创建新的队列并且提交任务到新的队列里面去,修改之后,我们的代码就变成如下的样子:
<span style="font-size:14px;">@IBActionfuncdidClickOnStart(sender:AnyObject){        letserialQueue=dispatch_queue_create("com.appcoda.imagesQueue",DISPATCH_QUEUE_SERIAL)            dispatch_async(serialQueue){()->Voidin                letimg1=Downloader.downloadImageWithURL(imageURLs[0])        dispatch_async(dispatch_get_main_queue(),{                        self.imageView1.image=img1        })            }    dispatch_async(serialQueue){()->Voidin                letimg2=Downloader.downloadImageWithURL(imageURLs[1])                dispatch_async(dispatch_get_main_queue(),{                        self.imageView2.image=img2        })            }    dispatch_async(serialQueue){()->Voidin                letimg3=Downloader.downloadImageWithURL(imageURLs[2])                dispatch_async(dispatch_get_main_queue(),{                        self.imageView3.image=img3        })            }    dispatch_async(serialQueue){()->Voidin                letimg4=Downloader.downloadImageWithURL(imageURLs[3])                dispatch_async(dispatch_get_main_queue(),{                        self.imageView4.image=img4        })    }    }</span>




以上的两种方法的不同之处就是顺序队列需要自己去创建。但是以下两点我们仍须注意:
1、顺序队列比并发队列花费的时间长,原因就是顺序队列每次仅下载一张图,任务被顺序的执行。
2、图片被下载顺序是 image1, image2, image3, and image4 ,因为顺序队列每次仅执行一个任务。

第二部分:Operation Queues
GCD是一个底层的C语言API,它允许开发者并发执行任务。而Operation queues是一个高级的抽象的队列模型,并且在GCD的顶端。那就是说你可以执行并发任务像GCD一样,但是是以面向对象的方式。简单的说,Operation queues就是让开发者的生活更简单了,方法更容易使用了。
它不像GCD一样先进先出的顺序,下面就是它与并发队列的不同之处。
1、不遵循先进先出的队列形式,在Operation queues中,你可以设置操作优先级并且可以设置任务之间的依赖(就是某个任务要在另一个任务执行之后才开始执行的这样的依赖),这就是为什么它不遵循先进先出的原则。
2、默认的也是并发执行的,你不能改变它(不能变成顺序队列),但你可以设置依赖关系。
3、Operation queue是这个类NSOperationQueue的一个实例。它的任务被封装在NSOperation实例中。

NSOperation

任务是以NSOperation的实例的形式被提交到operation queues中的,我们上面讨论的GCD的任务是以block的形式提交的。这里也是一样的道理,我们是以NSOperation的实例作为一个基础单元。NSOperation是一个抽象的类,不能被直接实例化,在iOS中我们提供了两个子类来实例化,这两个类可以直接使用,但并不拘于此,你也可以自己来继承NSOperation它并实例化它。两个子类如下:
1、NSBlockOperation –使用这个类来初始化NSOperation的时候是用一个或者多个block的形式,NSOperation它本身就含有不止一个的block,当所有的block被执行完后,NSOperation就被认为结束了。
2、NSInvocationOperation –是初始化NSOperation用调用一个指定对象的一个方法来实现的。
NSOperation的优势何在?

1、支持依赖


2、改变执行的优先级

<span style="font-size:14px;">publicenumNSOperationQueuePriority: Int{    caseVeryLow    caseLow    caseNormal    caseHigh    caseVeryHigh}</span>


3、对于任何给定的队列,你可以取消操作,当任务被加到队列中后,你可以调用NSOperation实例对象的cancel()方法来取消这个任务。但取消操作有如下三种情形可能:

任务已经执行完,取消则无效果。

任务正在被执行,如果那样,系统不会强制停止任务,但取消的属性(cancelled)将被置为真。

任务在队列等待执行,那么它将被取消。

4、NSOperation有三个有用的布尔值属性finished, cancelled, and ready。finished 将被置为真当任务完成的时候,cancelled将为真当任务被取消的时候,ready将被置为真,当其可以被执行的时候。

5、任何的NSOperation都有一个选项设置完成后的block,当任务被完成后调用,也就是当finished属性被赋值为真之后,block开始执行。

接下来我们重写上面的demo用NSOperationQueues,首先声明变量在ViewController中

varqueue=NSOperationQueue()

然后重写这个方法didClickOnStart

<span style="font-size:14px;">@IBActionfuncdidClickOnStart(sender:AnyObject){    queue=NSOperationQueue()     queue.addOperationWithBlock{()->Voidin                letimg1=Downloader.downloadImageWithURL(imageURLs[0])         NSOperationQueue.mainQueue().addOperationWithBlock({            self.imageView1.image=img1        })    }        queue.addOperationWithBlock{()->Voidin        letimg2=Downloader.downloadImageWithURL(imageURLs[1])                NSOperationQueue.mainQueue().addOperationWithBlock({            self.imageView2.image=img2        })     }        queue.addOperationWithBlock{()->Voidin        letimg3=Downloader.downloadImageWithURL(imageURLs[2])                NSOperationQueue.mainQueue().addOperationWithBlock({            self.imageView3.image=img3        })     }        queue.addOperationWithBlock{()->Voidin        letimg4=Downloader.downloadImageWithURL(imageURLs[3])                NSOperationQueue.mainQueue().addOperationWithBlock({            self.imageView4.image=img4        })     }}</span>


上面用了addOperationWithBlock这个方法,接下来我们加入了回调block重新处理下

<span style="font-size:14px;">@IBAction func didClickOnStart(sender: AnyObject) {        queue = NSOperationQueue()    let operation1 = NSBlockOperation(block: {        let img1 = Downloader.downloadImageWithURL(imageURLs[0])        NSOperationQueue.mainQueue().addOperationWithBlock({            self.imageView1.image = img1        })    })        operation1.completionBlock = {        print("Operation 1 completed")    }    queue.addOperation(operation1)        let operation2 = NSBlockOperation(block: {        let img2 = Downloader.downloadImageWithURL(imageURLs[1])        NSOperationQueue.mainQueue().addOperationWithBlock({            self.imageView2.image = img2        })    })        operation2.completionBlock = {        print("Operation 2 completed")    }    queue.addOperation(operation2)            let operation3 = NSBlockOperation(block: {        let img3 = Downloader.downloadImageWithURL(imageURLs[2])        NSOperationQueue.mainQueue().addOperationWithBlock({            self.imageView3.image = img3        })    })        operation3.completionBlock = {        print("Operation 3 completed")    }    queue.addOperation(operation3)        let operation4 = NSBlockOperation(block: {        let img4 = Downloader.downloadImageWithURL(imageURLs[3])        NSOperationQueue.mainQueue().addOperationWithBlock({            self.imageView4.image = img4        })    })        operation4.completionBlock = {        print("Operation 4 completed")    }    queue.addOperation(operation4)}</span>
取消的操作
为了验证取消操作,我们在导航栏上加了取消按钮。假设我们有四个任务,#3依赖#2,#2依赖任务#1,#4没有依赖。

下面是取消部分代码

<span style="font-size:14px;">@IBAction func didClickOnCancel(sender: AnyObject) {                self.queue.cancelAllOperations()    }</span>


这个需要关联取消按钮的,并且在这个方法didClickOnStart中加入如下两行代码:

<span style="font-size:14px;">operation2.addDependency(operation1)operation3.addDependency(operation2)</span>


然后改变回调方法,加上日志

<span style="font-size:14px;">operation1.completionBlock = {            print("Operation 1 completed, cancelled:\(operation1.cancelled) ")        }</span>


其他几个类似这样的。

点开始按钮之后,再迅速按取消,结果如下:

#1已经开始了,所以取消没有任何结果,但#2和#3都依赖于#1,所以#2和#3都被取消了,但#4没有任何关联,则和#1一起执行了。效果图如下:

水平有限,有机会再回来修改。









1 0