iOS Block 内存管理的探讨

时间:2021-06-19 05:03:13

在很多情况下Block是造成程序循环引用内存泄漏的元凶。下面我们就讲解一下block对内存管理的影响。在讲解之前。希望大家对block有一定的了解。如果大家还不是太清楚block的实现原理。希望大家可以看看这篇文章。里面详细的介绍了block的实现过程。http://blog.devtang.com/2013/07/28/a-look-inside-blocks/。

MRC和Block

在MRC时代,block会隐式的对进入其作用域的对象(或者说被Block捕获的指针的指向的对象)加retain 来确保 block使用到该对象时,能够正确访问。

这件事情在下面展示的代码中要格外小心

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的方法[dismissController...];这个时候 completionhandler会对myController做retain操作。

而我们知道myController对completionHandler也至少有一个retain(一般准确的讲是copy)这时就出现了内存管理中最糟糕的情况:循环引用。简单点说就是:myController retain了completionHandler,而completionHandler也retain了myController。循环引用导致了myController和completionHandler最终都不能被释放。我们在delegate关系中,对delegate指针用weak就是为了避免这种问题。

不过好在,编译器会及时地给我们一个警告,提醒我们可能会发生这类型的问题:

iOS Block 内存管理的探讨

对这种情况 我们一般用如下的方法解决:给要进入Block的指针加一个__block修饰符。

这个__Block在MRC时代有两个作用。

1.说明变量可以被改变

2.说明指针指向的对象不做这个隐式的retain操作。

一个变量如果不加__block 是不能在Block中修改的。不过这有一个例外 static变量和全局变量不需要加__block就可以在Block中修改。

使用这种方法,我们对代码做出修改 解决了循环引用的问题。

MyViewController * __block myController = [[MyViewController alloc] init…];
// ...
myController.completionHandler = ^(NSInteger result) {
[myController dismissViewControllerAnimated:YES completion:nil];
};
//之后正常的release或者retain

ARC和Block

在ARC引入后,不再人为的添加Retain和release操作。情况也发生了改变。在ARC中任何情况下,__block 都只有一个作用:说明变量可以修改。即使加上了__block修饰符,一个被block捕获的请引用也依然是一个强引用。这样 在ARC的情况下,如果我们按照MRC下的写法,completionHandler对myController有一个强引用。myController对completionHandler有一个强引用,这依然是循环引用,没有解决问题:

于是我们还需要对源码作出修改。简单的情况我们可以这样写

__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持有的是一个弱引用,而不是一个强引用。如此,我们继续修改代码:

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被调用前释放掉了。于是,我们对代码再次做出修改:

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的实现原理.关于block的实现原理。文章开头已经推荐了文章。大家可以看一下。

为了更清楚地说明问题,这里用一个简单的程序举例。比如我们有如下程序:

#include <stdio.h>

int main()
{
int b = ; int *a = &b; void (^blockFunc)() = ^(){ int *c = a; }; blockFunc(); return ;

程序中,同为int型的指针,a是被Block捕获的变量,而c是在Block内定义的变量。我们用clang -rewrite-objc处理后,可以看到如下代码:

原main函数:

int main()
{
int b = ; 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 ;
}

Block的结构:

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=) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

实际执行的函数:

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的结构体可以理解是这个样子

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=) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

而反观我们给Block传入一个弱引用weakMyController,这时我们Block的结构:

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=) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

再看在Block内声明的强引用strongMyController,它虽然是强引用,但存在于函数栈中,在函数执行期间,它一直存在,所以myController对象也一直存在,但是当函数执行完毕,strongMyController即被销毁,于是它对myController对象的强引用也被解除,这时Block对myController对象就不存在强引用关系了!加入了strongMyController的函数大体会是这个样子:

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等操作,实际只需要考虑循环引用的问题就行了。比如下面这种,是没有内存泄露的问题的:

TestObject *aObject = [[TestObject alloc] init];

aObject.name = @"hehe";

self.aBlock = ^(){

    NSLog(@"aObject's name = %@",aObject.name);

};