irpas技术客

【Effective Objective-C】—— 块与大中枢派发_轩墨?

网络 7593

文章目录 概述理解“块”这一概念块的基础知识块的内部结构全局块、栈块、堆块要点: 为常用的块类型创建typedef要点 用handler块降低代码分散程度要点: 用块引用其所属对象时不要出现保留环要点: 多用派发队列,少用同步锁要点: 多用GCD,少用performSelector系列方法要点: 掌握GCD及操作队列的使用时机要点: 通过Dispatch Group机制,根据系统资源状况来执行任务要点: 使用dispatch_once来执行只需运行一次的线程安全代码要点: 不要使用dispatch_get_current_queue要点:

具体Blocks底层的学习可以参考这篇博客:【iOS开发】—— 一文搞懂blocks底层源码

概述

当前多线程编程的核心就是“块”与“大中枢派发”。GCD是一种与块相关的技术,它提供了对线程的抽象,而这种抽象则基于“派发队列”(dIspatch queue)。开发者可将块排入到派发队列中,由GCD负责处理所有的调度事宜。

理解“块”这一概念

“块”是一种可在C、C++以及Objective-C代码中使用的“词法闭包”,借助它,开发者可以将代码像对象一样传递,令其在不同环境下运行。还有个关键的地方,在定义“块”的范围内,它可以访问到其中的全部变量。此项语言特性是作为“扩展”而加入GCC编译器中的。

块的基础知识

块的形式:

^{ //Block implementation here }

块其实是个值,有自己的相关类型,可以赋值等操作。

块和对象一样,也适用ARC管理机制。

块将块定义在Objective-C实例方法中,除了可以访问类的所有实例变量之外,还可以捕获self变量,因为实例变量和self所指代的实例关联在一起。

直接访问实例变量和通过self访问实例变量是等效的。通过属性访问实例变量,需要指明self。

self也是一个对象,因而在块捕获它时;如果self所指代的那个对象同时也保留了块,就有可能形成“保留环”。

块的内部结构

块对象的内存布局:

首个变量是指向Class对象的指针,该指针叫做isa。最重要的指针就是invoke变量,这是一个函数指针,指向块的实现代码。其原型至少要接受一个void*型的参数,此参数代表块。descriptor变量是指向结构体的指针,每个块里都有此结构体,声明了块对象的总体大小,还声明了copy和dispose这两个辅助函数所对应的函数指针。辅助函数是在拷贝及丢弃块对象时运行。块会把捕获的所有变量都拷贝一份。这些拷贝放在descriptor变量的后面。拷贝的不是这些对象本身,而是指向这些对象的指针变量。 全局块、栈块、堆块

定义块时,其所占的内存区域是分配在栈上,所以超出它的定义范围会被销毁。为解决此问题,可以利用copy方法,拷贝到堆上,成为堆块,这样就可以利用ARC机制了。除了以上两种类型的块,还有一种全局块。这种块不会捕获任何状态,且这种块所使用的内存区域,在编译器已经完全确定。因此,声明全局块可以声明在全局内存中,而不需要在每次操作的时候在栈中创建,另外,全局块的copy是空操作,所以,全局块绝对不会被系统所回收。这种块其实相当于单例。

要点: 块是C、C++、Objective-C的词法闭包块可接受参数,也可返回值。块可以分配在栈或堆上,也可以是全局的。分配在栈上可以拷贝到堆上。 为常用的块类型创建typedef

为了隐藏复杂的块类型,所以利用typedef关键字用于给类型起个易读的别名。

typedef return_type(^BlockName)(parameters)

这样定义之后,下次就可以直接使用:

BlockName block = ^(parameters){ //Implementation } 要点 以typedef重新定义块类型,可令块变量用起来更加简单。定义新类型时应遵从现有的命名习惯,勿使其名称与别的类型相冲突。不妨为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需修改相应typedef中的块签名即可,无须改动其他typedef。 用handler块降低代码分散程度

为用户界面编码时,一种常用的范式就是“异步执行任务”。这种范式的好处在于:处理用户界面的显示及触摸操作所用的线程,不会因为要执行I/O或网络通信这类耗时的任务而阻塞。这个线程通常称为主线程。

异步方法在执行完任务时,需要以某种手段通知相关代码,实现此功能的方法很多,常用的一个设计一个委托协议,令关注此事件的对象遵从该协议。但是使用委托协议有很多缺点:

代码不清晰,代码复杂。如果类要分别使用多个获取器下载不同的数据,那么就得在delegate回调方法里根据传入的获取器参数来切换。

所以我们改用块来写,有时由于成功和失败的情况要分开处理,所以就可以像下面这么写: 基于handler来设计API还有个原因,就是某些代码必须运行在特定的线程上,比方说Cocoa与Cocoa Touch中的UI操作必须在主线程上执行。这就相当于GCD中的“主队列”。因此,最好能由调用API的人来决定handler应该运行在哪个线程上。这里可以借助NSNotificationCenter来实现。

要点: 在创建对象时,可以使用内联的handler块将相关业务逻辑一并声明。有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,而若改用handler 块来实现,则可直接将块与相关对象放在一起。设计API时如果用到了handler块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。 用块引用其所属对象时不要出现保留环

举个栗子来说明:下面这个类提供了一套接口,调用者可由此从某个URL中下载数据。在启动获取器时,可设置completion handler,这个块会在下载结束之后以回调方式执行。为了能在下载完成后通过p_requestCompleted方法执行调用这所指定的块,这段代码需要把completion handler保存到实例变量中:

仔细看这段代码,就会发现其中存在一个保留环,因为completion handler块要设置_fetchedData实例变量,所以它要捕获self变量,这就是说handler保留了创建网络数据获取器的那个EOCClass实例,但是EOCClass又通过strong实例变量保留了获取器,最后获取器对象又保留了handler块。 将p_requestCompleted修改成下面这样就可以了:

这样一来,只要下载请求执行完毕,保留环就解除了。

要点: 如果块所捕获的对象直接或间接地保留了块本身,那么就得当心保留环的问题。一定要找个合适的时机解除保留环,而不能把责任推给API的调用者。 多用派发队列,少用同步锁

在Objective-C中,如果有多个线程要执行同一份代码,那么有时就可能出问题。这种情况下就要使用锁来实现某种同步机制。在GCD出现之前,有两种方法,一种是采用内置的“同步块”:

- (void)synchronizedMethod { @synchronized(self) { //safe } }

这种写法会根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕。执行到这段代码的结尾处,锁就释放了。但是若是频繁使用锁,会降低代码效率。因为共用同一个锁的那些同步块,都必须按顺序执行。 另外一个方法是直接使用NSLock对象:

_lock = [[NSLock alloc] init]; - (void)synchronizedMthod { [_lock lock]; //Safe [_lock unlock]; }

也可以使用NSRecursiveLock这种“递归锁”,线程能够多次持有该锁,而不会出现死锁现象。 这两种方法都很好,但是也有缺陷。比方说,在极端的情况下,同步块会导致死锁,另外其效率也不会很高。 所以就有了GCD来更简单更高效的形式为代码加锁。使用“串行同步队列”。将读取操作及写入操作都安排在同一个队列中,即可保证数据同步。 其用法如下: 此模式的思路是:把设置操作与获取操作都安排在序列化的队列里执行,这样的话,所有针对属性的访问操作都同步了。 可以进一步优化,将设置方法改为: 这次将同步派发改为了异步派发,可以提升设置方法的执行速度。执行异步派发时,需要拷贝块,若拷贝块所花费的时间明显超过执行块说花的时间,则这种做法将比原来的慢。

多个获取方法可以并发执行,而获取方法与设置方法不能并发执行,利用这个特点,就可以体现出GCD的好处了。这次不用串行并列,而改用并发队列: 但是只是这样还无法正确实现同步。所有读写操作和写入操作都会在同一个队列上执行,不过由于并发队列,所以读取和写入操作都是随意执行的,而我们并不想让这些操作随意执行,此问题用一个简单的GCD功能来解决,就是“栅栏”。下列函数可以向队列中派发块,将其作为栅栏使用: 在队列中,栅栏必须单独执行,不能与其他块并行。 在本例中,可以用栅栏块来实现属性的设置方法,之后对属性的读取操作依然可以并发执行,但是写入操作却必须单独执行。实现代码如下:

设置函数也可以该用同步的栅栏块来实现。

要点: 派发队列可用来表述同步语义(synchronization semantic),这种做法要比使用 @synchronized 块或 NSLock对象更简单。将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,而这么做却不会阻塞执行异步派发线程。使用同步队列及栅栏块,可以令同步行为更加高效。 多用GCD,少用performSelector系列方法

通过performSelector:调用方法,会程序发出内存泄露的警示信息。原因在于,编译器只有在运行期时才能确定选择子,所以编译器并不知道将要调用的选择子是什么,所以就没办法运用ARC的内存管理规则来判定返回的值是不是应该释放。鉴于此,ARC采用了比较谨慎的做法,就是不添加释放操作。然而这么做可能导致内存泄漏,因为方法在返回对象时可能已经将其保留了。 使用performSelector方法的弊端:

可能会导致内存泄漏。若返回值类型为C语言的结构体,不可以使用此方法传递参数时,参数类型只能是id对象类型。

所以采用替代方案,最主要的替代方案就是使用块。 例如要延后执行某项任务,可以有下面两种实现方式: 优先考虑第二种。 若想把任务放在主线程上执行,也可以有下面两种形式: 优先考虑后者。

要点: performSelector系列方法在内存管理方面容易有疏失。它无法确定将要执行的选择子具体是什么,因而ARC编译器也就无法插入适当的内存管理方法。performSelector系列方法所能处理的选择子太过局限了,选择子的返回值类型及发送给方法的参数个数都受到限制。如果想把任务放在另一个线程上执行,那么最好不要用performSelector系列方法,而是应该把任务封装到块里,然后调用大中枢派发机制的相关方法来实现。 掌握GCD及操作队列的使用时机

在执行后台任务时,GCD不一定是最佳方式,还有一种技术叫做NSOperationQueue,开发者可以把操作以NSOperation子类的形式放在队列中,而这些操作可以并发执行。

两者的区别 GCD是纯C的API,而操作队列则是Objective-C的对象。在GCD中,任务用块来解释,而块是个轻量级数据结构。与之相反,“操作”则是个更为重量级的Objective-C对象,虽说如此,但GCD并不总是最佳方案,有时采用对象所带来的的开销微乎其微,使用完整对象所带来的好处反而大大超过其缺点。

用NSOperationQueue类的“addOperationWithBlock:”方法搭配NSBlockOperation类来使用操作队列。

要点: 在解决多线程与任务管理问题时,派发队列并非唯一方案。操作队列提供了一套高层的Objective-C API,能实现纯GCD所具备的绝大部分功能而且还能完成一些更为复杂的操作,那些操作若改用GCD 来实现,则需另外编写代码。 通过Dispatch Group机制,根据系统资源状况来执行任务

Dispatch Group是GCD的一种特性,能够把任务分组。 为任务分组有下面两种方法: 第一种: 它是普通dispatch_async函数的变形,比原来多一个参数,用于表示待执行块所归属的组。 还有种方法能指定任务所属的dispatch group: 前者能使分组里的任务数递增,后者能使之递减。调用了前者之后,必须有与之对应的后者才行。 下面这个函数可用于等待dispatch group执行完毕:

次函数要接受两个参数,一个是等待的group,另一个是代表等待时间的timeout值。timeout参数表示函数在等待dispatch group执行完毕时,应该阻塞多久。如果执行dispatch group所需的时间小于timeout,则返回0,否则返回非0值。此参数也可以取常量DISPATCH_TIME_FOREVER,这表示函数会等待dispatch group执行完毕完,而不会超时。 还可以换个方法: 开发者可以向此函数传入块,等dispatch group执行完毕后,块会在特定的线程上执行。

假如把块派给了当前队列(或者体系中高于当前队列的某个串行队列),就会导致死锁。若想在后台执行任务,则应该使用dispatch group。

要点: 一系列任务可归入一个dispatch group之中。开发者可以在这组任务执行完毕时获得通知。通过 dispatch group,可以在并发式派发队列里同时执行多项任务。此时 GCD 会根据系统资源状况来调度这些并发执行的任务。开发者若自己来实现此功能,则需编写大量代码。 使用dispatch_once来执行只需运行一次的线程安全代码

GCD引入了一项特性,能使单例实现起来更为容易。所用的函数是: 该函数的参数中有一个是dispatch_once_t的特殊参数,这个要将其声明在stastic或者 global作用域里。 首次调用该函数时,必然会执行块中的代码,最重要的一点在于,此操作完全是线程安全的。

要点: 经常需要编写“只需执行一次的线程安全代码”(thread-safe single-code execution)。通过GCD所提供的dispatch_once 函数,很容易就能实现此功能。标记应该声明在static或global作用域中,这样的话,在把只需执行一次的块传给 dispatch_once函数时,传进去的标记也是相同的。 不要使用dispatch_get_current_queue 要点: dispatch_get_current_queue函数的行为常常与开发者所预期的不同。此函数已经废弃,只应做调试之用。由于派发队列是按层级来组织的,所以无法单用某个队列对象来描述“当前队列”这一概念。dispatch_get_current_queue 函数用于解决由不可重入的代码所引发的死锁,然而能用此函数解决的问题,通常也能改用“队列特定数据”来解决。


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #effective #ObjectiveC #块与大中枢派发 #Queue