iOS 中使用Block时需要注意的retain circle

时间:2023-03-09 18:19:04
iOS 中使用Block时需要注意的retain circle

现在在ios中,block是越来越多了。自己在类中定义block对象时,需要注意block对象的使用方法,防止产生retain circle,导致内存泄露。

现在分析一下产生retain circle的原因

比如我建立了Tools类,之后 建立了一个strong 类型的 block指针 callbackBlock,作为回掉函数使用。

之后在A类中,建立了Tools类的一个对象,并用Strong指针 aTool指向这个对象,再将它的callbackBlock指针赋值,也就是写出具体的block实现。

如果我在block的实现中用到了 self 这个指针,那么会导致retain circle。

形成的circle 如下图

iOS 中使用Block时需要注意的retain circle

解决方法:不使用self指针,使用A对象的weak引用,如果必须使用self指针,那么可以再block结束处加上 aTool = nil,用来打断retain circle。

除了这种比较明显的循环引用外,还有一些不明显的,比如以下代码

[[NSNotificationCenter defaultCenter] addObserverForName:@"UIWindowDidRotateNotification" object:nil queue:nil usingBlock:^(NSNotification *note) {
if ([note.userInfo[@"UIWindowOldOrientationUserInfoKey"] intValue] >= ) {
self.navigationController.navigationBar.frame = (CGRect){, , self.view.frame.size.width, };
}
}];

这是一段在 view controller 中的代码,由于block中使用了self指针,导致这个view controller无法被释放。由于调用的是系统函数,我们并不知道为何形成了循环引用,但是这个现象给了我们一个提示:在对象内部使用block时,一定要使用 weak self 指针!

另外,即使不直接使用self,而是使用self中定义的变量也不行!(ios8,ios9,测试了,不行,但是我记得这种情况以前不会有问题啊???难道是ios7?)

比如一下代码

@interface HubPreviewViewController (){

    NSURL *webViewURL;

}

调用方法如下:

[[NSNotificationCenter defaultCenter] addObserverForName:@"UIWindowDidRotateNotification" object:nil queue:nil usingBlock:^(NSNotification *note) {

        NSLog(@"webview url is %@",    [weakSelf valueForKey:@"webViewURL"]);//一切正常,没有循环引用
     NSLog(@"webview url is %@", webViewURL); //会造成循环引用 ,为什么呢?????没有self的调用啊??
}];

我猜测一个对象内调用的block的所有权都在这个对象上,比如上面这个例子,虽然引用block时是这样写的

^(NSNotification *note) {
if ([note.userInfo[@"UIWindowOldOrientationUserInfoKey"] intValue] >= ) {
self.navigationController.navigationBar.frame = (CGRect){, , self.view.frame.size.width, };
}
}

没有看到明显的strong 类型的 block对象指针,但是实际上,系统隐含地为这个block在 view controller对象中创建了一个 strong 类型指针,用来保存具体内容,这个block 的所有权和 上面代码中的NSNotificationCenter无关,所以从notification center 里是否移除 self 这个observer 都对block的释放没有影响。

这里推荐一篇关于block的专业文章,http://blog.****.net/jasonblog/article/details/7756763

阅读后,开始测试,先贴上我的测试文件:

#import <Foundation/Foundation.h>

@interface Person : NSObject{

    NSURL *Testurl;
} @end @implementation Person - (void)test{ [[NSNotificationCenter defaultCenter] addObserverForName:@"test" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) { NSURL * innerUrl = [NSURL URLWithString:@"test"];
}]; } - (void)dealloc{
[super dealloc];
NSLog(@"dealloc........");
} @end int main(int argc, const char * argv[]) {
// insert code here...
printf("Hello, World!\n");
Person *person = [[Person alloc] init];
[person test];
[person release];
return ;
}

这个是person对象可以正常释放的版本,由于clang编译器的一些原因,这里没有用ARC。

在控制台输入编译命令:

clang  -framework Foundation test.m -o test  

得到可执行2进制文件:

iOS 中使用Block时需要注意的retain circle

拖入控制台执行:

iOS 中使用Block时需要注意的retain circle

再执行一条命令,生成可以被我们研究的cpp文件:

clang  -rewrite-objc test.m

没有引用循环的版本完成了,再改写person对象无法正确释放的代码:

#import <Foundation/Foundation.h>

@interface Person : NSObject{

    NSURL *Testurl;
} @end @implementation Person - (void)test{ [[NSNotificationCenter defaultCenter] addObserverForName:@"test" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) { Testurl = [NSURL URLWithString:@"test"];
}]; } - (void)dealloc{
[super dealloc];
NSLog(@"dealloc........");
} @end int main(int argc, const char * argv[]) {
// insert code here...
printf("Hello, World!\n");
Person *person = [[Person alloc] init];
[person test];
[person release];
return ;
}

同样经过刚才的2个步奏,但是这次的控制台没有输出 dealloc 。

为了探究原因,我们看2次生成的cpp有什么不同。

没有问题的版本:

struct __Person__test_block_impl_0 {
struct __block_impl impl;
struct __Person__test_block_desc_0* Desc;
__Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, int flags=) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

有问题的版本:

struct __Person__test_block_impl_0 {
struct __block_impl impl;
struct __Person__test_block_desc_0* Desc;
Person *self;
__Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, Person *_self, int flags=) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

而且,有问题的版本还会多2个函数:

static void __Person__test_block_copy_0(struct __Person__test_block_impl_0*dst, struct __Person__test_block_impl_0*src) {_Block_object_assign((void*)&dst->self, (void*)src->self, /*BLOCK_FIELD_IS_OBJECT*/);}

static void __Person__test_block_dispose_0(struct __Person__test_block_impl_0*src) {_Block_object_dispose((void*)src->self, /*BLOCK_FIELD_IS_OBJECT*/);}

这里的self,正是Person对象,推测这2个函数就是为了释放block对person的“强引用”而存在的。可见,有问题的版本的中的block的确对person对象进行了所谓的强引用,那么又是谁对这个block进行了强引用呢?我在代码中没有找到对应的指针,后来发现是notification center:

The block to be executed when the notification is received.

The block is copied by the notification center and (the copy) held until the observer registration is removed.

To unregister observations, you pass the object returned by this method to removeObserver:. You must invoke removeObserver: or removeObserver:name:object: before any object specified by addObserverForName:object:queue:usingBlock: is deallocated.

又这个提示我又写了改写了代码:

#import <Foundation/Foundation.h>

@interface Person : NSObject{

    NSURL *Testurl;
} @end @implementation Person - (id)test{ void (^abcdTestBolck) (NSNotification * _Nonnull note) = ^ void (NSNotification * _Nonnull note){ Testurl = [NSURL URLWithString:@"test"]; }; id returnValue = [[NSNotificationCenter defaultCenter] addObserverForName:@"test" object:nil queue:nil usingBlock:abcdTestBolck]; return returnValue; } - (void)dealloc{
[super dealloc];
NSLog(@"dealloc........");
} @end int main(int argc, const char * argv[]) {
// insert code here...
printf("Hello, World!\n");
Person *person = [[Person alloc] init];
id returnedByNoti = [person test];
[[NSNotificationCenter defaultCenter] removeObserver:returnedByNoti]; [person release];
return ;
}

上面这段代码的 person对象可以正确释放!因为把 block从 notification center 中移除了,所以 person对象的强引用也就没了!

由此我们看出,block中使用 self中的属性变量,会导致block对self的强引用,因此需要注意block的释放时机,不要导致self无法被释放!