之前的文章中介绍了关于block的定义和使用。这篇文章主要介绍跟block的内存管理的有关知识。重点介绍block在使用内部变量、外部变量和ARC、非ARC的情况下的使用,以及block引发的循环引用问题。
根据Block在内存中的位置分为三种类型NSGlobalBlock,NSStackBlock, NSMallocBlock。(知道就行)
<1>NSGlobalBlock:类似函数,位于text段;
<2>NSStackBlock:位于栈内存,函数返回后Block将无效;
<3>NSMallocBlock:位于堆内存。
使用内部变量在ARC和非ARC条件下:
对于没有引用外部变量的Block,无论在ARC还是非ARC下,类型都是__NSGlobalBlock__,这种类型的block可以理解成一种全局的block,不需要考虑作用域问题。同时,对block进行Copy或者Retain操作也是无效的,
typedef int(^MyBlock)();
MyBlock func() {
return ^{ return 123; };
}
// 在非ARC下执行如下代码:
- (void)test {
MyBlock block = func();
NSLog(@"%d", block());
NSLog(@"%@", [block class]);
MyBlock block2 = [block copy];
//Copy操作对__NSGlobalBlock__类型无效
NSLog(@"%d", block == block2);
}
输出:
123
__NSGlobalBlock__
1
可以看到,copy后的block和原来是同一个对象的。
使用外部变量在ARC条件下:
对于引用了外部变量的block,如果没有对他进行copy,它的作用域只会在声明它的函数栈内(类型是__NSStackBlock__),如果在ARC下直接返回此类block是正确的,因为在ARC下,系统会自动添加copy,如下:
typedef int(^MyBlock)();
MyBlock func() {
int i = 123;
return ^{ return i; };// 此处block使用了外部变量i
}
// 在非ARC下执行如下代码:
void test() {
MyBlock block = func();
NSLog(@"%d", block());
NSLog(@"%@", [block class]);
}
输出:
123
__NSMallocBlock__
类型是__NSMallocBlock__,说明block已经被copy到了堆中了。
使用外部变量在非ARC条件下:
block内使用外部变量,在非ARC下编译会直接报错(Xcode提示Returningblock that lives on the local stack),因为此时block还在栈中,不能执行retain操作。
当然其实在非ARC下,也可以使上面的情况编译通过,如下
typedef int(^MyBlock)();
MyBlock func()
{
int i = 123;
//非ARC下不要这样!!!
MyBlock ret = ^{ return i; };
return ret;
}
我们把原来的返回值赋给一个变量,然后再返回这个变量,就可以编译通过了。不过虽然编译通过了,这个返回的block作用域仍是在函数栈中的,因此一旦函数运行完毕后再使用这个block很可能会引发BAD_ACCESS错误。
所以在非
ARC下,必须把block复制到堆中才可以在函数外使用,或使用外部变量,如下:
typedef int(^MyBlock)();
MyBlock func()
{
//非ARC
int i = 123;
return [^{ return i; } copy];
}
block修改外部变量的值
block使用外部变量,并不对它修改的时候,一般来说,block 只是将变量值复制过来的。而当我们用__block标记的时候,被标记__block的变量事实上应该说是被移动到了堆上。表示在 block 中的修改对于 block 外也是有效地。(ARC和非ARC是一样的)
下面总结一下block被copy后,它所引用的变量被复制到了堆中的情况,如下代码(非ARC下):
//非ARC
void func()
{
int a = 123;
__block int b = 123;
NSLog(@"%@", @"=== block copy前");
NSLog(@"&a = %p, &b = %p", &a, &b);
void(^block)() = ^{
NSLog(@"%@", @"=== Block");
NSLog(@"&a = %p, &b = %p", &a, &b);
NSLog(@"a = %d, b = %d", a, b = 456);
};
block = [block copy];
block();
NSLog(@"%@", @"=== block copy后");
NSLog(@"&a = %p, &b = %p", &a, &b);
NSLog(@"a = %d, b = %d", a, b);
[block release];
}
输出:
=== block copy前
&a = 0x7fff5fbff8bc, &b = 0x7fff5fbff8b0
=== Block
&a = 0x100201048, &b = 0x100201068
a = 123, b = 456
=== block copy后
&a = 0x7fff5fbff8bc, &b = 0x100201068
a = 123, b = 456
可以看到,在block执行中,他所引用的变量a和b都被复制到了堆上。因此,当block执行后,函数栈内访问被__block标记的b的时候,b的地址会变成堆中的地址。而变量a,仍会指向函数栈内原有的变量a的空间。
循环引用
只要对象没有强指针指向它,它就会被释放,最后被销毁。
前面说过,使用block不管在ARC还是非ARC ,都必须使用copy将block复制到堆中。但block在copy时都会对block内部用到的对象进行强引用(ARC)或者retainCount增1(非ARC)。在ARC与非ARC环境下对block使用不当都会引起循环引用问题。
一般表现为:某个类将block作为自己的属性变量,然后该类在block的方法体里面又使用了该类本身。如下图:
block的这种循环引用会被编译器捕捉到并及时提醒。
网上大部分帖子都表述为"block里面引用了self导致循环引用",但事实真的是如此吗?其实这种说法是不严谨的,不一定要显式地出现"self"字眼才会引起循环引用。我们改一下代码,不通过属性self去访问变量,而是通过实例变量_去访问,如下:
很明显了:
即使在你的block代码中没有显式地出现"self",也会出现循环引用!只要你在block里用到了self所拥有的东西!就有可能引发循环引用。
循环引用使得两者都无法释放,导致内存泄露。解决思想:可以将其中一个声明为弱指针,这样就可以避免循环引用问题。
ARC
__weak typeof(self) weakSelf=self; // 其实 __weak someClass *weakSelf = self也是OK的!!!
__block typeof(self) blockSelf=self; // 其实 __block someClass*blockSelf = self也是OK的!!!
非ARC
__block typeof(self) blockSelf=self; // 其实 __block someClass *blockSelf = self也是OK的!!!
__block和__weak修饰符的区别其实是挺明显的:
1.__block不管是ARC还是MRC模式下都可以使用,可以修饰对象,还可以修饰基本数据类型。
2.__weak只能在ARC模式下使用,也只能修饰对象(如:NSString),不能修饰基本数据类型(如:int)。
3.__block对象可以在block中被重新赋值,__weak不可以。
PS:__unsafe_unretained修饰符可以被视为iOSSDK 4.3以前版本的__weak的替代品,不过不会被自动置空为nil。所以尽可能不要使用这个修饰符。
看下面的例子(ARC):
定义一个HXTool工具类,如下:
#import <Foundation/Foundation.h>
typedef void (^blockOperation)();
@interface HXTool : NSObject
@property (nonatomic, copy) blockOperation operation;// 执行打印操作
@end
在控制器view中,ViewController.m
#import "ViewController.h"
#import "HXTool.h"
@interface ViewController ()
@property (nonatomic, strong) HXTool *tool;
@property (nonatomic, assign) int age;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
HXTool *tool = [[HXTool alloc] init];
self.tool = tool;
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.tool.operation = ^(){
self.age ++;
NSLog(@"age = %d", self.age);
};
self.tool.operation();
}
@end
上面写法对应ViewController和HXTool的关系:
每个对象都有强指针引用着,谁也释放不了,导致循环引用。
将self声明为__blockViewController *vc = self;或者 __weak ViewController *vc = self;则关系变成下图:
(上面2张图中实线代表强引用,虚线代表弱引用)
由于self被声明为弱引用,block不会再对它进行强制引用,当不在使用self的时候,由于没有强引用引用它,所有它可以被释放掉,这样就不会循环引用。
retain cycle问题的根源在于Block和obj可能会互相强引用,互相retain对方,这样就导致了retaincycle,最后这个Block和obj就变成了孤岛,谁也释放不了谁。
解决:将其中一个声明为弱指针即可。