初恋Blocks
一、基础
1.定义:
Blocks是带有自动变量(局部变量)的匿名函数。
2.格式:
^ 返回值类型 参数列表 表达式
^int (int count){return count + 1;}
其中的返回值类型
和参数列表
可以省略。
3.Block类型变量声明:
返回参数 (^变量名称)(接受参数);
该用法就和平时变量的用法相同了,也可以进行赋值等操作。
int (^blk)(int) = ^(int count) {return count + 1;};
int (^blk_t)(int);
blk_t = blk;
4.使用typedef重命名Block类型:
因为Block
类型变量和平时的使用类型相同,为了方便我们使用,我们通常都是用typedef
来重命名Block
。
typedef 返回参数 (^该Block的名称)(接收参数)
此时就可以用该Block的名称
代表该Block了。
typedef int (^blk_t)(int)
blk_t blk = ^(int count) {return count + 1;};
5.Block截获自动变量值和__block说明符:
Block
截获自动变量值就是说在定义Block
的时候,其之前的变量都会被该Block
所截获,该Block
后再改变变量的值然后再调用,其Block
中所捕获的值不会受到改变。
int main(int argc, const char * argv[]) {
int val = 10;
const char *fmt = "val = %d\n";
void (^blk)(void) = ^{
printf(fmt, val);
};
val = 2;
fmt = "These values were changed. val = %d\n";
blk();
return 0;
}
此时我们发现,在定义
Block之后的这val
,fmt
两个变量被改变了,但是再次调用
该Block还是定义
Block之前的值,这就是捕获。我的理解就是定义
Block之前的变量都会被该Block捕获,你在定义
Block后改变变量其值还是该Block之前的定义的值,就不会收到影响,当然若你在Block中改变之前变量的值,他就会报错,想要在其中改变变量的值,这里就的用到__block
说明符了。
注意这里的定义Block和调用Block,这是俩个不同的概念,定义就是首次出现该Block的地方,而调用就是之后出现Block的地方。
上面说了Block有截获功能,这里的__block
说明符可以说就是用来打破其自动截获功能的,并且还可以使变量可以在Block中进行改变。
int main(int argc, const char * argv[]) {
__block int val = 10;
__block const char *fmt = "val = %d\n";
void (^blk)(void) = ^{
printf(fmt, val);
val = 20;
};
val = 2;
fmt = "These values were changed. val = %d\n";
blk();
printf(fmt, val);
return 0;
}
之前说到的,定义Block后再改变变量的值,再次调用时不会改变值,但是使用__block
说明符之后,你就会发现其当时的自动截获功能被打破了,不再是截获定义之前的了,而是开始截获调用Block之前的值了,并且你还可以在Block中对变量进行改变了。 这就是__block
说明符的秒处。
但是要注意OC对象和C语言的数组!!!
先用OC中的NSMutableArray
来说,截获它其实就是截获该类对象用的结构体实例指针,就是说你只要不改变其指针的指向和其指针的值,他就不会出错!!!
int main(int argc, const char * argv[]) {
id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
id obj = [[NSObject alloc] init];
[array addObject:obj];
};
blk();
return 0;
}
这里因为你并没有改变其array
指针的指向和值,而只是在其地址后边新加了一个值,所以他就不会报错!!!
int main(int argc, const char * argv[]) {
id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
array = [[NSMutableArray alloc] init];
};
blk();
return 0;
}
但是你这样写就改变了array
的初始地址,所以他就会报错!!!
再说C语言中的数组,在截获时一定要小心使用其指针。
int main(int argc, const char * argv[]) {
const char text[] = "hello"; //这里是定义一个数组
void (^blk)(void) = ^{
printf("%c\n", text[2]);
};
blk();
return 0;
}
这里你看似没有什么错误,但是会出现下面的编译错误。
因为在现在的Blocks中,截获自动变量的方法并没有实现对C语言数组的截获,这时我们可以使用指针来解决该问题。
int main(int argc, const char * argv[]) {
const char *text = "hello"; //这里是使用指针引用
void (^blk)(void) = ^{
printf("%c\n", text[2]);
};
blk();
return 0;
}
这样就没问题了。
二、Blocks实现
1.Blocks本质:
前面简单说了说Blocks,但是我们还是不太清楚其实质是什么东西,为了我们可以理解,我们通过“-rewrite-objc
”选项就能将含有 Block 语法的源代码变换为C++的源代码。说是C++,其实也仅是使用了struct 结构,其本质是C语言源代码。
clang -rewrite-objc 源代码文件名
下面我们转换Block语法:
int main() {
void (^blk)(void) = ^{
printf("Block\n");
};
blk();
return 0;
}
使用clang
转换成源代码就是下面这样的:
//block类的结构体指针,每个block的这个都是相同的,存储一些block常用的标识等等
struct __block_impl {
void *isa; //指向所属类的指针,也就是Block类型,参考OC的self指针
int Flags; //标志变量,在实现block的内部操作时会用到
int Reserved; //保留变量
void *FuncPtr; //block执行时调用的函数指针,其指向block代码块中的地址
};
//block本体,block的构造函数,我们在定义blockd的时候其实就是定义了一个这样的结构体,其中存储了block的全部信息,捕获的变量、指向的代码块的地址、block本身的大小、block的类型等等
struct __main_block_impl_0 {
struct __block_impl impl; //定义一个block通用的数据,其中包括了block的类型和block中实现的代码块
struct __main_block_desc_0* Desc; //定义一个保存block结构体大小的变量,其中保存了定义的该block的大小
//block的构造方法,及初始化方法
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
//block的类型,分为全局Block、栈Block、堆Block
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp; //令定义的新的block的函数指针指向你Block代码块中写的代码
Desc = desc; //保存该Block的所占内存大小
}
};
//block中的代码块
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Block\n");
}
static struct __main_block_desc_0 {
size_t reserved; //保留字段
size_t Block_size; //block大小
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
}; //构造函数,对该结构体进行初始化处理,保存该block的大小
int main(int argc, const char * argv[]) {
//定义Block
//因为block相当于一个函数指针,所以下面这个是&,之后再通过数据内容和数据的大小信息创建一个block的结构体对象,并且这里调用__main_block_impl_0的构造函数,还将Block中的代码块和Block结构体所占的大小传给了该Block,使其初始化
void (*blk)(void) = (void (*)(void))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
//调用Block
//相当于函数指针的调用,将Block结构体传给block的代码块,让其执行代码块中的代码
((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
return 0;
}
2.截获自动变量值:
代码:
int main(int argc, const char * argv[]) {
int dmy = 256;
int val = 10;
const char *fmt = "val = %d\n";
void (^blk)(void) = ^{
printf(fmt, val);
};
return 0;
}
源代码:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
const char *fmt;
int val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself {
const char *fmt = __cself->fmt;
int val = __cself->val;
printf(fmt, val);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
int main(int argc, const char * argv[]) {
int dmy = 256;
int val = 10;
const char *fmt = "val = %d\n";
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));
return 0;
}
通过上述两个源代码的对比发现,这里多了val
,fmt
这两个变量,这就是block截获的变量,其实截获的本质就是通过结构体将截获变量的值存储起来,就是因为在定义的时候已经将值存储起来了,所以你之后无论再怎么改变改变了的值他都不会受影响。要注意的是,Block语法表达式中没有使用的自动变量不会被追加,即代码块中没有涉及到的变量是不会被截获的,就像这里的变量dmy
。
我们之前说的,数组类型的数据Block目前暂时无法进行截获,这就是因为Block在进行截获的时候在结构体中对该数据的创建是和你当时在外部时创建的方法是完全相同的,并且其传递数据是使用值传递来传递给结构体的,下面来用例子来说明:
这里使用一个函数当作Block结构体:
void func(char array[10]) {
char arrayCopy[10] = array;
printf("%d\n", b[0]);
}
int main(int argc, const char * argv[]) {
char array[10] = {2};
func(array);
return 0;
}
这里的func
函数就相当于Block
,我们在func(Block)
前使用char array[10]
的方式创建了这个数组,所以在func(Block)
中就会以相同的方式创建一个该数据的临时变量即char arrayCopy[10]
(这里名称不同是为了进行区分,其实名称是相同的),然后再将你传输给函数(结构体)的值进行赋值,这样做当然是不可以的!!!所以Block现在没有办法捕获数组的值,我们只能用指针来实现。
3.__block说明符:
block其实就是超出之前作用域并且在新的函数中访问之前的变量,要实现这种方法变量,我们可以使用static
进行修饰,但是他并不太好,因为变量作用域结束的同时,原来的自动变量被废弃,因此Block 中超过变量作用域而存在的变量同静态变量一样,将不能通过指针访问原来的自动变量。
当然我们还可以使用__block
修饰符,它类似于static、auto、和register说明符,用于指定将变量值设置到哪个存储域中。
下面我们就来看看:
int main(int argc, const char * argv[]) {
__block int val = 10;
void (^blk)(void) = ^{
val = 1;
};
return 0;
}
源代码:
//__block说明符修饰后的变量的结构体
struct __Block_byref_val_0 {
void *__isa; //指向所属类的指针
__Block_byref_val_0 *__forwarding; //指向自己的内存地址的指针
int __flags; //标志性参数,暂时没用到所以默认为0
int __size; //该结构体所占用的大小
int val; //该结构体存储的值,即原变量的赋的值
};
//block本体
struct __main_block_impl_0 {
struct __block_impl impl; //block的主体
struct __main block desc 0* Desc; //存储该block的大小
__Block_byref_val_0 *val; //__block修饰的变量的值
//构造函数
__main_block_impl_0(void *fp, struct __main_block_desc 0 *desc, __Block_byrefval_0 *_val, int flags=0) : val(_val->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
};
//封装的block逻辑,存储了block的代码块
static void __main_block_func_0(struct__main_block_impl_0 *_cself) {
__Block_byref_val_0 *val =__cself->val;
(val->__forwarding->val) = 1;
}
static void_main_block_copy_0(struct __main_block_impl_0* dst, struct __main_block_impl_0* src) {
//根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用
_Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}
static void __main_block_dispose_0(struct __main_block_imp1_0* src) {
//自动释放引用的auto变量(相当于release)
_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}
static struct __main_block_desc_0 {
unsigned long reserved; //保留字段
unsigned long Block_size; //block大小
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*); //copy的函数指针,下面使用构造函数进行了初始化
void (*dispose)(struct __main_block_impl_0*); //dispose的函数指针,下面使用构造函数进行了初始化
}
//构造函数,初始化保留字段、block大小及两个函数
__main_block_desc_0_DATA = {
0,
sizeof(structmain_block_impl_0),
__main_block_copy_O,
__main_block_dispose_0
};
int main() {
//之前的 __block int val = 10;变成了结构体实例
struct __Block_byref_val_0 val = {
0, //isa指针
&val, //指向自身地址的指针
0, //标志变量
sizeof(__Block_byref_val_0), //block大小
10 //该数据的值
};
blk = &__main_block_impl_0(
__main_block_func_0, &__main_block_desc_0_DATA, &val, 0x22000000);
return 0;
}
通过上述的代码和之前代码进行比对,发现使用__block
说明符修饰的变量变成了一个结构体,它是利用这个结构体来进行存储数据的。
为什么会有成员变量__forwarding呢?
因为__block变量用结构体成员变量__forwarding可以实现无论__block变量配置在栈上还是堆上时都能够正确的访问__block变量。
__block
变量的__Block_byref_val_0
结构体并不在Block
用__main_block_impl_0
结构体中,这是为了在多个Block
中使用__block
变量,即共用这个结构体。
4.Block存储域:
之前说过Block的本质,它是一个结构体,其中还有isa指针
它指向的是所属类的指针,之前没有具体说过Block的所属类,下面就来浅谈一下:
(1)Block的所属类:
- _NSConcreteStackBlock //栈
- _NSConcreteGlobalBlock //全局(.data区)
- _NSConcreteMallocBlock //堆
应用程序的内存分配如图:
(2)全局Block:
在记述全局变量的地方使用Block语法时,生成的Block为_NSConcreteGlobalBlock
类对象。
void (^blk)(void) = ^{printf("Global Block\n");};
int main(void) {
······
}
其isa指针
初始化如下:
impl.isa = &_NSConcreteGlobalBlock;
该类Block用结构体实例设置在程序的数据区域中。因为在使用全局变量的地方不能使用自动变量,所以不存在对自动变量进行截获。只在截获自动变量时,Block 用结构体实例截获的值才会根据执行时的状态变化。也就是说,全局Block不可以截获自动变量,否则其就不可以设置为全局Block。
也就是说,即使在函数内而不在记述广域变量的地方使用Block语法时,只要Block不截获自动变量,就可以将Block用结构体实例设置在程序的数据区域。
总结:
- 记述全局变量的地方有Block语法时
- Block语法的表达式中不使用应截获的自动变量时
以上的这些情况下,Block为_NSConcreteGlobalBlock
类对象。即Block配置在应用程序的数据区域中。
(3)栈Block:
其isa指针
初始化如下:
impl.isa = &_NSConcreteStackBlock;
除上述的设置两种全局Block之外的Block语法生成的Block为_NSConcreteStackBlock
类对象,且设置在栈上。
配置在全局变量上的Block
,从变量作用域外也可以通过指针安全地使用。但设置在栈上的Block
,如果其所属的变量作用域结束,该Block
就被废弃。由于__block变量
也配置在栈上,同样地,如果其所属的变量作用域结束,则该__block变量
也会被废弃。我们也就无法再访问到该变量了。
为了解决无法访问被废弃变量这个问题,就出现了堆block
,同时也出现了将Block
和__block变量
从栈上复制到堆上的方法。这样即使Block
语法记述的变量作用域结束,堆上的Block
还可以继续存在。下面我们就来说说:
(4)堆Block:
其isa指针
初始化如下:
impl.isa = &_NSConcreteMallocBlock;
堆上的Block其实就是将栈上的Block复制过来而成的,有时我们在_block变量
配置在堆上的状态下,也可以访问栈上的__block变量
。在此情形下,只要栈上的结构体实例成员变量__forwarding
指向堆上的结构体实例,那么不管是从栈上的__block 变量
还是从堆上的__block 变量
都能够正确访问。
那么Blocks 提供的复制方法究竟是什么呢?
实际上当ARC有效时,大多数情形下编译器会恰当地进行判断,自动生成将Block从栈上复制到堆上的代码。这是为了防止其被废弃而导致我们访问错误。
下面这个例子就是堆Block,用它来说明:
typedef int (^blk_t)(int);
blk_t func(int rate) {
return ^(int count) {return rate * count;};
}
源代码:
blk_t func(int rate) {
//因为ARC处于有效的状态,所以blk_t tmp实际上与附有__strong 修饰符的blk_t __strong tmp 相同
blk_t tmp = &__func_block_impl_0(__func_block_func_0,&__func_block_desc_0_DATA, rate);
//通过 objc4运行时库的runtime/objc-arrmm可知,objc_retainBlock函数实际上就是_Block_copy 函数
tmp = objc_retainBlock(tmp);
//等同于 tmp = _Block_copy(tmp);
//最后将tmp放入自动释放池中进行返回
return objc_autoreleaseReturnValue(tmp);
}
这个过程到底发生了什么?我们通过下列源代码中的注释来看看:
/*
*将通过Block语法生成的Block,
*即配置在栈上的Block用结构体实例
*赋值给相当于Block类型的变量tmp中。
*/
tmp = _Block_copy(tmp);
/*
*_Block_copy 函数
*将栈上的Block复制到堆上。
*复制后,将堆上的地址作为指针赋值给变量tmp。
*/
return objc_autoreleaseReturnValue(tmp);
/*
*将堆上的Block作为Objective-c对象
*注册到autoreleasepool中,然后返回该对象。
*/
说明,将 Block 作为函数返回值返回时,编译器会自动生成复制到堆上的代码。
前面讲到过“大多数情况下编译器会适当地进行判断”,不过在此之外的情况下需要手动生成代码,将 Block 从栈上复制到堆上。此时我们使用“copy实例方法”。这就是ARC 一章中大量出现的alloc
/new
/copy
/mutableCopy
方法中的一个方法,copy
方法。那么编译器不能进行判断究竟是什么样的状况呢?如下所示:
- 向方法或函数的参数中传递Block时
但是如果在方法或函数中适当地复制了传递过来的参数,那么就不必在调用该方法或函数前手动复制了。以下的方法或函数不用手动复制:
- Cocoa 框架的方法且方法名中含有usingBlock 等时
- Grand Central Dispatch 的 API
下面举例说明:
- (id)getBlockArray {
int val =10;
return [[NSArray alloc] initWithObjects:
^{NSLog(@"blk0:%d",val);},
^{NSLog(@"blk1:%d",val);}, nil];
}
这种情况因为你向initWithObjects
方法中传递了两个Block,这种情况ARC就无法识别然后进行自动管理,此时就得需要我们手动进行copy了。
若不进行手动copy它就会因为栈上的Block被废弃的缘故,我们再次访问该数组中的元素就会出错。
id obj = getBlockArray();
typedef void (^blk_t)(void);
blk_t blk = (blk_t)[obj objectAtIndex:0];
blk();
因为没有对其进行copy到堆上处理,所以这里的blk();
就会出错。
我们只需要这样更改代码就行了:
- (id)getBlockArray {
int val = 10;
return [[NSArray alloc] initWithObjects:
[^{NSLog(@"blk:%d", val);} copy],
[^{NSLog(@"blk1:%d",val);} copy], nil];
}
也就是让其进行一个copy操作就不会出错了。当然也可以对Block变量进行copy,就像这样:
typedef int (^blk_t)(int);
blk_t blk = ^(int count){return rate * count;};
blk = [blk copy];
(5)Copy Block:
所以,不管Block配置在什么地方,用copy方法复制都不会出现问题,并且在ARC有效时,你连续copy也不会出现任何问题:
blk = [[[[blk copy] copy] copy] copy];
(6)__block变量存储域:
上节只对 Block 进行了说明,那么对__block变量
又是如何处理的呢?使用__block变量
的 Block
从栈复制到堆上时,__block 变量
也会受到影响。总结如表所示。
就是说,不管你的__block变量
原本在哪块存储区,只要使用该__block变量
的Block
被复制到堆区时,其__block变量
也就会被复制到堆区,并被该Block持有。
那么多个Block中使用同一个__block变量会怎么样呢?
在多个
Block
中使用__block 变量
时,因为最先会将所有的Block
配置在栈上,所以__block变量
也会配置在栈上。在任何一个Block
从栈复制到堆时,__block 变量
也会一并从栈复制到堆并被该Block
所持有。当剩下的Block
从栈复制到堆时,被复制的Block
持有__block 变量
,并增加__block变量
的引用计数。
这里的持有就和OC中的引用计数式内存管理完全相同,已经复制到堆上的Block
就对堆上的从栈上复制过去的该__block变量
成了持有状态,而没有复制到堆上的即还在栈上的Block
它还是对之前在栈上的__block变量
是一个使用的状态,即现在有了两个该Block
和该__block变量
,一个在栈上,一个在堆上。
但是因为你复制之后的Block
会将__block变量
也一并复制到堆中,所以此时出现了一个问题此时有了两个该__block变量
,他们之间又是怎样的关系呢?
用下面的代码来说明:
int main(int argc, const char * argv[]) {
__block int val = 10;
void (^blk)(void) = [^{++val;} copy];
++val;
blk();
NSLog(@"%d", val);
return 0;
}
上述的代码打印出来的值为12
,这就表示++val;
和blk();
所++
的是同一个值,但是一个是在栈上一个是在堆上这又是怎么做到访问的为同一块内存区域的呢,其实上述的++val;
都会转换成这样的代码
++(val.__forwarding->val);
也就是说,该__block变量
转换成结构体后,结构体中的__forwarding
指针发挥了作用,该指针指向的是同一块内存区域,所以才可以做到val
的信息同步。如图解:
通过该功能,无论是在 Block 语法中、Block语法外使用__block变量
,还是__block变量
配置在栈上或堆上,都可以顺利地访问同一个__block变量
。
(7)截获对象:
先看代码:
blk_t blk;
{
id array = [[NSMutableArray alloc] init]; //使用strong修饰
blk = [^(id obj) {
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
} copy];
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
输出如下:
array count = 1
array count = 2
array count = 3
我们就会产生疑问,这个array对象不是超出作用域了吗,为什么它还可以正常的使用,程序还不会出错。
我们再将上述的代码转换为源代码来看看:
/*Block用结构体/函数部分*/
struct __main_block_impl_0 {
struct __block_impl_impl;
struct __main_block_desc_0* Desc;
id __strong array;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id __strong _array, int flags=0) : array(_array) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) {
id strong array = __cself->array;
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
}
static void __main_block_copy_0(struct __main_block_impl_o *dst, struct __main_block_impl_0 *src) {
_Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT);
}
static void_main_block_dispose_0(struct __main_block_impl_0 *src) {
_Block_object_dispose(src->array, BLOCK_FIELD_IS_OBJECT);
}
static struct __main block desc_0 {
unsigned long reserved;
unsigned long Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0),
__main_block_copy_0,
__main_block_dispose_0
};
/*Block语法,使用Block部分 */
blk_t blk;
{
id __strong array = [[NSMutableArray alloc] init];
blk = &_main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, array, 0x22000000);
blk = [blk copy];
}
(*b1k->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*b1k->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*b1k->impl.FuncPtr)(blk, [[NSObject alloc] init]);
我们发现,之前截获的array
它是使用strong
修饰的,在Objective-C中,C语言结构体不能含有附有strong
修饰符的变量。因为编译器不知道应何时进行C语言结构体的初始化和废弃操作,不能很好地管理内存。
但是Obiective-C的运行时库能够准确把握Block
从栈复制到堆以及堆上的Block
被废弃的时机,因此Block
用结构体中即使含有附有__strong
修饰符或__weak
修饰符的变量,也可以恰当地进行初始化和废弃。为此需要使用在__main_block_desc_0
结构体中增加的成员变量copy
和 dispose
,以及作为指针赋值给该成员变量的__main_block_copy_0
函数和_main_block_dispose_0
函数。
copy和dispose函数:
_Block_object_assign
函数调用相当于retain
实例方法的函数,将对象赋值在对象类型的结构体成员变量中。
_Block_object_dispose
函数调用相当于release
实例方法的函数,释放赋值在对象类型的结构体成员变量中的对象。
但是我们发现在转换后的源代码中,这些函数(copy和dispose)包括使用指针全都没有被调用。那么这些函数是从哪调用呢?
在Block从栈复制到堆时以及堆上的Block被废弃时会调用这些函数。
那么什么时候栈上的 Block 会复制到堆呢?
- 调用Block的
copy
实例方法时 - Block作为函数返回值返回时
- 将Block 赋值给附有
__strong
修饰符id类型的类或Block类型成员变量时 - 在方法名中含有
usingBlock
的Cocoa框架方法
或Grand Central Dispatch
的 API 中传递 Block 时
其实,我们了解过上述的复制时机时,就会发现在上面这些情况下栈上的Block 被复制到堆上,可归结为其实可归结为_Block_copy
函数被调用时 Block从栈复制到堆。
相对的,在释放复制到堆上的Block后,谁都不持有Block而使其被废弃时调用dispose
函数。这相当于对象的dealloc
实例方法。
有了这种构造,通过使用附有__strong修饰符
的自动变量,Block中截获的对象就能够超出其变量作用域而存在。
虽然这种使用copy
函数和dispose
函数的方法在前面没做任何说明,但实际上在使用 block变量时已经用到了。
static void __main_block_copy0(
struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}
我们发现函数中所传的最后一个参数我们并不知道是什么意思,其实它是用于区分copy
函数和 dispose
函数的对象类型是对象
还是__block变量
。
由此可知,Block中使用的赋值给附有__strong修饰符
的自动变量的对象和复制到堆上的__block变量
由于被堆上的Block所持有,因而可超出其变量作用域而存在。
那么在刚才的源代码中,如果不调用Block的copy
实例方法又会如何呢?
blk_t blk;
{
id array =[[NSMutableArray alloc] init];
blk = ^(id obj) {
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
};
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
执行该源代码后,程序会强制结束。
因为只有调用_Block_copy函数
才能持有截获的附有__strong修饰符
的对象类型的自动变量值,所以像上面源代码这样不调用_Block_copy函数
的情况下,即使截获了对象,它也会随着变量作用域的结束而被废弃。
因此,Block中使用对象类型自动变量时,除以下情形外,推荐调Block的copy
实例方法。
- Block作为函数返回值返回时
- 将Block 赋值给类的附有__strong修饰符的id类型或Block类型成员变量时。
- 向方法名中含有usingBlock的Cocoa 框架方法或Grand Central Dispatch 的API中传递 Block 时
虽然书上说了不使用copy修饰符即使截获了对象,它也会随着变量作用域的结束而被废弃,但是你实际进行操作其还是会保留,并没有废弃,不太理解。
(8)__block变量和对象:
__block变量
说明符可指定任何类型的自动变量。
如下:
__block id obj = [[NSObject alloc] init];
等同于:
__block id __strong obj = [[NSObject alloc] init];
ARC有效时,id类型以及对象类型变量必定附加所有权修饰符,缺省为附有__strong修饰符
的变量。该代码可通过clang
转换如下:
/*_block变量用结构体部分*/
struct __Block byref_obj_0 {
void *__isa;
_Block_byref_obj_0 *_forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*,void*);
void (*__Block_byref_id_object_dispose)(void*);
__strong id obj;
};
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *)((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *sre) {
_Block_object_dispose(*(void * *)((char*)src + 40), 131);
}
/*_block变量明部分*/
__Block_byref_obj_0 obj = {
0,
&obj,
0x2000000,
sizeof(__Block_byref_obj_0),
__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,
[[NSObject alloc] init]
};
我们可以发现,这里__block变量
和之前使用copy后的Block很像,都具有copy
和dispose
函数,这里的__block变量
其实和之前的copy后的Block一样,当__block变量
从栈复制到堆时,使用_Block_object_assign
函数,持有赋值给__block变量
的对象。当堆上的__block变量
被废弃时,使用_Block_object_dispose
函数,释放赋值给Block
变量的对象。
由此可知,即使对象赋值复制到堆上的附有__strong修饰符
的对象类型__block变量
中,只要__block变量
在堆上继续存在,那么该对象就会继续处于被持有的状态。这与Block中使用赋值给附有__strong修饰符
的对象类型自动变量的对象相同。
如果使用__weak修饰符
会如何呢?
blk_t blk;
{
id array = [[NSMutableArray alloc] init];
id __weak array2 = array;
blk = [^(id obj) {
[array2 addObject:obj];
NSLog(@"array2 count = %ld", [array2 count]);
} copy];
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
我们发现这与之前的输出完全不同,这是由于附有__strong修饰符
的变量array
在该变量作用域结束的同时被释放、废弃,nil
被赋值在附有__weak修饰符
的变量array2
中。
若同时指定__block说明符
和__weak =修饰符
会怎样呢?
blk_t blk;
{
id array = [[NSMutableArray alloc] init];
__block id __weak array2 = array;
blk = [^(id obj) {
[array2 addObject:obj];
NSLog(@"array2 count = %ld", [array2 count]);
} copy];
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
执行结果与之前相同,这是因为即使附加了__block说明符
,附有__strong修饰符
的变量array
也会在该变量作用域结束的同时被释放废弃,nil
被赋值给附有__weak修饰符
的变量array2
中。
另外,由于附有__unsafe_unretained修饰符
的变量只不过与指针相同,所以不管是在Block
中使用还是附加到__block变量
中,也不会像__strong修饰符
或__weak修饰符
那样进行处理。因此在使用附有__unsafe_unretained修饰符
的变量时,注意不要通过悬垂指针访问已被废弃的对象。
因为并没有设定__autoreleasing修饰符
与Block
同时使用的方法,所以没必要使用autoreleasing修饰符
。另外,它与__block说明符
同时使用时会产生编译错误。
__block id __autoreleasing obj = [[NSObject alloc] init];
(9)Block循环引用:
如果在Block中使用附有__strong修饰符
的对象类型自动变量,那么当Block
从栈复制到堆时,该对象为Block
所持有。这样容易引起循环引用。我们来看看下面的源代码:
typedef void (^blk_t)(void);
@interface MyObject : NSObject
{
blk_t blk_;
}
@end
@implementation MyObject
- (id)init {
self = [super init];
blk_ = ^{
NSLog(@"self = %@", self);
};
return self;
}
- (void)dealloc {
NSLog(@"dealloc");
}
@end
int main(){
id o = [[MyObject alloc] init];
NSLog(@"%@", o);
return 0;
}
该源代码中MyObject类
的dealloc
实例方法一定没有被调用,因为其中出现了Block循环引用。
MyObject类
持有Block,而Block
中又调用了self
,即持有了MyObject类
,这就形成了一个循环引用。
编译器在编译该源代码时能够查出循环引用,因此编译器能正确地进行警告。
为避免此循环引用,可声明附有__weak修饰符
的变量,并将self
赋值使用。
- (id)init {
self = [super init];
id __weak tmp = self;
blk_ = ^{
NSLog(@"self = %@",tmp);
};
return self;
}
在该源代码中,由于Block
存在时,持有该Block
的MyObject
类对象即赋值在变量tmp
中的self
必定存在,因此不需要判断变量tmp
的值是否为nil
。
在面向iOS4
,Snow Leopard
的应用程序中,必须使用__unsafe_unretained
修饰符代替__weak
修饰符。在此源代码中也可使用__unsafe_unretained
修饰符,且不必担心悬垂指针。
- (id)init {
self = [super init];
id __unsafe_unretained tmp = self;
blk_ = ^{
NSLog(@"self = %@",tmp);
};
return self;
}
另外,并不是说你使用了self
才会发生循环引用,你在Block
中使用类对象的时候,这个类对象其实就相当于引用了self
。
@interface MyObject : NSObject
{
blk_t blk_;
id obj_;
}
@end
@implementation MyObject
- (id)init {
self = [super init];
blk_ = ^{
NSLog(@"obj_ = %@", obj_);
};
return self;
}
这里的obj_
其实就相当于self->obj_
,这就是间接引用了self
,造成了循环引用。当然这里也可以使用__weak修饰符
或__unsafe_unretained修饰符
来解决问题。
- (id)init {
self = [super init];
id __weak obj = obj_;
blk_ = ^{
NSLog(@"obj_ = %@", obj_);
};
return self;
}
另外,还可以使用__block变量
来避免循环引用。
typedef void (^blk_t)(void);
@interface MyObject : NSObject {
blk_t blk_;
}
@end
@implementation MyObject
- (id)init {
self = [super init];
__block id tmp = self;
blk_ = ^{
NSLog(@"self = %@", tmp);
tmp = nil;
};
return self;
}
- (void)execBlock {
blk_();
}
-(void)dealloc {
NSLog(@"dealloc");
}
@end
int main(void) {
id o = [[MyObject alloc] init];
[o execBlock];
return 0;
}
但是一定要调用execBlock
实例方法,如果不调用execBlock
实例方法,即不执行赋值给成员变量blk_
的Block
,便会循环引用并引起内存泄漏。在生成并持有MyObject
类对象的状态下会引起以下循环引用。
即tmp
持有self
,self
持有Block
,Block
持有tmp
,造成了循环引用。
但是你执行execBlock
实例方法后,Block
被实行,nil
被赋值在__block变量tmp
中。
这样就打破了循环引用。
使用__block变量
的优点如下:
- 通过
__block变量
可控制对象的持有期间 - 在不能使用
__weak修饰符
的环境中使用__unsafe_unretained 修饰符
即可(不必担心悬垂指针) - 在执行
Block
时可动态地决定是否将nil
或其他对象赋值在__block变量
中。
使用__block变量的缺点如下:
- 为避免循环引用必须执行
Block
存在执行了Block
语法,却不执行Block
的路径时,无法避免循环引用。若由于Block
引发了循环引用时,根据Block
的用途选择使用__block变量
、__weak修饰符
或__unsafe_unretained修饰符
来避免循环引用。
(10)copy/release
ARC
无效时,一般需要手动将Block
从栈复制到堆。另外,由于ARC
无效,所以肯定要释放复制的Block
。这时我们用copy
实例方法用来复制,用release
实例方法来释放。
void (^blk_on_heap)(void) = [blk_on_stack copy];
[blk_on_heap release];
只要Block
有一次复制并配置在堆上,就可通过retain
实例方法持有。
[blk_on_heap retain];
但是对于配置在栈上的Block
调用retain
实例方法则不起任何作用。
[blk_on_stack retain];
该源代码中,虽然对赋值给blk_on_stack
的栈上的Block
调用了retain
实例方法,但实际上对此源代码不起任何作用。因此推荐使用copy
实例方法来持有Block
。
另外,ARC
无效时,__block说明符
被用来避免Block
中的循环引用。 这是由于当Block
从栈复制到堆时,若Block
使用的变量为附有__block说明符
的id
类型或对象类型的自动变量,不会被retain
;若Block
使用的变量为没有__block说明符
的id
类型或对象类型的自动变量,则被retain
。例如下面的源代码中,不管ARC
有效无效都会引起循环引用,Block
持有self
,且self
持有Block
。
typedef void (^blk_t)(void);
@interface MyObject : NSObject {
blk_t blk_;
}
@end
@implementation MyObject
- (id)init {
self = [super init];
blk_ = ^{
NSLog(@"self = %@", self);
};
return self;
}
- (void)execBlock {
blk_();
}
- (void)dealloc {
NSLog(@"dealloc");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
id o = [[MyObject alloc] init];
[o execBlock];
}
return 0;
}
这时我们使用__block变量
来避免该问题。
- (id)init {
self = [super init];
__block id tmp = self;
blk_ = ^{
NSLog(@"self=%@", tmp);
};
return self;
}