原文地址:http://tanqisen.github.io/blog/2013/04/19/gcd-block-cycle-retain/

使用指南:http://blog.csdn.net/nicktang/article/details/6906352

Block简介

Block作为C语言的扩展,并不是高新技术,和其他语言的闭包或lambda表达式是一回事。需要注意的是由于Objective-C在iOS中不支持GC机制,使用Block必须自己管理内存,而内存管理正是使用Block坑最多的地方,错误的内存管理 要么导致return cycle内存泄漏要么内存被提前释放导致crash。 Block的使用很像函数指针,不过与函数最大的不同是:Block可以访问函数以外、词法作用域以内的外部变量的值。换句话说,Block不仅 实现函数的功能,还能携带函数的执行环境。

可以这样理解,Block其实包含两个部分内容

  1. Block执行的代码,这是在编译的时候已经生成好的;
  2. 一个包含Block执行时需要的所有外部变量值的数据结构。 Block将使用到的、作用域附近到的变量的值建立一份快照拷贝到栈上。

Block与函数另一个不同是,Block类似ObjC的对象,可以使用自动释放池管理内存(但Block并不完全等同于ObjC对象,后面将详细说明)。

Block基本语法

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  1. // 声明一个Block变量
  2. long (^sum) (int, int) = nil;
  3. // sum是个Block变量,该Block类型有两个int型参数,返回类型是long。
  4. // 定义Block并赋给变量sum
  5. sum = ^ long (int a, int b) {
  6. return a + b;
  7. };
  8. // 调用Block:
  9. long s = sum(1, 2);

定义一个实例函数,该函数返回Block:

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  1. - (long (^)(int, int)) sumBlock {
  2. int base = 100;
  3. return [[ ^ long (int a, int b) {
  4. return base + a + b;
  5. } copy] autorelease];
  6. }
  7. // 调用Block
  8. [self sumBlock](1,2);

是不是感觉很怪?为了看的舒服,我们把Block类型typedef一下

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  1. typedef long (^BlkSum)(int, int);
  2. - (BlkSum) sumBlock {
  3. int base = 100;
  4. BlkSum blk = ^ long (int a, int b) {
  5. return base + a + b;
  6. }
  7. return [[blk copy] autorelease];
  8. }

Block在内存中的位置

根据Block在内存中的位置分为三种类型NSGlobalBlock,NSStackBlock, NSMallocBlock。

  • NSGlobalBlock:类似函数,位于text段;
  • NSStackBlock:位于栈内存,函数返回后Block将无效;
  • NSMallocBlock:位于堆内存。
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  1. BlkSum blk1 = ^ long (int a, int b) {
  2. return a + b;
  3. };
  4. NSLog(@"blk1 = %@", blk1);// blk1 = <__NSGlobalBlock__: 0x47d0>
  5. int base = 100;
  6. BlkSum blk2 = ^ long (int a, int b) {
  7. return base + a + b;
  8. };
  9. NSLog(@"blk2 = %@", blk2); // blk2 = <__NSStackBlock__: 0xbfffddf8>
  10. BlkSum blk3 = [[blk2 copy] autorelease];
  11. NSLog(@"blk3 = %@", blk3); // blk3 = <__NSMallocBlock__: 0x902fda0>

为什么blk1类型是NSGlobalBlock,而blk2类型是NSStackBlock?blk1和blk2的区别在于,blk1没有使用Block以外的任何外部变量,Block不需要建立局部变量值的快照,这使blk1与函数没有任何区别,从blk1所在内存地址0x47d0猜测编译器把blk1放到了text代码段。blk2与blk1唯一不同是的使用了局部变量base,在定义(注意是定义,不是运行)blk2时,局部变量base当前值被copy到栈上,作为常量供Block使用。执行下面代码,结果是203,而不是204。

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  1. int base = 100;
  2. base += 100;
  3. BlkSum sum = ^ long (int a, int b) {
  4. return base + a + b;
  5. };
  6. base++;
  7. printf("%ld",sum(1,2));

在Block内变量base是只读的,如果想在Block内改变base的值,在定义base时要用 __block修饰:__block int base = 100;

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  1. __block int base = 100;
  2. base += 100;
  3. BlkSum sum = ^ long (int a, int b) {
  4. base += 10;
  5. return base + a + b;
  6. };
  7. base++;
  8. printf("%ld\n",sum(1,2));
  9. printf("%d\n",base);

输出将是214,211。Block中使用__block修饰的变量时,将取变量此刻运行时的值,而不是定义时的快照。这个例子中,执行sum(1,2)时,base将取base++之后的值,也就是201,再执行Blockbase+=10; base+a+b,运行结果是214。执行完Block时,base已经变成211了。

Block的copy、retain、release操作

不同于NSObjec的copy、retain、release操作:

  • Block_copy与copy等效,Block_release与release等效;
  • 对Block不管是retain、copy、release都不会改变引用计数retainCount,retainCount始终是1;
  • NSGlobalBlock:retain、copy、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对不同类型的变量的存取

基本类型
  • 局部自动变量,在Block中只读。Block定义时copy变量的值,在Block中作为常量使用,所以即使变量的值在Block外改变,也不影响他在Block中的值。
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  1. int base = 100;
  2. BlkSum sum = ^ long (int a, int b) {
  3. // base++; 编译错误,只读
  4. return base + a + b;
  5. };
  6. base = 0;
  7. printf("%ld\n",sum(1,2)); // 这里输出是103,而不是3
  • static变量、全局变量。如果把上个例子的base改成全局的、或static。Block就可以对他进行读写了。因为全局变量或静态变量在内存中的地址是固定的,Block在读取该变量值的时候是直接从其所在内存读出,获取到的是最新值,而不是在定义时copy的常量。
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  1. static int base = 100;
  2. BlkSum sum = ^ long (int a, int b) {
  3. base++;
  4. return base + a + b;
  5. };
  6. base = 0;
  7. printf("%d\n", base);
  8. printf("%ld\n",sum(1,2)); // 这里输出是3,而不是103
  9. printf("%d\n", base);

输出结果是0 4 1,表明Block外部对base的更新会影响Block中的base的取值,同样Block对base的更新也会影响Block外部的base值。

  • Block变量,被__block修饰的变量称作Block变量。 基本类型的Block变量等效于全局变量、或静态变量。
Block被另一个Block使用时,另一个Block被copy到堆上时,被使用的Block也会被copy。但作为参数的Block是不会发生copy的。
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  1. void foo() {
  2. int base = 100;
  3. BlkSum blk = ^ long (int a, int b) {
  4. return base + a + b;
  5. };
  6. NSLog(@"%@", blk); // <__NSStackBlock__: 0xbfffdb40>
  7. bar(blk);
  8. }
  9. void bar(BlkSum sum_blk) {
  10. NSLog(@"%@",sum_blk); // 与上面一样,说明作为参数传递时,并不会发生copy
  11. void (^blk) (BlkSum) = ^ (BlkSum sum) {
  12. NSLog(@"%@",sum); // 无论blk在堆上还是栈上,作为参数的Block不会发生copy。
  13. NSLog(@"%@",sum_blk); // 当blk copy到堆上时,sum_blk也被copy了一分到堆上上。
  14. };
  15. blk(sum_blk); // blk在栈上
  16. blk = [[blk copy] autorelease];
  17. blk(sum_blk); // blk在堆上
  18. }
ObjC对象,不同于基本类型,Block会引起对象的引用计数变化。

先看下面代码

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
  45. 45
  46. 46
  47. 47
  48. 48
  49. 49
  50. 50
  1. @interface MyClass : NSObject {
  2. NSObject* _instanceObj;
  3. }
  4. @end
  5. @implementation MyClass
  6. NSObject* __globalObj = nil;
  7. - (id) init {
  8. if (self = [super init]) {
  9. _instanceObj = [[NSObject alloc] init];
  10. }
  11. return self;
  12. }
  13. - (void) test {
  14. static NSObject* __staticObj = nil;
  15. __globalObj = [[NSObject alloc] init];
  16. __staticObj = [[NSObject alloc] init];
  17. NSObject* localObj = [[NSObject alloc] init];
  18. __block NSObject* blockObj = [[NSObject alloc] init];
  19. typedef void (^MyBlock)(void) ;
  20. MyBlock aBlock = ^{
  21. NSLog(@"%@", __globalObj);
  22. NSLog(@"%@", __staticObj);
  23. NSLog(@"%@", _instanceObj);
  24. NSLog(@"%@", localObj);
  25. NSLog(@"%@", blockObj);
  26. };
  27. aBlock = [[aBlock copy] autorelease];
  28. aBlock();
  29. NSLog(@"%d", [__globalObj retainCount]);
  30. NSLog(@"%d", [__staticObj retainCount]);
  31. NSLog(@"%d", [_instanceObj retainCount]);
  32. NSLog(@"%d", [localObj retainCount]);
  33. NSLog(@"%d", [blockObj retainCount]);
  34. }
  35. @end
  36. int main(int argc, char *argv[]) {
  37. @autoreleasepool {
  38. MyClass* obj = [[[MyClass alloc] init] autorelease];
  39. [obj test];
  40. return 0;
  41. }
  42. }

执行结果为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对象的行为

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  1. @property (nonatomic, copy) void(^myBlock)(void);
  2. MyClass* obj = [[[MyClass alloc] init] autorelease];
  3. self.myBlock = ^ {
  4. [obj doSomething];
  5. };

对象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就变成了孤岛,谁也释放不了谁。比如:

  1. 1
  2. 2
  3. 3
  4. 4
  1. ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
  2. [request setCompletionBlock:^{
  3. NSString* string = [request responseString];
  4. }];
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  1. +-----------+ +-----------+
  2. | request | | Block |
  3. ---> | | --------> | |
  4. | retain 2 | <-------- | retain 1 |
  5. | | | |
  6. +-----------+ +-----------+

解决这个问题的办法是使用弱引用打断retain cycle:

  1. 1
  2. 2
  3. 3
  4. 4
  1. __block ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
  2. [request setCompletionBlock:^{
  3. NSString* string = [request responseString];
  4. }];
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  1. +-----------+ +-----------+
  2. | request | | Block |
  3. ---->| | --------> | |
  4. | retain 1 | < - - - - | retain 1 |
  5. | | weak | |
  6. +-----------+ +-----------+

request被持有者释放后。request 的retainCount变成0,request被dealloc,request释放持有的Block,导致Block的retainCount变成0,也被销毁。这样这两个对象内存都被回收。

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  1. +-----------+ +-----------+
  2. | request | | Block |
  3. --X->| | ----X---> | |
  4. | retain 0 | < - - - - | retain 0 |
  5. | | weak | |
  6. +-----------+ +-----------+

与上面情况类似的陷阱:

  1. 1
  2. 2
  3. 3
  1. self.myBlock = ^ {
  2. [self doSomething];
  3. };

这里self和myBlock循环引用,解决办法同上:

  1. 1
  2. 2
  3. 3
  4. 4
  1. __block MyClass* weakSelf = self;
  2. self.myBlock = ^ {
  3. [weakSelf doSomething];
  4. };
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  1. @property (nonatomic, retain) NSString* someVar;
  2. self.myBlock = ^ {
  3. NSLog(@"%@", _someVer);
  4. };

这里在Block中虽然没直接使用self,但使用了成员变量。在Block中使用成员变量,retain的不是这个变量,而会retain self。解决办法也和上面一样。

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  1. @property (nonatomic, retain) NSString* someVar;
  2. __block MyClass* weakSelf = self;
  3. self.myBlock = ^ {
  4. NSLog(@"%@", self.someVer);
  5. };

或者

  1. 1
  2. 2
  3. 3
  4. 4
  1. NSString* str = _someVer;
  2. self.myBlock = ^ {
  3. NSLog(@"%@", str);
  4. };

retain cycle不只发生在两个对象之间,也可能发生在多个对象之间,这样问题更复杂,更难发现

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  1. ClassA* objA = [[[ClassA alloc] init] autorelease];
  2. objA.myBlock = ^{
  3. [self doSomething];
  4. };
  5. self.objA = objA;
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  1. +-----------+ +-----------+ +-----------+
  2. | self | | objA | | Block |
  3. | | --------> | | --------> | |
  4. | retain 1 | | retain 1 | | retain 1 |
  5. | | | | | |
  6. +-----------+ +-----------+ +-----------+
  7. ^ |
  8. | |
  9. +------------------------------------------------+

解决办法同样是用__block打破循环引用

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  1. ClassA* objA = [[[ClassA alloc] init] autorelease];
  2. MyClass* weakSelf = self;
  3. objA.myBlock = ^{
  4. [weakSelf doSomething];
  5. };
  6. self.objA = objA;

注意:MRC中__block是不会引起retain;但在ARC中__block则会引起retain。ARC中应该使用__weak__unsafe_unretained弱引用。__weak只能在iOS5以后使用。

Block使用对象被提前释放

看下面例子,有这种情况,如果不只是request持有了Block,另一个对象也持有了Block。

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  1. +-----------+ +-----------+
  2. | request | | Block | objA
  3. ---->| | --------> | |<--------
  4. | retain 1 | < - - - - | retain 2 |
  5. | | weak | |
  6. +-----------+ +-----------+

这时如果request 被持有者释放。

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  1. +-----------+ +-----------+
  2. | request | | Block | objA
  3. --X->| | --------> | |<--------
  4. | retain 0 | < - - - - | retain 1 |
  5. | | weak | |
  6. +-----------+ +-----------+

这时request已被完全释放,但Block仍被objA持有,没有释放,如果这时触发了Block,在Block中将访问已经销毁的request,这将导致程序crash。为了避免这种情况,开发者必须要注意对象和Block的生命周期。

另一个常见错误使用是,开发者担心retain cycle错误的使用__block。比如

  1. 1
  2. 2
  3. 3
  4. 4
  1. __block kkProducView* weakSelf = self;
  2. dispatch_async(dispatch_get_main_queue(), ^{
  3. weakSelf.xx = xx;
  4. });

将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。

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  1. // MyClass.m
  2. - (void) test {
  3. __block MyClass* weakSelf = self;
  4. double delayInSeconds = 10.0;
  5. dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
  6. dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
  7. NSLog(@"%@", weakSelf);
  8. });
  9. // other.m
  10. MyClass* obj = [[[MyClass alloc] init] autorelease];
  11. [obj test];

这里用dispatch_after模拟了一个异步任务,10秒后执行Block。但执行Block的时候MyClass* obj已经被释放了,导致crash。解决办法是不要使用__block

【转】正确使用Block避免Cycle Retain和Crash的更多相关文章

  1. 正确使用Block避免Cycle Retain和Crash

    Block简介 Block作为C语言的扩展,并不是高新技术,和其他语言的闭包或lambda表达式是一回事.需要注意的是由于Objective-C在iOS中不支持GC机制,使用Block必须自己管理内存 ...

  2. 教你 Debug 的正确姿势——记一次 CoreMotion 的 Crash

    作者:林蓝东 最近的一个手机 QQ 版本发出去后收到比较多关于 CoreMotion 的 crash 上报,案发现场如下: 但是看看这个堆栈发现它完全不按照套路出牌啊! 乍一看是挂在 CoreMoti ...

  3. ARC下需要注意的内存管理

    ARC下需要注意的内存管理 2016/04/03 · iOS开发 · 内存管理 分享到:1 原文出处: 一不(@luoyibu)    之前发了一篇关于图片加载优化的文章,还是引起很多人关注的,不过也 ...

  4. ARC的内存管理

        在objective-c中,内存的引用计数一直是一个让人比较头疼的问题.尤其是当引用计数涉及到arc.blocks等等的时候.似乎ARC的出现只是让我们解放了双手,由于底层实现依然依赖引用计数 ...

  5. objective-c启用ARC时的内存管理 (循环引用)

    PDF版下载:http://download.csdn.net/detail/cuibo1123/7443125          在Objective-C中,内存的引用计数一直是一个让人比较头疼的问 ...

  6. objective-c启用ARC时的内存管理

    PDF版下载:http://download.csdn.net/detail/cuibo1123/7443125      在objective-c中,内存的引用计数一直是一个让人比較头疼的问题.尤其 ...

  7. 小结OC中Retain cycle(循环引用)

    retain cycle 的产生 说到retain cycle,首先要提一下Objective-C的内存管理机制. 作为C语言的超集,Objective-C延续了C语言中手动管理内存的方式,但是区别于 ...

  8. block使用小结、在arc中使用block、如何防止循环引用

    引言 使用block已经有一段时间了,感觉自己了解的还行,但是几天前看到CocoaChina上一个关于block的小测试主题: [小测试]你真的知道blocks在Objective-C中是怎么工作的吗 ...

  9. block的那些事(从懵懂到使用)

    从大学开始自学iOS,在iOS岗位已经两年了,遇到传值等操作,代理和block二选一的话,以前我会毫不犹豫选择代理.久而久之,入职到大公司之后,发现处处是block的天地,才慢慢的了解block并爱上 ...

随机推荐

  1. python学习之---函数进阶

    一,递归函数: 做程序应该都知道,在一个函数的内部还可以调用其它函数,这叫函数的调用,但是有一种特殊的情况,在一个函数内部对自身函数的调用,我们成这为函数的递归调用. 在此,使用一个家喻户晓的例子来演 ...

  2. 【SPOJ 1182】 SORTBIT - Sorted bit squence (数位DP)

    SORTBIT - Sorted bit squence no tags Let's consider the 32 bit representation of all integers i from ...

  3. [转贴]Eclipse IDE for c++配置

    从工作到现在已经有快一年多没用过C/C++了,现在想重新捡起来,但是以前一直是在windows下面进行开发,使用最多的是Eclipse和Myeclipse,因为这些都是开源的软件,并不收费,所以现在也 ...

  4. SPRING IN ACTION 第4版笔记-第三章ADVANCING WIRING-002-激活PROFILE、设置默认值、@ActiveProfiles

    一. Spring honors two separate properties when determining which profiles are active:spring.profiles. ...

  5. [OJ] Lowest Common Ancestor

    LintCode 88. Lowest Common Ancestor (Medium) LeetCode 236. Lowest Common Ancestor of a Binary Tree ( ...

  6. 利用if else 求已发奖金总数

    class Program    {        static void Main(string[] args)        {            while (true)           ...

  7. 通过 WIN32 API 实现嵌入程序窗体

    写了一个不使用 COM, 而是通过 WIN32 API 实现的示例, 它把写字板程序嵌在了自己的一个面板中. 这么做可能没有实际意义, 因为两个程序之前没有进行有价值的交互, 这里仅仅是为了演示这么做 ...

  8. Mac 把图片反色

    黑色图变白色 1:用预览打开 2:打开"调整颜色" 3:把"自动色阶"两边的按钮, 拖动换位置,就可以看到效果了.

  9. ASP.NET使用EasyUI-DataGrid + ashx + JQuery Ajax:实现数据的增删查改,查询和分页!

    转自:http://www.cnblogs.com/lt-style/p/3457399.html 数据表: 学生表:学生编号.姓名.性别.班级编号.年龄 班级表:班级编号.班级名称 开发过程: 1. ...

  10. (转载)MySql按日期进行统计(前一天、本周、某一天)

    (转载)http://www.yovisun.com/mysql-date-statistics.html 在mysql数据库中,常常会遇到统计当天的内容.例如,在user表中,日期字段为:log_t ...