EffectiveOC的学习-7

这里是EffectiveOC的 37 - 46条 ,GCD和block

2016-08-05 | 阅读

37.理解block

block可以实现闭包,这项语言特性作为扩展而加入GCCClang编译器,可以在C,C++,Objective-CObjective-C++代码中使用。

block类型的语法结构如下:

return_type (^block_name)(parameters)

block的特点是能够截获声明范围内的变量,如 :

int a = 1;
int (^addBlock)(int b) = ^(int b){
	return a + b;
}
a = 3 ;
int add = addBlock(2);// add = 1+2

拥有截获变量的特性,所以我们说block可以实现闭包。

默认情况下,block捕获的变量,不能进行修改,但是可以在声明变量时,加上__block修饰符,以支持修改。

block捕获变量,所以block也会持有该变量。而block本身也是一个对象,会在自己释放时 释放持有的变量。 所以就会出现保留环的问题,在使用block时要注意这一点,防止保留环的产生。

如果将block定义在类的实例方法中,block可以访问类的实例变量,且在不需要声明__block的情况下,修改实例变量。这是因为当我们在block中访问实例变量时 ,如:

//...
^{
	_aVariable = @"b";
}

实际上相当于这种写法:

self->_aVariable=@"b";

block持有的不是这个实例变量,而是对象本身。所以需要注意这种情况,避免在直接访问实例变量而产生的保留环问题。

block的结构

block是一个对象,其内存结构如下图所示 :

首个属性指向Class对象指针,即isa。 最重要的是invoke属性,即函数指针,指向了block的实现代码,这个函数的第一个参数是void *类型,代表block本身。block是一种代替函数指针的语法结构,与函数指针的不同在于其会截获其他变量。

descriptor变量指向一个struct,其中记录了block的大小,还声明了copydispose两个函数,用于复制block和释放内存。

block会把它所捕获的所有变量都拷贝一份,放在descriptor变量后。 invoke函数的参数有block对象,就是为了在执行时,读取这些被捕获的变量。 变量的捕获发生在block的构造函数中,对于非__block的变量:

  • 普通的自动变量的值会被保存,编译器为这些复制的值设置为const,所以在block中不能进行修改。
  • 静态变量会将指针传入,所以可以进行修改。
  • 设置为__block的变量,会生成一个struct,这个struct中拥有指向自身的指针,所以可以进行修改。

block的类型

定义block时,其所在的内存区域是分配在栈中的,即block只在它的声明范围内有效。

void (^block)();
if( ... ) {
	block = ^{
		NSLog(@"block A");
	}
}else {
	block = ^ {
		NSLog(@"block B");
	}
}
block();

以上代码就有很多问题,定义在if/else语句中的两个block,其作用域与block声明不在一个地方。即在代码执行离开作用域时,声明的block有可能被编译器覆写,若覆写,则block内存不完整,程序崩溃。这里就会出现编译期决定的崩溃,有时编译后正常,有时编译后必定崩溃。

我们通过copy方法将block从栈复制到堆上,一旦复制到堆上,block就成为了一个带有引用计数的对象,之后执行的copy函数都只是递增引用计数,不再进行复制。

block分为三种 :

  • global : 全局的block不会捕捉如何变量,运行时也没有任何状态参与。其整个内存区域在编译期就已经完全确定了。 其声明在全局内存中,不需要在每次使用时在栈中创建。 copy方法是一个空操作,因为其不可能被系统回收。 全局block的存在,是一种对于性能的优化,避免对极其简单的block进行copydispose操作。
  • stack: 栈上的block,无需明确地释放,内存由系统进行管理。一般生成的block都是在栈上的。
  • heap : 堆上的block ,拥有引用计数。 所有的堆上的block都是由栈上复制而来。

38. 为常用的block类型创建typedef

使用typedef对block类型进行命名,以增加代码可读性。

typedef void(^xxxhandler)(NSData *data,NSError *error);
- (void)startWithCompletionHandler:(xxxhandler)completion;

39. 用handler block降低代码分散程度

通过block将一个流程的主要代码集中在一块,避免代码的分散。 一些异步方法如果过多的使用delegate,会导致整个流程的代码过于分散。如果使用block,将代码集中会提高可读性。

使用block的优点:

  • 代码集中,使用起来更加灵活,可读性高
  • 自动捕获变量, 避免无意义且繁琐的变量传递。

使用delegate的优点:

  • 无需考虑保留环问题
  • 代码分散,可以避免代码臃肿问题

40.使用block时要避免保留环

保留环的预防,主要看个人开发经验,不赘述

41. 多使用GCD, 少用同步锁

对于需要锁的地方,我们通常会使用synchronized处理,如 :

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

但是如果多处需要进行锁时,这种方法就不合适了,因为两处地方可能真正要锁的内容不同,如一处是处理属性A,一次是处理属性B,但两处同时使用self进行锁,明显会降低代码效率。

所以对于这种情况,我们可能会使用NSLock对象或者NSRecursiveLock.

GCD也为我们提供一种方案,通过使用串行同步队列serial synchronization queue:

_syncQueue = dispatch_queue_create("xxx.xxx.xxx",NULL);

- (void)something{
	dispatch_sync(_syncQueue,^{
		....
	});
}

GCD中还有一种barrier block, 作用在并发队列中,对于barrier block,其会等到之前的所有并发block执行完成后,再单独执行这个barrier block,然后才能继续之后并发执行block。 即对于concurrent queue,通过barrier可以做到部分并发,部分同步。特别适用于读写操作 :

_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);

- (NSString *)someString {
	__block NSString *localVar;
	// 同步  并发读。
	dispatch_sync(_syncQueue,^{
		localVar = _someString;
	});
	return localVar;
}

- (void)setSomeString:(NSString *)someString {
	// 异步 序列写
	dispatch_barrier_async(_syncQueue,^{
		_someString = someString;
	});
}

42. 多用GCD , 少用 performSelector

使用performSelector时,在ARC下会爆出警告 performSelector may cause a leak because its selector is unknown.

这个警告的原因,使用performSelector导致编译器不知道selector的返回值是什么。在正常情况下,编译器知道一个函数的返回值,ARC会对返回值进行一些操作,如当函数retain了返回值时,ARC会执行autoRelease。但是使用performSelector,ARC就不添加释放操作,这样可能会导致内存泄露。

performSelector的返回值类型是id,则函数的返回值只能是void或对象类型,如果是其他类型,就需要执行一些复杂的转换操作。其传递的参数也只能是id类型。

所以,一般来说,能够使用GCD,就用GCD来替代performSelectorperformSelector只用于复杂的动态性的需求,如JSPatch这种。

43. 掌握GCD及操作队列的使用时机

除了GCD,还有操作队列这种方式。NSOperationQueue为操作队列,为其添加NSOperation任务,可以进行顺序处理。相对于 GCD,优点如下:

  • 可以取消操作。 调用cancel方法就可以在任务运行之前进行取消。GCD本身无法进行取消。
  • 指定操作之间的依赖关系: 可以为多个操作之间设置依赖关系,即一个操作依赖于其他多个操作的完成。
  • NSOperation的监控: 由于任务是一个NSOperation对象,所以可以通过KVO等方法进行监控,进行比GCD更为精细的控制。
  • 指定操作的优先级 : 操作队列拥有调度算法,我们可以为操作设置优先级。 而GCD的优先级是正对整个队列而言的,不太适用这种场景。
  • 重用NSOperation对象 : 系统内置了一些NSOperation的子类,如NSBlockOperation,比GCD强大很多。

44. 通过Dispatch Group机制,根据系统资源状况执行任务

dispatch group能够把任务分组,将并发执行的多个任务合为一组,于是调用者就可以知道一组任务何时全部完成。

创建dispatch group

dispatch_group_t dispatch_group_create();

对于任务编组,有两种方式。 异步添加任务:

void dispatch_group_async(dispatch_group_t group,dispatch_queue_t queue,dispatch_block_t block);

或者使用类似信号量的方式:

void dispatch_group_enter(dispatch_group_t group);
void dispatch_group_leave(dispatch_group_t group);

对于分组任务完成的通知,可以通过阻塞线程的方式获取 :

long dispatch_group_wait(dispatch_group_t group , dispatch_time_t timeout);

也可以通过异步注册的方式 :

void dispatch_group_notify(dispatch_group_t group,dispatch_queue_t queue, dispatch_block_t block);

需要注意的是, dispatch groupdispatch queue是两个概念,没有关联,任务可以分发到不同的队列上,只不过任务有个标记记录了其分组。

根据系统资源状况执行任务: 因为GCD在执行并发队列时,并发的线程数量取决于多种因素,主要因素就是当前的系统资源状况,GCD会在适当的时机自动创建新线程或复用旧线程,做到最高效地利用系统资源。

45. 使用dispatch_once 来执行只运行一次的线程安全代码

单例模式很常见,而OC中自行编写的单例方法一般如下:

+ (instance)sharedInstance{
	static XXClass *instance;
	if(!instance) {
		@synchronized(self) {
			if(!instance){
				instance = [[self alloc] init];
			}
		}
	}
	return instance;
}

GCD提供了一个更加方便的函数 dispatch_once

该方法没有使用锁,而是使用 atomic access 来查询标记,性能要优于synchronized.

46. 不要使用dispatch_get_current_queue

这里我就不欣赏原作者编写的内容,没太多意义, 我总结为 :

设计代码时,不要用过多层次去嵌套block,且尽量不要使用GCD的同步方法。

dispatch_sync很容易发生死锁问题,使用dispatch_get_current_queue并不能从根本上解决问题,且这个函数已经被弃用了。

还有一点极其重要, dispatch_sync看似执行在queue上,实际上一般都会在当前线程上执行,为了性能, 苹果官方文档如此说 :

As an optimization, this function invokes the block on the current thread when possible.

一般来说,dispatch_sync要求在主线程中执行时,会正常在主线程中执行。其他情况下,都会直接在当前线程上执行。