ARC与Block

http://www.cnblogs.com/flyFreeZn/p/4264220.html

ARC与Block


在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捕获引用都要这么处理,一定要注意!

猜你喜欢

转载自blog.csdn.net/chun799/article/details/45295937
arc
今日推荐