Block基础和retain cycle(循环引用)

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)(int,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/     
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容

  • Block使用场景,可以在两个界面的传值,也可以对代码封装作为参数的传递等。用过GCD就知道Block的精妙之处。...
    Coder_JMicheal阅读 696评论 2 1
  • iOS代码块Block 概述 代码块Block是苹果在iOS4开始引入的对C语言的扩展,用来实现匿名函数的特性,B...
    smile刺客阅读 2,244评论 2 26
  • 使用block已经有一段时间了,感觉自己了解的还行,但是几天前看到CocoaChina上一个关于block的小测试...
    心愿2016阅读 332评论 0 0
  • 前言 Blocks是C语言的扩充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了这...
    小人不才阅读 3,727评论 0 23
  • 高兴,快乐……都可以形容心中的喜悦,但还有一种喜悦是无法形容的。 ...
    平淡很华阅读 190评论 0 0