在MRC时代,Block会隐式地对进入其作用域内的对象(或者说被Block捕获的指针指向的对象)加retain,来确保Block使用到该对象时,能够正确的访问。
这件事情在下面代码展示的情况中要更加额外小心。
1
2
3
4
5
6
7
8
9
10
|
MyViewController *myController = [[MyViewController alloc] init…];
// 隐式地调用[myController retain];造成循环引用
myController.completionHandler = ^(NSInteger result) {
[myController dismissViewControllerAnimated:YES completion:nil];
};
[self presentViewController:myController animated:YES completion:^{
[myController release];
// 注意,这里调用[myController release];是在MRC中的一个常规写法,并不能解决上面循环引用的问题
}];
|
在这段代码中,myController的completionHandler调用了myController的方法[dismissViewController...],这时completionHandler会对myController做retain操作。而我们知道,myController对completionHandler也至少有一个retain(一般准确讲是copy),这时就出现了在内存管理中最糟糕的情况:循环引用!简单点说就是:myController retain了completionHandler,而completionHandler也retain了myController。循环引用导致了myController和completionHandler最终都不能被释放。我们在delegate关系中,对delegate指针用weak就是为了避免这种问题。
不过好在,编译器会及时地给我们一个警告,提醒我们可能会发生这类型的问题:
对这种情况,我们一般用如下方法解决:给要进入Block的指针加一个__block修饰符。
这个__block在MRC时代有两个作用:
说明变量可改
说明指针指向的对象不做这个隐式的retain操作
一个变量如果不加__block,是不能在Block里面修改的,不过这里有一个例外:static的变量和全局变量不需要加__block就可以在Block中修改。
使用这种方法,我们对代码做出修改,解决了循环引用的问题:
1
2
3
4
5
6
|
MyViewController * __block myController = [[MyViewController alloc] init…];
// ...
myController.completionHandler = ^(NSInteger result) {
[myController dismissViewControllerAnimated:YES completion:nil];
};
//之后正常的release或者retain
|
在ARC引入后,没有了retain和release等操作,情况也发生了改变:在任何情况下,__block修饰符的作用只有上面的第一条:说明变量可改。即使加上了__block修饰符,一个被block捕获的强引用也依然是一个强引用。这样在ARC下,如果我们还按照MRC下的写法,completionHandler对myController有一个强引用,而myController对completionHandler有一个强引用,这依然是循环引用,没有解决问题:(
于是我们还需要对原代码做修改。简单的情况我们可以这样写:
1
2
3
4
5
6
|
__block MyViewController * myController = [[MyViewController alloc] init…];
// ...
myController.completionHandler = ^(NSInteger result) {
[myController dismissViewControllerAnimated:YES completion:nil];
myController = nil;
// 注意这里,保证了block结束myController强引用的解除
};
|
在completionHandler之后将myController指针置nil,保证了completionHandler对myController强引用的解除,不过也同时解除了myController对myController对象的强引用。这种方法过于简单粗暴了,在大多数情况下,我们有更好的方法。
这个更好的方法就是使用weak。(或者为了考虑iOS4的兼容性用unsafe_unretained,具体用法和weak相同,考虑到现在iOS4设备可能已经绝迹了,这里就不讲这个方法了)(关于这个方法的本质我们后面会谈到)
为了保证completionHandler这个Block对myController没有强引用,我们可以定义一个临时的弱引用weakMyViewController来指向原myController的对象,并把这个弱引用传入到Block内,这样就保证了Block对myController持有的是一个弱引用,而不是一个强引用。如此,我们继续修改代码:
1
2
3
4
5
6
|
MyViewController *myController = [[MyViewController alloc] init…];
// ...
MyViewController * __weak weakMyViewController = myController;
myController.completionHandler = ^(NSInteger result) {
[weakMyViewController dismissViewControllerAnimated:YES completion:nil];
};
|
这样循环引用的问题就解决了,但是却不幸地引入了一个新的问题:由于传入completionHandler的是一个弱引用,那么当myController指向的对象在completionHandler被调用前释放,那么completionHandler就不能正常的运作了。在一般的单线程环境中,这种问题出现的可能性不大,但是到了多线程环境,就很不好说了,所以我们需要继续完善这个方法。
为了保证在Block内能够访问到正确的myController,我们在block内新定义一个强引用strongMyController来指向weakMyController指向的对象,这样多了一个强引用,就能保证这个myController对象不会在completionHandler被调用前释放掉了。于是,我们对代码再次做出修改:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
MyViewController *myController = [[MyViewController alloc] init…];
// ...
MyViewController * __weak weakMyController = myController;
myController.completionHandler = ^(NSInteger result) {
MyViewController *strongMyController = weakMyController;
if
(strongMyController) {
// ...
[strongMyController dismissViewControllerAnimated:YES completion:nil];
// ...
}
else
{
// Probably nothing...
}
};
|
到此,一个完善的解决方案就完成了:)
官方文档对这个问题的说明到这里就结束了,但是可能很多朋友会有疑问,不是说不希望Block对原myController对象增加强引用么,这里为啥堂而皇之地在Block内新定义了一个强引用,这个强引用不会造成循环引用么?理解这个问题的关键在于理解被Block捕获的引用和在Block内定义的引用的区别。为了搞得明白这个问题,这里需要了解一些Block的实现原理,但由于篇幅的缘故,本文在这里就不展开了,详细的内容可以参考其他的文章,这里特别推荐唐巧的文章和另外2位作者的博文:这个和这个,讲的都比较清楚。
这里假设大家已经对Block的实现原理有所了解了。我们就直入主题了!注意前方高能(=。=)
为了更清楚地说明问题,这里用一个简单的程序举例。比如我们有如下程序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#include < stdio.h>
int main()
{
int b = 10;
int *a = &b;
void (^blockFunc)() = ^(){
int *c = a;
};
blockFunc();
return
1;
}
|
程序中,同为int型的指针,a是被Block捕获的变量,而c是在Block内定义的变量。我们用clang -rewrite-objc处理后,可以看到如下代码:
原main函数:
1
2
3
4
5
6
7
8
9
10
11
12
|
int main()
{
int b = 10;
int *a = &b;
void (*blockFunc)() = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a);
((void (*)(__block_impl *))((__block_impl *)blockFunc)->FuncPtr)((__block_impl *)blockFunc);
return
1;
}
|
Block的结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *a;
// 被捕获的引用 a 出现在了block的结构体里面
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
|
实际执行的函数:
1
2
3
4
5
6
7
|
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *a = __cself->a;
// bound by copy
int *c = a;
// 在block中声明的引用 c 在函数中声明,存在于函数栈上
}
|
我们可以清楚得看到,a和c存在的位置完全不同,如果Block存在于堆上(在ARC下Block默认在堆上),那么a作为Block结构体的一个成员,也自然会存在于堆上,而c无论如何,永远位于Block内实际执行代码的函数栈内。这也导致了两个变量生命周期的完全不同:c在Block的函数运行完毕,即会被释放,而a呢,只有在Block被从堆上释放的时候才会释放。
回到我们的MyViewController的例子中,同上理,如果我们直接让Block捕获我们的myController引用,那么这个引用会被复制后(引用类型也会被复制)作为Block的成员变量存在于其所在的堆空间中,也就是为Block增加了一个指向myController对象的强引用,这就是造成循环引用的本质原因。对于MyViewController的例子,Block的结构体可以理解是这个样子:(准确的结构体肯定和以下这个有区别,但也肯定是如下这种形式:)
1
2
3
4
5
6
7
8
9
10
11
12
13
|
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
MyViewController * __strong myController;
// 被捕获的强引用myController
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
|
而反观我们给Block传入一个弱引用weakMyController,这时我们Block的结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
MyViewController * __weak weakMyController;
// 被捕获的弱引用weakMyController
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
|
再看在Block内声明的强引用strongMyController,它虽然是强引用,但存在于函数栈中,在函数执行期间,它一直存在,所以myController对象也一直存在,但是当函数执行完毕,strongMyController即被销毁,于是它对myController对象的强引用也被解除,这时Block对myController对象就不存在强引用关系了!加入了strongMyController的函数大体会是这个样子:
1
2
3
4
5
6
7
|
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
MyViewController * __strong strongMyController = __cself->weakMyController;
// ....
}
|
综上所述,在ARC下(在MRC下会略有不同),Block捕获的引用和Block内声明的引用无论是存在空间与生命周期都是截然不同的,也正是这种不同,造成了我们对他们使用方式的区别。
以上就解释了之前提到的所有问题,希望大家能看明白:)
好的,最后再提一点,在ARC中,对Block捕获对象的内存管理已经简化了很多,由于没有了retain和release等操作,实际只需要考虑循环引用的问题就行了。比如下面这种,是没有内存泄露的问题的:
1
2
3
4
5
6
7
8
9
|
TestObject *aObject = [[TestObject alloc] init];
aObject.name = @
"hehe"
;
self.aBlock = ^(){
NSLog(@
"aObject's name = %@"
,aObject.name);
};
|
我们上面提到的解决方案,只是针对Block产生循环引用的问题,而不是说所有的Block捕获引用都要这么处理,一定要注意!