Block基础和retain cycle(循环引用)
blcok简介
Block 是c语言的扩展,并不是什么高新技术是从c扩展而来的,和swift语言的闭包需要注意的是由于 Objective-C在iOS中不支持GC机制。错误的内存管理 要么导致return cycle内存泄漏要么内存被提前释放导致crash.Block的使用很像函数指针,不过与函数最大的不同是:Block可以访问函数以外、词法作用域以内的外部变量的值。换句话说,Block不仅 实现函数的功能,还能携带函数的执行环境。
blcok基本语法
-
1.如何定义block变量
第一个block是一个int类型的返回值,并且有两个参数 第二个block是没有返回值,没有参数的block int (^sumBlock)(init,int); void (^myBlock)()
-
2.如何使用block来封装代码
最基本的用法 int (^sumBlock)(int,int) = ^(int a,int b){ return a- b; }; 宏定义一个block typedef int (^MyBlock)(int, int); 利用宏定义来定义变量 MyBlock sumBlock; 定义一个block变量来实现两个参数相加 sumBlock = ^(int a, int b) { return a + b; }; 定义一个block变量来实现两个参数相减 MyBlock minusBlock = ^(int a, int b) { return a - b; }; 定义一个block变量来实现两个参数相乘 MyBlock multiplyBlock = ^(int a, int b) { return a * b; };
-
3如何调用block
NSLog(@"%d - %d - %d", multiplyBlock(2, 4), sumBlock(10 , 9), minusBlock(10, 8)); 这个依次输出是 8,19,2
-
4.block可以访问外部变量
int a = 10; 给局部变量加上__block之后就可以改变b局部变量的值,将取变量此刻运行时的值 __block int b = 2; //定义一个block void (^block)(); block = ^{ 默认情况下,block内部不能修改外面的局部变量 a = 20; 给局部变量加上__block关键字,这个局部变量就可以在block内部修改 b = 25; }; block(); NSlog("%d,%d",a,b);
block基本理解
- 1.Block执行的代码其实在编译的时候就已经准备好了,就等着我们调用
- 2.一个包含Block执行时需要的所有外部变量值的数据结构。 Block将使用到的、作用域附近到的变量的值建立一份快照拷贝到栈上
blcok在内存中的分析
block内存中的三个位置 NSGlobalBlock,NSStackBlock, NSMallocBlock
NSGlobalBlock : 和函数类似,位于text代码段
NSStackBlock : 栈内存,函数返回后Block将无效
-
NSMallocBlock : 堆内存
宏定义一个block typedef long (^BlockSum)(int, int); BlockSum block1 = ^ long(int a,int b){ return a + b ; }; //<__NSGlobalBlock__: 0x100001060> NSLog(@"%@",block1); int base = 100; BlockSum block2 = ^ long (int a,int b){ return base + a + b; }; //arc和非arc所在的内存位置不同 //mrc // <__NSStackBlock__: 0x7fff5fbff7e8>/ //arc //<__NSMallocBlock__: 0x10010deb0> NSLog(@"%@",block2); BlockSum block3 = [block2 copy]; //<__NSMallocBlock__: 0x10010deb0> NSLog(@"%@",block3);
上述中为什么block1在NSGlobalBlock中,block2在NSStackBlock(mrc),NSMallocBlock(arc)中
因为block用到了外部的变量base,需要建立局部变量的快照,所以在(定义,不是运行)局部变量被拷贝到栈上(mrc),堆(arc)
ObjectC int base = 2;
base + = 2;
BlockSum sum = ^ long (int a,int b){
return base + a + b;
}
base ++ ;
NSLog("%ld",sum(1,2));
``
分析上述代码,因为有局部变量拷贝到栈里或者堆里,所以不会用运行时的变量base而是拷贝base所以
输出的结果为 7,不是8
Block的copy,retain,release操作
- 对block retain操作并不会改变引用计数器,retainCount ,始终为1
- NSGlobalBlock:retain、copy、release操作都无效;
- Block_copy与copy等效,Block_release与release等效
- NSStackBlock:retain、release操作无效,必须注意的是,NSStackBlock在函数返回后,Block内存将被回收。即使retain也没用。容易犯的 错误是[[mutableAarry addObject:stackBlock],在函数出栈后,从mutableAarry中取到的stackBlock已经被回收,变成了野指针。正确的做法是先将stackBlock copy到堆上,然后加入数组:[mutableAarry addObject:[[stackBlock copy] autorelease]]。支持copy,copy之后生成新的NSMallocBlock类型对象。
- NSMallocBlock支持retain、release,虽然retainCount始终是1,但内存管理器中仍然会增加、减少计数。copy之后不会生成新的对象,只是增加了一次引用,类似retain
- 尽量不要对Block使用retain操作
Block不同类型的变量
-
static 和基本数据类型
static int base = 100; int base = 100; BlockSum sum = ^ long (int a,int b){ return a + b + base; }; base = 0; NSLog(@"%ld\n",sum(1,2));
上述的类型如果是static的时候外部可以改变base变量,因为一直是一个内存地址,并没有建立局部变量的快照,不是在定义时copy的常量
如果是基本类型的话会建立一个拷贝,不是同一个地址所以值不会改变
所以static输出的是 3 ,基本数据类型是 103
-
static变量 如果block中也有变量的时候
static int base = 10; BlockSum sum = ^long (int a,int b){ base ++; return base + a + b; } base = 0; NSLog("%d\n,%ld\n,%d\n",base,sum(1,2),base);
这段代码输出的结果为,0,4,1,这段代码说明block内部对外部static修饰的变量可以在内部进行修改,如果不加static或者block的会报错
Block变量,被__block修饰的变量,称作block变量, 基本类型的Block变量等效于全局变量、或静态变量
Block被另一个Block使用时,另一个Block被copy到堆上时,被使用的Block也会被copy。但作为参数的Block是不会发生copy的
arc的block所有的都在堆上边
-
mrc的看下边的实例
int main(){ int base = 10; BlockSum block1 = ^ long(int a,int b){ return base + a + b; }; //<__NSStackBlock__: 0x7fff5fbff7f8> NSLog(@"%@",block1); bar(block1); return 0;
}
void bar(BlockSum block2){
// <__NSStackBlock__: 0x7fff5fbff7f8>
NSLog(@"%@",block2);
void (^block3) (BlockSum) = ^(BlockSum sum){
NSLog(@"%@",sum);
NSLog(@"%@",block2);
};
// <__NSStackBlock__: 0x7fff5fbff7f8>
// <__NSStackBlock__: 0x7fff5fbff7f8>
block3(block2);
block3 = [block3 copy];
// <__NSStackBlock__: 0x7fff5fbff7f8>
// <__NSMallocBlock__: 0x100206780>
block3(block2);
}
-
ObjC对象,不同于基本类型,Block会引起对象的引用计数变化
@interface MyClass : NSObject { NSObject* _instanceObj; } @end @implementation MyClass NSObject* __globalObj = nil; - (id) init { if (self = [super init]) { _instanceObj = [[NSObject alloc] init]; } return self; } - (void) test { static NSObject* __staticObj = nil; __globalObj = [[NSObject alloc] init]; __staticObj = [[NSObject alloc] init]; NSObject* localObj = [[NSObject alloc] init]; __block NSObject* blockObj = [[NSObject alloc] init]; typedef void (^MyBlock)(void) ; MyBlock aBlock = ^{ NSLog(@"%@", __globalObj); NSLog(@"%@", __staticObj); NSLog(@"%@", _instanceObj); NSLog(@"%@", localObj); NSLog(@"%@", blockObj); }; aBlock = [[aBlock copy] autorelease]; aBlock(); NSLog(@"%d", [__globalObj retainCount]); NSLog(@"%d", [__staticObj retainCount]); NSLog(@"%d", [_instanceObj retainCount]); NSLog(@"%d", [localObj retainCount]); NSLog(@"%d", [blockObj retainCount]); } @end int main(int argc, char *argv[]) { @autoreleasepool { MyClass* obj = [[[MyClass alloc] init] autorelease]; [obj test]; return 0; } }
执行结果为1 1 1 2 1。
__globalObj和__staticObj在内存中的位置是确定的,所以Block copy时不会retain对象。
_instanceObj在Block copy时也没有直接retain _instanceObj对象本身,但会retain self。所以在Block中可以直接读写_instanceObj变量。
localObj在Block copy时,系统自动retain对象,增加其引用计数。
blockObj在Block copy时也不会retain。
非ObjC对象,如GCD队列dispatch_queue_t。Block copy时并不会自动增加他的引用计数,这点要非常小心。
-
Block中使用的ObjC对象的行为
@property (nonatomic, copy) void(^myBlock)(void); MyClass* obj = [[[MyClass alloc] init] autorelease]; self.myBlock = ^ { [obj doSomething]; };
对象obj在Block被copy到堆上的时候自动retain了一次。因为Block不知道obj什么时候被释放,为了不在Block使用obj前被释放,Block retain了obj一次,在Block被释放的时候,obj被release一次。
retain cycle(循环引用的问题)
retain cycle问题的根源在于Block和obj可能会互相强引用,互相retain对方,这样就导致了retain cycle,最后这个Block和obj就变成了孤岛,谁也释放不了谁。比如:
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setCompletionBlock:^{
NSString* string = [request responseString];
}];
在上边这个实例中request和Block循环引用,所以我们只需要打断其中的循环即可,
解决这个问题的办法是使用弱引用打断retain cycle:
__block ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setCompletionBlock:^{
NSString* string = [request responseString];
}];
request被持有者释放后。request 的retainCount变成0,request被dealloc,request释放持有的Block,导致Block的retainCount变成0,也被销毁。这样这两个对象内存都被回收
与上面情况类似的是
//self和block循环引用解决办法同上
self.myBlock = ^{
[self doSomething];
}
@property (nonatomic, retain) NSString* someVar;
self.myBlock = ^ {
NSLog(@"%@", _someVer);
};
NSString* str = _someVer;
self.myBlock = ^ {
NSLog(@"%@", str);
};
上述的循环引用是对象的属性的话,retain会reatin对象,所以产生self和block的循环引用
-
retain cycle不只发生在两个对象之间,也可能发生在多个对象之间,这样问题更复杂,更难发现
ClassA* objA = [[[ClassA alloc] init] autorelease]; objA.myBlock = ^{ [self doSomething]; }; self.objA = objA;
解决办法同样是用__block打破循环引用
ClassA* objA = [[[ClassA alloc] init] autorelease];
MyClass* weakSelf = self;
objA.myBlock = ^{
[weakSelf doSomething];
};
self.objA = objA;
对上边的进行分析 self(retain 1) ----> objA(retain 1) ---->Block (retain 1)---->self 循环引用
- 注意:MRC中__block是不会引起retain;但在ARC中__block则会引起retain。ARC中应该使用__weak或__unsafe_unretained弱引用。__weak只能在iOS5以后使用。
block对象被提前释放
看下面例子,有这种情况,如果不只是request持有了Block,另一个对象也持有了Block(下边的等号是一条虚线一条实线,block指向request 的是虚线)
--->request =======>Block<---- ObjA
这时request已被完全释放,但Block仍被objA持有,没有释放,如果这时触发了Block,在Block中将访问已经销毁的request,这将导致程序crash。为了避免这种情况,开发者必须要注意对象和Block的生命周期。
另一个常见错误使用是,开发者担心retain cycle错误的使用__block。比如
__block kkProducView* weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
weakSelf.xx = xx;
});
将Block作为参数传给dispatch_async时,系统会将Block拷贝到堆上,如果Block中使用了实例变量,还将retain self,因为dispatch_async并不知道self会在什么时候被释放,为了确保系统调度执行Block中的任务时self没有被意外释放掉,dispatch_async必须自己retain一次self,任务完成后再release self。但这里使用__block,使dispatch_async没有增加self的引用计数,这使得在系统在调度执行Block之前,self可能已被销毁,但系统并不知道这个情况,导致Block被调度执行时self已经被释放导致crash。
// MyClass.m
- (void) test {
__block MyClass* weakSelf = self;
double delayInSeconds = 10.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
NSLog(@"%@", weakSelf);
});
// other.m
MyClass* obj = [[[MyClass alloc] init] autorelease];
[obj test];
这里用dispatch_after模拟了一个异步任务,10秒后执行Block。但执行Block的时候MyClass* obj已经被释放了,导致crash。解决办法是不要使用__block。
大部分都是借鉴转载自:tanqisen.github.io/blog/2013/04/19/gcd-block-cycle-retain/