文章目录
- @[TOC](文章目录)
- 前言
- 理解“块”这一概念
- 块的基础知识
- 块的内部结构
- 全局块,栈块,堆块
- 为常用的块类型创建typedef
- 用handler块降低代码分散程度
- 用块引用其所属对象时不要出现保留环
- 多用派发系列,少用同步锁
- 多用GCD,少用performSelector方法
- 掌握GCD及操作对列的使用时机
- 通过Dispatch Group机制,根据系统资源状况执行任务
- 使用dispatch_once来执行只需运行一次的线程安全代码
- 不要使用dispatch_get_current_queue
文章目录
- @[TOC](文章目录)
- 前言
- 理解“块”这一概念
- 块的基础知识
- 块的内部结构
- 全局块,栈块,堆块
- 为常用的块类型创建typedef
- 用handler块降低代码分散程度
- 用块引用其所属对象时不要出现保留环
- 多用派发系列,少用同步锁
- 多用GCD,少用performSelector方法
- 掌握GCD及操作对列的使用时机
- 通过Dispatch Group机制,根据系统资源状况执行任务
- 使用dispatch_once来执行只需运行一次的线程安全代码
- 不要使用dispatch_get_current_queue
前言
块与大中枢派发
开发应用程序时,应该多留意多线程问题。即使开发的应用程序用不到多线程,他们仍可能是多线程的,因为系统框架通常会在UI线程之外再使用一些线程来执行任务。
当前多线程编程的核心是“块”与“大中枢派发”。块是一种可以在C,C++,及Objective-C代码中使用的“词法闭包”,借此机制,开发者可以将代码像对象一样传递,令其在不同环境下运行。在定义“块”的范围内,它可以访问到其中的全部变量
GCD是一种与块有关的技术,它提供了对线程的抽象,这种抽象基于“派发队列”。开发者可以将块排入队列中,由GCD负责处理所以调度事宜。GCD会根据系统资源情况,适时的创建,复用,摧毁后台线程,以便处理每个队列。GCD还可以方便的完成常见编程任务,比如编写“只执行一次线程安全的代码”或者根据可用的系统之一来兵法之行多个操作。
提示:以下是本篇文章正文内容,下面案例可供参考
理解“块”这一概念
- “块”可以实现闭包。这项特性是作为”拓展“加入GCC编译器中的,在近期版本中的Clang中都可以使用。这是一个位于C语言层面的特性,因此,只要有支持此特性的编译器,以及能执行块的运行期组件,就可以在C,C++,Objective-C,Objective-C++,代码中使用。
块的基础知识
- 块与函数类似,块是直接定义在另一个函数里表示的。和定义它的那个函数共享同一个范围内的东西。块用符号“^”来表示。后面跟着一对花括号,括号里是块的实现代码。
- 块其实就是一个值,而且有其相关类型。与int,float,或Objectiove-C对象一样,也可以把这个歌块付给变量,然后像使用其他变量那样使用它。块类型的语法与函数类。
void (^someBlock) () = ^{
};
- 定义了一个名为someBlock的变量。由于变量名写在正中间,所以看上去有点怪。
return_type (^block_name)(parameters)
int (^addBlock)(int a, int b) = ^(int a, int b){
return a + b;
};
int add = addBlock(2,5);
//add = 7;
- 块的强大之处在于:声明他的范围里,所以变量都可以为其所捕获。也就是说,那个范围里的全部变量,在块里仍然可以使用。默认情况下,为快所捕获的变量,是不可以在块里修改的。如果声明变量的时候加上_bolk修饰符,就可以在块内修改了。
- 如果块所捕获的变量是对象类型,那么就会自动保留它。系统在释放这个块的时候,也会将其一并释放。块本身可以视作对象,实际上在其他OC所能响应的选择子中途,有很多块也可以响应。最重要的是:块本身也和其他对象一样,有引用计数。当最后一个指向块的引用移走之后,块就回收了。回收时也会释放块所捕获的变量。以便平衡捕获时所执行的保留操作。
- 如果将块定义在Objective-C类的实例丰富中,那么除了可以访问类的所以实例变量之外,还可以使用self变量。块总能修改实例变量,所以在声明时无需加_block。如果通过读取或写入操作捕获了实例变量,那么也会自动把self变量一并捕获。因为实例变量时与self所指代的实例关联在一起的。
- self也是一个对象,块在捕获它时也会将其保留。如果self所指代的那个对象同时也保留了块,那么这种情况通常会导致“保留环”。
块的内部结构
- 每个Objective-C对象都占据着某个内存区域。因为实例变量的个数及对象所包含的关联数据互不相同,所以每个对象所占内存区域也有大有小。块本身是对象,在存放块对象的内存区域中,首个变量是指向Class对象的指针,叫做isa。其余内存里含有块对象政策运转所需的各种信息。
- 在内存布局中,最重要的是invoke变量。这是一个函数指针,指向块的实现代码。函数原型至少要接受一个void*型的参数,此参数代表块。块其实就是一种代替函数指针的语法结构,原来使用函数指针时,需要用不透明的void指针来传递状态。改用块之后,可以吧原来用标准的C语言特性所编的代码封装成简明且易用的接口。
- descriptor变量指向结构体的指针,每个块里都包含此结构体。其中声明了块对象总体大小,还声明了copy与dispose这两个辅助函数所对应的函数指针。辅助函数在拷贝及丢弃块对象时运行,其中会执行一些操作,比方说,钱折腰保留捕获的对象,后者将之释放。
- 块还会把他锁捕获的所有变量都拷贝一份。这些拷贝对象放在descriptor变量后面,捕获了多少个变量,就要占据多少内存空间。(拷贝的是 指向这些对象的指针变量)。因为执行块时,要把变量从内存中读出来,invoke函数需要把块作为对象传进来。
全局块,栈块,堆块
- 定义块的时候,所占内存区域是分配在栈中的。块只在定义他的那个范围内有效。
- 可以给块对象发送copy消息拷贝来解决这个问题。这样的话,就可以把块从栈复制到堆。拷贝后的块,可以定义在他的那个范围之外使用。一旦复制到堆上,块就变成了带引用计数的对象了。后续的复制操作都不会真的复制,只是递增块对象的引用计数。如果不在使用这个块,应该将其释放。在ARC环境下会自动释放,手动管理引用计数需要自己来调用release方法。当引用计数降为0后,“分配在堆上的块”会像其他对象一样,为系统所回收。“分配在栈上的块”无需明确释放。因为栈内存本来就会自动回收。
- 全局块不会不做任何状态,运行时也无需有状态来参与。块所使用的整个内存区域,在编译期已经完全确定了。因此,全局块可以声明在全局内存里,而不需要在每次用到的时候与栈中创建。全局块的拷贝相当于一个空操作,因为全局块绝不可能为系统回收。相当于一个单例。
为常用的块类型创建typedef
- 每个块都有用其固有类型,因而可以将其复制给适当类型的变量。这个类型由块所接受的参数机器返回值组成。
- 与其它类型的变量不同,在定义块变量时,要把变量名放在类型之中,而不要放在右侧。可以起一个更为易读的名字来表示块,把块的类型隐藏在后面。
typedef int(^EOCSomeBlock) (BOOL flag, int value);
- typedef关键字用于给类型起一个易读的名。
- 声明变量时,要把名称放在类型中间,并在前面加上^符号,而定义新类型时也得这么做。上面的语句向系统中新增了一个名为EOCSomeBlock的类型,此后,直接使用新类型即可。
- 与定义的其他变量一样,变量类型在左边,变量名在右边。 通过这个特性,可以把使用块的API做的更容易一些。类里面有些方法可能需要用块来做参数,比如执行异步任务时所用的completion handler。参数就是块,但凡遇到这种情况,都可以通过定义别名使代码更为易读。
- 定义方法参数所用的块类型语法,又和定义变量时不同。若能把方法签名中的参数类型写成一个词,那么读起来就顺口多了。于是,可以给参数类型七个别名,然后使用此名称来定义。
- 使用类型定义还有一个好处,就是当你打算重构块的类型签名时会很方便。
- 最后在使用块类型的类中定义这些typedef,而且应该把这个类的名字加在由typedef所定义的新类型的后面。这样可以阐明块的用途。还可以用typedef给同一个块签名类型创建数个别名。
- 如果由好几个类都要执行相似但各有区别的异步任务,而这几个类又不能防乳同一个体系继承,每个类就应该有自己的completion handler类型。这个几个completion handler的签名签名也许完全相同,但最好还是在每个类里都各自定义一个别名,而不要共用一个名称。繁殖,若这些类能纳入同一个继承中,则应该将类型定义语句放在超类中,供子类使用。
用handler块降低代码分散程度
- 为用户界面编码时,常用范式“异步执行任务”。这种范式的有点在于:处理用户界面的现实及触摸操作所用的线程,不会因为要执行I/O或网络通信这类好事的任务而阻塞。这个线程通常称为主线程。如果把执行异步任务做成同步的,那么在执行任务时,用户界面就变得无法响应用户输入了。
- 异步方法在执行完任务后,需要以某种手段通知相关代码。比如通过委托协议,令关注此事件的对象遵循该协议。对象成为delegate后,就可以在相关事件发生时得到通知。
- 如果用块来写一个从URL中获取数据的类,嗲吗会更加氢气。块可以令这种API变得更为紧致,同时令开发者调用起来更方便。把completion handler定义为块类型,将其当作参数传递给方法。这种模式和委托协议很像,但是可以在调用方法时直接以内联形式定义completion handler,以此方式来使用“网络数据获取器”,可以令代码比原先易读
- 与委托模式的代码相比。块写出来的代码更简洁。异步任务执行完毕后所需运行的业务逻辑,和启动一步任务所应的代码放在了一起。而且,由于块声明在创建获取器的范围里,所以它可以访问此范围内的全部变量。
- 委托模式有一个缺点,如果类要分别使用多个获取器下载不同数据,那么就得在delegate回调方法里根据传入的获取器参数切换。这样做会令delegate回调方法变得很长,而且还要把网络数据获取器对象保存为实例变量。用块来改写的好处是:无需保存选择器,也无需在回调方法里切换。每个completion handler的业务逻辑,都是和相关的获取器对象一起来定义的。
- 可以分别用两个处理程序来处理操作失败和成功的不同情况。也可以吧处理失败的情况所需的代码,与处理正常情况所用的代码,都封装到同一个completion handler块里。由于成功和失败的情况分开处理,所以调用此API的代码也会按照逻辑,吧应对成功和失败的代码分开来写,令代码可读性更高。
- 如果把全部逻辑写在一起,会令块变得比较长,比较复杂。都是使用一个块来处理也更为灵活。调用API的代码也可能会在处理成功响应的过程中发现错误。如果把成功和失败情况交给老公不同的处理程序来负责,那么就没办法共享同一份错误处理代码里,除非吧这段代码单独放在一个方法里,但是这样违背了我们想把全部逻辑代码放在一处的初衷。
- 有时候在相关事件点执行回调操作,这种情况也可以使用handler块。
- 基于handler来设计API还有一个原因,某些代码必须运行在特定的线程上。因此,最好能由调用API的人来决定handler应该运行在哪个线程上。NSNotificationCenter就是这种APi,它提供了一个方法, 调用者可以经由此方法来注册想要接收的通知等到相关事件发生时,通知中心就会执行注册号的那个块。调用者可以指定某个块应该安排在哪个执行队列里。若没有指定队列,按照默认方法执行,也就是由投递通知的那个线程来执行。
用块引用其所属对象时不要出现保留环
- 使用块时,若不仔细思量,很容易导致“保留环”。
- 如果设计API时用到了completion handler这样的回调块,那么很容易形成保留环。一般来说,只要适时清理掉环中的某个引用,即可解决此问题。
- 如果completion handler块所引用的对象最终又引用了这个块本身,那么就会出现保留环。
- 如果块所捕获的对象直接或间接保留了块本身,那么就得当心保留环
- 一定要找一个适当的时机解除保留环,而不能把责任推卸给API的调用者。
多用派发系列,少用同步锁
- 在Objective-C中,如果有多个线程要执行同一份代码,那么又是可能会出现问题。这个时候通常使用锁实现同步机制。在GCD出现前,有两种方法。
- 第一种是采用内置的“同步块”。这种写法会根据给定对象,自动创建一个锁,并等待块中的代码执行完毕,执行到代码结束时,锁就释放了。
- 另一种方法是直接使用NSLock对象。也可以使用NSRecursiveLock这种“递归锁”,线程能够多次持有该锁,而不会出现死锁现象。
- 在极端情况下,同步锁会导致死锁,效率也不一定提高,如果直接使用锁对象,一旦遇到死锁,就会非常麻烦。
- 代替方案就是使用GCD,能更简单,更高效的为代码加锁。
- 使用“串行同步队列”,将读取操作都安排在同一个队列里,即可保证数据同步。这种方法可以更简单,高效的代替同步块或锁对象。
- 这种模式的思路是:把设置操作与获取操作都安排在序列化的队列里执行,这样的话,所有针对属性的访问操作就都同步了。为了使块代码能够设置局部变量,获取方法中用到了__block语法。若是抛开这一点,这种写法比签名那些更简洁。全部加锁任务都在GCD中处理,而GCD是在相当深的底层来实现的。于是能够做许多优化。
- 还可以进行一步优化,设置方法不一定非得是同步的,设置实例变量所用的块,并不需要向设置方法返回什么值。
- 把同步派发改为异步派发时,从调用者的角度看,这个小改动可以提升设置方法的执行速度,而读取操作与写入操作依然会按照顺序执行。但是也有一个坏处:这种写法比原来慢。因为执行异步派发时,需要拷贝块。若拷贝块的时间明显超过执行块的时间,真宗方法会比原来慢。
- 多个获取方法可以并发执行,而获取次方法与设置方法之间不能并发执行,利用这个特点,可以写出来更快的代码。
- 在队列中,栅栏块必须单独进行,不能与其他块并行。这只是对并发队列有意义,因为串行队列中的块总是按照顺序来逐个执行。并发队列如果发现接下来的要处理的块是栅栏块,那么就一直要等到当前所有的并发块都执行完毕,才会单独执行这个歌栅栏块。栅栏块执行完毕后,在按照正常方式继续向下执行。
- 这种做法肯定会比使用串行队列要快。注意,设置函数也可以改用同步的栅栏块来实现,这样可能更高细哦啊。最好还是测一测每种方式的性能,选择最好的。
多用GCD,少用performSelector方法
- Objective-C本质上是一门非常动态的语言。NSObject定义了几个方法,可以随意调用。在出现了大中暑派发及块这样的新技术之后, 这几个方法就没有那么重要了。
- 最简单的:-(id)performSelector:(SEL)selector
- 与直接调用选择子等效。[object performSelector:@Selector(selectorName)];
[object selectorName]; - 这两行代码效果相同。
- 如果某个方法只是这么来调用,此方法就有一点多余。如果选择子是在运行期决定的,就等于在动态绑定上再次使用动态绑定。
- 还有一种用法,把选择子保存起来,等到某个事件发生之后在调用。不论哪个用法,编译器都不知道要执行的选择子是什么。必须等到运行期才能确定。但是在ARC环境下编译代码的话,编译器会发出警告。提示出现内存泄漏。因为编译器不知道将要调用的选择子是什么,也不了解方法签名和返回值,甚至不知道是不是有返回值。由于编译器不知道方法签名,也就没办法运用ARC的内存管理规则来判定返回值是不是应该释放。于是ARC不添加释放擦欧哦,这样做就可能导致内存泄漏,因为方法在返回对象时可能已经将其保留了。
- 选择子最多只能接收两个参数,也就是调用“performSelector:withObject :withObject:”这个版本。在参数不止两个的时候,没有对应的performSelector方法能够执行此种选择子。
- performSelector还可以延后执行选择子,或者将其放在另一个线程上进行。
- 这些方法太过局限,具备延后执行的那些方法无法处理带有两个参数的选择子。能够指定执行线程的方法也不是很通用。
- 如果改用其他方法,就没有这些限制。最常见的是改用块。performSelector系列方法提供的线程功能,都可以在大中枢派发机制中使用块来实现。延后执行可以使用dsidpatch_after来实现,在另一个线程上可以用dispatch_sync和dispatch_async来实现。
掌握GCD及操作对列的使用时机
- 要了解每项技巧的使用时机,如果选错了工具,那么编出来的代码就会难以维护。
- 对于那些只执行一次的代码来说,GCD的dispatch_once最为方便。在执行后台任务时,GCD不一定是最佳方式。还有一种技术叫做NSOperationQueue,开发者可以把操作以NSOperation的子类的形式放在队列中,这些操作也能并发执行。“操作队列”在GCD之前就有了,其中某些设计原理因操作队列流行,GCD就是基于这些原理构造的。
- GCD是纯C的API,操作队列是Objective-C的对象。在GCD种,任务用块来表示,块是一个轻量级的数据结构。与之相反,操作是一个更重量级的Objective-C对象。但是GCD并不是总是最佳方案,有时候采用对象所带来的开销微乎其微,使用完整对象所带来的好处反而超过其缺点
- 用NSOperationQueue类的“addOperationQueueWithBlock:”方法搭配NSB咯出口O普洱阿嚏哦你类来使用操作队列,语法和纯GCD类似。使用NSOperation和NSOperationQueue的好处如下:
一:取消某个操作:如果使用操作队列,取消操作很容易。运行任务之前,可以在NSOperation对象上调用cancel方法,该方法会设置对象内的标志位,用以表明此任务不需要执行。已经启动的任务无法取消。若不使用操作队列,把块安排到GCD队列,就无法取消了。那套架构是“安排好任务之后就不管了”。开发者可以在应用程序层之间实现取消功能。这样做需要编写很多代码。
二:指定操作间的依赖关系。一个操作可以包含其他多个操作。开发者能够指定操作之间的依赖关系,使特定的操作必须在另外一个操作顺利执行完毕后执行。
三:通过键值观测机制监控NSOperation对象的属性。NSOperation对象有多个属性都适合使用KVO来监听。
四:指定操作的优先级。操作的优先级便是此操作与队列中其他操作之间的优先关系。优先级高的操作先执行,优先级低的操作后执行。操作队列的调度方法虽不透明,但是必须经过一番深思熟虑在写成。GCD没有直接实现此功能的方法。但是GCD的队列确实有优先级。那是针对整个队列来说的,而不是针对每个块。NSOperation对象也有线程优先级,这决定了运行此操作的线程处在何种优先级上。用GCD也可以实现此功能,但是采用队列更简单,只要设置一个属性即可。
五:崇勇NSOpera提哦你对象。系统内设置了一些NSOperation对象的子类供开发者使用。这些子类就是普通的Objective-C对象,能够存放任何信息。对象在执行时可以充分利用存于其中的信息,而且还可以随意调用定义在类中的方法。 - 操作队列有很多地方都胜过派发队列。操作队列提供了多种执行任务的方法,都是写好的,直接就能用。开发者不在编写复杂的调度器,也不用自己来实现取消操作或指定操作的优先级的功能。
- 有一个API选用了操作队列而非派发队列,NSNotificationCenter。开发者可以注册监听器,以便在发生相关事件时得到通知。这个方法接收的参数类型是块,不是选择子。
通过Dispatch Group机制,根据系统资源状况执行任务
- dispatch group是GCD的一项特性,能够把任务分组。调用者可以等待着族任务执行完毕,也可以在提供回调函数之后继续往下执行,这组任务完成时,调用者会接到通知。这个功能有许多用途,最重要的是吧将要并发执行的多个任务合为一组,调用者就可以知道这些任务何时能全部执行完毕。
- dispatch group就是一个简单的数据结构,这种结构彼此之间没有什么区别,它不像派发队列,后者还有一个区别身份的标识符。想把数组编组,有两种方法。第一种是利用函数。
他是普通dispatch_async函数的辩题,比原来多一个参数,用于表示带执行的块所属的组。
前者能够使分组里正要执行的任务数递增,后者使其递减。调用前者之后,必须调用后者。和引用计数类似。
此函数接收两个参数,一个是要等待的group,另一个是代表等待时间的timeout。timeout表示函数在等待dispatch group执行完毕时,应该阻塞多久。如果执行dispatch group所用时间小于timeout,返回0,否则返回非0值。
也可以使用这个函数等待dispatch group执行完毕。开发者可以向此函数传入块,等dispatch group执行完毕后,块会在特定的线程上执行。假如当前线程不应阻塞,开发者又想在那些任务全部完成时得到通知,此做法就很有必要。 - 一系列任务可归入一个dispatch group之中,开发者可以在这组任务执行完毕时获得通知。
- 通过dispatch group,可以在并发式派发队列里同时执行多项任务。此时GCD会根据系统资源状况调度这些并发执行的任务。
使用dispatch_once来执行只需运行一次的线程安全代码
- 单例模式的常见实现模式为:在类中编写方法,该方法只会返回全类公用的单例实例,不会每次调用时都创建出新的实例。
- 为保证线程安全,上述代码将创建单例实例的代码包裹在同步块里。
- GCD使用这个函数让实现单例模式更容易。此函数接收类型为dispatch_once_t的特殊参数,还接收块参数。对于给定的标记来说,该函数保证相关的块必定执行,且仅执行一次。首次调用这个函数时,必然执行其中的代码,最重要的是这个操作线程完全安全。对于只执行一次的函数来说,每次调用函数时传入的标记都必须完全相同。
- dispatch_once可以简化代码并彻底保证线程安全,无需担心加锁或同步。所有问题都由GCD在底层处理。每次调用时都必须使用完全相同的标记,所以标记要声明成static。吧该变量定义在static作用域中,保证编译器每次执行sharedInstance方法时都会复用这个变量,不会创建新的变量。
- dispatch_once更高效。他没有使用总量级的同步机制。采用“原子访问”来查询标记,来判断对应代码是否已经执行。
不要使用dispatch_get_current_queue
- 使用GCD时,经常要判断当前的代码正在哪个队列上执行,向多个队列派发任务时也如此。
- dispatch_queue_t dispatch_get_current_queue
- 这个函数返回当前证在执行的队列。用一种典型的错误用法“反模式”,就是用它检测当前队列是不是某个特定的队列。试图以此避免执行同步派发时可能遭遇的死锁问题。
- dispatch_get_current_queue函数的行为常常和开发者预期的不同,此函数已经被废弃。只应该做调试之用。
- 派发队列是按层级来阻止的,无法单独使用某个队列对象来描述“当前队列”这一概念。
- dispatch_get_current_queue函数用于解决由不可重入的代码所引发的思索,但是能用此函数解决的问题,也能用“队列特定数据来解决”