今天看到一些关于block的问题。发现自己尽管在使用的时候没有注意到,但是还是有很多问题是自己平时没有注意到但是其实很重要的。我把自己看到的那些问题记录下来,让自己多温故,也为偶尔看到这篇blog的也恰好对一些问题有点疑惑的人提一点醒吧~~
1.block截获自动变量
关于这一点非常好理解。我们知道block是带有自动变量(局域变量)的匿名函数。也就是说在使用block的时候我们可以给这个block传递参数。但是当使用这个参数的时候在block中参数究竟是多少呢?
我们来分情况说明一下
1.当参数没有__block修饰时,在block截获的自动变量的值,就是保存该变量的瞬间的值。让我们看一段代码。
int val = 0;
NSLog(@"the address of val is %d", &val);
void (^blk)(void) = ^{
NSLog(@"the val is %d and the address is %d", val, &val);
};
blk();
val = 1;
blk();
打印出的结果为:
the address of val is 1548822496
the val is 0 and the address is -2006868880
the val is 0 and the address is -2006868880
由此可见,在block截获了自动变量后,无论在外界这个自动变量如何变化,在block中都是不变的。并且通过查看地址我们可以发现在block中的val和外界声明的val根本就是两片内存。
2.当参数有__block修饰时,block可以修改该参数的值。让我们再看一段代码。
__block int val = 0;
NSLog(@"the val is %d and address is %d", val, &val);//有__block修饰,则使用同一片内存
void (^blk)(void) = ^{
NSLog(@"the val in block is %d and address is %d", val, &val);
val = 1;};
blk();
NSLog(@"the first val is %d", val);
val = 4;
blk();
打印出的结果如下:
the val is 0 and address is 1373611992
the val in block is 0 and address is 1666570600
the first val is 1
the val in block is 4 and address is 1666570600
由此可见,如果使用__block修饰声明的变量,则在block中的变量和外部声明的变量为同一片内存。一改则全改。
2.Block的实现
通过观察block结构体的isa指针我们可以发现其实block是Objective-C的一个对象。它的类为NSConcreteStackBlock等。
我们这一节主要讲述关于截取自动变量和获得由__block修饰的变量的差别。
1.截取到得自动变量会被以成员变量的形式放入block结构体中,因为是复制的一片新内存,加上没有修改的方法,所以不会被改变值了。
2.关于__block修饰的变量比较特殊,它会被生成一个__Block_byref_...类型的结构体被单独地保存在内存之中。从而不同的block可以访问到同一个值。而block中也保存*forwarding指针指向自己,然后再找到这个值,对这个值进行改变等。因为__block_byref结构体中存着变量的指针,所以我们不必担心两个值所处的内存不同。
block根据存在的内存的区域不同可分为三种:在栈中存放的是NSConcreteStackBlock,在堆中存放的为NSConcreteMallocBlock,在数据区中存放的为NSConcreteGlobalBlock.Block若要在其所在的作用域外发挥作用,则需要从栈中复制到堆中才可以。一般地,由函数返回的block都会被存放在堆中。并且在系统的大部分API如果存在usingBlock或者CGD方法中的block都会放入堆中。而block作为参数存在时,我们需要注意下是否需要手动地将其复制。复制的方法很简单,调用copy方法即可。在ARC下,多次调用copy并不会导致内存泄露。
而在block被复制时,其所使用的__block变量也会被影响。不管block之前是存放在栈中或者已经存放于堆中,当被拷贝时,其所持有的__block变量都会被放入堆中并被该block所持有。当多个block使用同一个__block变量时,当其中的一个block被复制到堆中时,__block变量也会被复制到堆中一份,并且被已经被复制到堆中的block所持有。
让我们看看下面这段代码,当一个block被从栈复制到堆上时发生了什么
__block int val = 0;
NSLog(@"the val address is %d", &val);
void (^blk)(void) = [^{++val;} copy];
NSLog(@"the val address is %d", &val);
++val;
blk();
NSLog(@"the val address is %d", &val);
NSLog(@"the val is %d", val);
控制台中打出的结果为:
the val address is 1444706264
the val address is 961698216
the val address is 961698216
the val is 2
花擦!看到一个奇怪的现象了嘛?第一个log和第二个log里,同一个变量竟然地址都是不一样的?!
其实,在访问一个__block变量时我们是通过forwarding指针访问的。而在这个block被复制到了堆之后,栈中的block的forwarding指针也会指向被复制到堆中的那份__block变量的内存。所以我们就看到了这个现象,这也是苹果保证在block使用过程中始终访问到同一个__block变量的方法。
3.block截获对象
如果block截获了一个对象,如果这个对象被strong所修饰,则在block被复制到堆上时,该对象会被持有。所以可以在该对象的作用域外被操作。
如果block截获的对象被weak所修饰,则不会被持有。
如果block是一个对象的属性,则如果该block直接引用了该对象或对象的其他属性,则会强持有该对象,那么便会引起循环引用。为了避免这种事情发生,可以用weak修饰对象本身或相应的属性,就可以避免循环引用。在ARC下,如果通过__block修饰self再传递到block中,再在block中将weakSelf= nil。也可以达到避免循环引用的效果,当然你需要执行这个block。
在堆上的block可以被以retain的方式持有,在栈上的block调用retain则无效。
在非ARC下,直接使用__block修饰self,也可以避免循环引用,而无需执行这段block。