block与GCD
“块”(block)与“大中枢派发”(GCD)是苹果公司为解决多线程编程而一起引入的解决方案。
block是一种可以在C、C++以及Objective-C代码中使用,类似于“闭包”(closure)的代码块,借助block机制,开发者可以将代码像对象一样在不同的上下文环境中进行传递。
GCD是一种与block有关的技术,它主要用于优化应用程序以支持多核处理器的调度。开发者可以将块排入队列中,由GCD负责处理所有的调度事宜。GCD会根据系统资源情况,适时地创建、复用、销毁后台线程(Background Thread),以便处理每个队列。和其他的多线程技术方案相比,使用起来更加简单和方便。
block有哪几种定义的方式
在Objective-C中,block定义包含了block的类型声明和实现,基本形式如下:
返回值类型(^block名称)(参数类型)=^(参数类型和参数名){};
其中,返回值类型和参数可以是空。如果有参数,那么在定义block的时候,必须要标明参数的类型和参数名。所以,block大致有3种细分的定义方式。
1)没有返回值,没有参数的定义方式。
void(^myBlock)() = ^{ //代码 }
2)有返回值,有参数的定义方式。
int(^myBlock)(int) = ^(int a){ return a; }
3)有返回值,没有参数的定义方式。
int(^myBlock)() = ^{ return 100; }
当然,block也有属于自己的类型,就像在Objective-C中,字符串对象属于NSString类型一样。block类型的格式就是:
返回值类型(^)(参数类型)
也就是说,上面第一种定义方式的block类型就是void(^)(),myBlock不是变量名,而是这种block类型的别名。在Objective-C中,可以使用typedef关键字定义block类型,也可以直接使用inline提示符来自动生成block格式。示例代码如下:
/*使用typedef关键字定义block类型*/ typedef void(^myBlock)(); myBlock block = ^{ }; /*使用inline提示符来自动生成block格式*/ <#returnType#>(^<#blockName#>)(<#parameterTypes#>) = ^(<#parameters#>){ <#statements#> };
在ARC环境下,是否需要使用copy关键字来修饰block
先要明确的是,block其实包含两个组成部分,一部分是block所执行的代码,这一部分在编译的时候已经确定;另一部分是block执行时所需要的外部变量值的数据结构。根据block在内存中的位置,系统将block分为3类。
1)NSGlobalBlock:该类型的block类似函数,内存地址位于内存全局区。只要block没有对作用域中局部变量进行引用,此block会被系统设置为该类型。示例代码如下:
- (void)test{ void(^gBlock1)(int, int) =^(int a, int b){ NSLog(@"a + b = %d", a + b); }; NSLog(@"%@", gBlock1); }
以上代码的输出结果是:
<NSGlobalBlock:0x1025e8110>
事实上,对于NSGlobalBlock类型的block,无需做更多的处理,不需要使用retain和copy进行修饰。即使使用了copy,系统也不会改变block的内存地址,操作是无效的。
2)NSStackBlock:该类型的block内存位于栈,其生命周期由函数决定,函数返回后block将无效。
在MRC环境下,若block内部引用了局部变量,此block就会被系统设置为该类型。对于NSStackBlock类型的block,使用retain和release操作都是无效的,必须调用Block_copy()方法,或者使用copy进行修饰,其作用就是将block的内存从栈转移到堆,此时block就会转变为NSMallocBlock类型,这也是一直使用copy修饰block的原因。
在ARC环境下,若block内部引用了局部变量,系统默认使用了copy对block进行修饰,使其变成NSMallocBlock类型。所以在ARC环境下,不需要手动使用copy关键字来修饰block。
3)NSMallocBlock:当对NSStackBlock类型的block进行copy操作后,block就会转为此类型。在MRC环境下,可以使用retain、release等方法手动管理此类型block的生命周期。在ARC环境下,系统会帮助管理此类型block的生命周期。
在block内如何修改block外部变量
在block内部修改block外部变量会造成编译错误,提示变量缺少__block修饰,不可赋值。要想在block内部修改block外部变量,则必须在外部定义变量时,前面加上__block修饰符。示例代码如下:
/*block外部变量*/ __block int var1 = 0; int var2 = 0; /*定义block*/ void(^block)(void) =^{ /*试图修改block外部变量*/ var1 = 100; /*编译错误,在block内部不可对var2赋值*/ //var2 = 1; }; /*执行block*/ block(); NSLog(@"修改后的var1:%d",var1);//修改后的var1:100
block内部为何不能直接修改外部变量呢?因为当外部变量没有使用__block修饰符修饰时,block在截获外部的自动变量时会在内部新创建一个新的变量val来保存所截获的外部变量的瞬时值,新变量val成为block的成员变量(Objective-C中block也是对象),之后在block代码中修改的值是成员变量val的值,而不是截获的外部变量的值,所以外部变量的值不会受影响。此时,修改外部变量是先取值并赋值给成员变量val,然后修改val的值。可用下面的代码模拟其原理,假设block对外部变量var进行了加1操作,block使用一个名为block的函数来表示。
int var = 1; void block(){ int val = var; val += 1; }
当外部变量使用了__block修饰符进行修饰的时候则是另外一种情形了,此时block并不是截获外部自动变量的瞬时值并保存到自己的新成员变量中,而是保存了对外部变量的指引引用,因此对指针变量的修改会直接影响外部变量的值。此时使用代码模拟其原理如下,依然是block对外部变量var进行加1操作。
__block int var = 1; void block(){ int *ptr = &var; *ptr += 1; }
因此,block内部不可以直接修改外部变量,如果要修改外部变量,那么该外部变量必须使用__block修饰符进行修饰,否则编译器会直接进行报错提示。
需要注意的是,此处讨论的是自动变量,而静态变量由于默认传给block的就是地址值,所以是可以直接修改的。另外,全局变量和静态全局变量由于作用域很广,也是可以在block中直接被修改的,编译器也不会报错。
在block中使用self关键字是否一定导致循环引用
在block中使用self关键字并不总会引起循环引用。事实上,只有当block和self相互持有时,才会导致循环引用。由于block会对block中的对象进行持有操作,就相当于持有了其中的对象,此时如果block中的对象又持有了该block,那么就会造成循环引用。典型的场景就是当block作为self的属性使用时,又在block内部调用了self的属性或者方法。示例代码如下:
typedef void(^block)(); @property (copy, nonatomic) block myBlock; @property (copy, nonatomic) NSString *blockString; - (void)testBlock{ self.myBlock = ^{ /*其实注释中的代码,同样会造成循环引用*/ NSString *localString = self.blockString; } }
在上面这个例子中,myBlock和self相互引用了对方。此时,self的销毁依赖于myBlock的销毁,而myBlock的销毁又依赖self的销毁,这样就造成了循环引用,即使在外界已经没有任何指针能够访问到它们了,它们也无法被释放,如图所示。
block循环引用
解决循环引用的关键是断开引用链。在实际开发中,主要使用弱引用(weak reference)的方法来避免循环引用的产生。在ARC环境下,使用__weak修饰符定义一个__weak self的引用,并且在里面使用这个弱引用。使用这种方式对示例代码修改如下:
typedef void(^block)(); @property (copy, nonatomic) block myBlock; @property (copy, nonatomic) NSString *blockString; - (void)testBlock{ __weak typeof(self) weakSelf = self; self.myBlock = ^{ /*其实注释中的代码,同样会造成循环引用*/ NSString *localString = weakSelf.blockString; }; }
当使用__weak修饰的弱类型self时,block便不会再持有self的引用了,也就不会再产生循环引用了。
下面是不会造成循环引用的几种情况:
1)大部分GCD方法。
示例代码如下:
/* 使用GCD异步执行主队列任务*/ dispatch_async(dispatch_get_main_queue(), ^{ [self doSomething]; });
在例子中,因为self并没有对GCD的block进行持有,只有block持有了self的引用,所以不会造成循环引用。
2)block作为临时变量。在这种情况下,同样self并没有持有block,所以也不会造成循环引用。
3)block执行过程中self对象被释放。事实上,block的具体执行时间不确定,当block被执行的时候block中被__weak修饰的self对象有可能已经被释放了(例如,控制器对象已经被POP了)。当在并发执行,涉及异步服务的时候,这种情况有可能会出现。
对于这种情况,应该在block中使用__strong修饰符修饰self对象,使得在block期间对对象持有,当block执行结束后,解除其持有。示例代码如下:
- (void)testBlock{ __weak typeof(self) weakSelf = self; self.myBlock = ^{ __strong typeof(self)strongSelf = weakSelf; NSString *localString = strongSelf.blockString; } }