我们在前一章介绍了block的用法,而正确使用block必须要求正确理解block的内存管理问题。
这一章,我们只陈述结果而不追寻原因,我们将在下一章深入其原因。
一、block放在哪里
我们针对不同情况来讨论block的存放位置:
1.栈和堆
以下情况中的block位于堆中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
void
foo() { __block
int
i = 1024 ; int
j = 1 ; void
(^blk)( void ); void
(^blkInHeap)( void ); blk
= ^{ printf( "%d,
%d\n" ,
i, j);}; //blk在栈里 blkInHeap
= Block_copy(blk); //blkInHeap在堆里 } -
( void )fooBar { _oi
= 1 ; OBJ1*
oj = self ; void
(^oblk)( void )
= ^{ printf( "%d\n" ,
oj.oi);}; void
(^oblkInHeap)( void )
= [oblk
copy ]; //oblkInHeap在堆中 } |
2.全局区
以下情况中的block位于全局区:
1
2
3
4
5
6
7
8
9
10
|
static
int (^maxIntBlock)( int ,
int )
= ^( int
a, int
b){ return
a>b?a:b;}; -
( void )fooBar { int (^maxIntBlockCopied)( int ,
int )
=[maxIntBlock
copy ]; } void
foo() { int (^maxIntBlockCopied)( int ,
int )
= Block_copy(maxIntBlock); } |
需要注意的是,这里复制过后的block依旧位于全局区,实际上,复制操作是直接返回了原block对象。
二、block引用的变量在哪里
1.全局区
全局区的变量存储位置与block无关:
1
2
3
4
5
6
7
8
|
static
int
gVar = 0 ; //__block
static int gMVar = 1; void
foo() { static
int
stackVar = 0 ; //
__block static int stackMVar = 0; } |
注意,static变量是不允许添加__block标记的
2.堆栈
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
void
foo() { __block
int
i = 1024 ; //此时i在栈上 int
j = 1 ; //此时j在栈上 void
(^blk)( void ); blk
= ^{printf( "%d,
%d\n" ,
i, j); }; //此时,blk已经初始化,它会拷贝没有__block标记的常规变量自己所持有的一块内存区,这块内存区现在位于栈上,而对于具有__block标记的变量,其地址会被拷贝置前述的内存区中 blk(); //1024,
1 void (^blkInHeap)( void )
= Block_copy(blk); //复制block后,block所持有的内存区会被拷贝至堆上,此时,我们可以说,这个block现在位于堆上 blkInHeap(); //1024,1 i++; j++; blk(); //1025,1 blkInHeap(); //1025,1 } |
让我们一步步剖析:
首先,我们在栈上创建了变量ij,并赋予初始值,然后创建一个block变量名为blk,但未赋值。
然后我们初始化这个blk,赋值为一个只有一句printf的block,值得注意的是,一个block一旦创建,其引用到的常规变量会进行如下操作:
没有__block标记的变量,其值会被复制一份到block私有内存区
有__block标记的变量,其地址会被记录在block私有内存区
然后调用blk,打印1024, 1很好理解
接下来复制blk到堆,名曰blkInHeap,调用之,打印1024, 1也很好理解
接下来我们为ij增值,使其变为1025和2,此时再调用blk或者blkInHeap,会发现结果为1025, 1,这是因为变量j早已在创建原始的block时,被赋值进block的私有内存区,后续对i的操作并非操作的私有内存区的复制品,当调用blk或者blkInHeap时,其打印使用的是私有内存区的复制品,故而打印结果依旧为1;而变量j的修改会实时生效,因为block记录的是它的地址,通过地址来访问其值,使得外部对j的修改在block中得以生效。对于变量i来讲,可算是物是人非吧?
因此,无论j++这一句放到blk()这句之前或者之后,只要它位于block初始化之后,这段代码执行的控制台打印结果都会是:1024, 1。而不是1024, 2(假设不调用i++)
三、其他特性
1.复制的行为
对block调用复制,有以下几种情况:
1.对全局区的block调用copy,会返回原指针,并且这期间不处理任何东西(至少目前的内部实现是这样);
2.对栈上的block调用copy,每次会返回新复制到堆上的block的指针,同时,所有__block变量都会被复制至堆一份(多次拷贝,只会生成一份)。
3.对已经位于heap上的block,再次调用copy,只会增加block的引用计数。
为什么我们不讨论retian的行为?原因是并没有Block_retain()这样的函数,而且objc里面的retain消息发送给block对象后,其内部实现是几乎什么都不做(会增加objective-c引用计数)。
2.objc类中的block复制
objc类实例方法中的block如果被复制至heap,那么当前实例会被增加引用计数,当这个block被释放时,此实例会被减少引用计数。
但如果这个block没有使用当前实例的任何成员,那么当前实例不会被增加引用计数。这也是很自然的道理,我既然没有用到这个instance的任何东西,那么我干嘛要retian它?
我们要注意的一点是,我看到网上有很多人说block引起了实例与block之间的循环引用(retain-cycle),并且给出解决方案:不直接使用self而先将self赋值给一个临时变量,然后再使用这个临时变量。
但是,大家注意,我们一定要为这个临时变量增加__block标记(多谢第三篇文章回帖网友的提醒)。
这一章我们以结果导向的方式来说明了各种情况下,block的内存问题,下一章,我将剖析运行时库的源码,从根源阐述block的行为。也就是过程导向的方式了。