【Effective Objective-C】—— 块与大中枢派发

文章目录

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

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

概述

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

理解“块”这一概念

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

块的基础知识

块的形式:

^{
	//Block implementation here
}
  • 块其实是个值,有自己的相关类型,可以赋值等操作。

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

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

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

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

块的内部结构

块对象的内存布局:
【Effective Objective-C】—— 块与大中枢派发_第1张图片

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

全局块、栈块、堆块

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

要点:

  1. 块是C、C++、Objective-C的词法闭包
  2. 块可接受参数,也可返回值。
  3. 块可以分配在栈或堆上,也可以是全局的。分配在栈上可以拷贝到堆上。

为常用的块类型创建typedef

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

typedef return_type(^BlockName)(parameters)

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

BlockName block = ^(parameters){
		//Implementation
}

要点

  1. typedef重新定义块类型,可令块变量用起来更加简单。
  2. 定义新类型时应遵从现有的命名习惯,勿使其名称与别的类型相冲突。
  3. 不妨为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需修改相应typedef中的块签名即可,无须改动其他typedef

用handler块降低代码分散程度

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

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

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

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

要点:

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

用块引用其所属对象时不要出现保留环

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

【Effective Objective-C】—— 块与大中枢派发_第3张图片
【Effective Objective-C】—— 块与大中枢派发_第4张图片

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

【Effective Objective-C】—— 块与大中枢派发_第6张图片
这样一来,只要下载请求执行完毕,保留环就解除了。

要点:

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

多用派发队列,少用同步锁

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

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

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

_lock = [[NSLock alloc] init];

- (void)synchronizedMthod {
	[_lock lock];
	//Safe
	[_lock unlock];
}

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

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

【Effective Objective-C】—— 块与大中枢派发_第12张图片
【Effective Objective-C】—— 块与大中枢派发_第13张图片
设置函数也可以该用同步的栅栏块来实现。

要点:

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

多用GCD,少用performSelector系列方法

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

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

所以采用替代方案,最主要的替代方案就是使用块。
例如要延后执行某项任务,可以有下面两种实现方式:
【Effective Objective-C】—— 块与大中枢派发_第14张图片
优先考虑第二种。
若想把任务放在主线程上执行,也可以有下面两种形式:
【Effective Objective-C】—— 块与大中枢派发_第15张图片
优先考虑后者。

要点:

  • 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函数的变形,比原来多一个参数,用于表示待执行块所归属的组。
【Effective Objective-C】—— 块与大中枢派发_第16张图片
还有种方法能指定任务所属的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 函数,很容易就能实现此功能。
  • 标记应该声明在staticglobal作用域中,这样的话,在把只需执行一次的块传给 dispatch_once函数时,传进去的标记也是相同的。

不要使用dispatch_get_current_queue

要点:

  • dispatch_get_current_queue函数的行为常常与开发者所预期的不同。此函数已经废弃,只应做调试之用。
  • 由于派发队列是按层级来组织的,所以无法单用某个队列对象来描述“当前队列”这一概念。
  • dispatch_get_current_queue 函数用于解决由不可重入的代码所引发的死锁,然而能用此函数解决的问题,通常也能改用“队列特定数据”来解决。

你可能感兴趣的:(block底层,objective-c,ios,开发语言)