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閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容

  • Block使用場景,可以在兩個界面的傳值,也可以對代碼封裝作為參數的傳遞等。用過GCD就知道Block的精妙之處。...
    Coder_JMicheal閱讀 731評論 2 1
  • iOS代碼塊Block 概述 代碼塊Block是蘋果在iOS4開始引入的對C語言的擴展,用來實現匿名函數的特性,B...
    smile刺客閱讀 2,361評論 2 26
  • 使用block已經有一段時間了,感覺自己了解的還行,但是幾天前看到CocoaChina上一個關于block的小測試...
    心愿2016閱讀 349評論 0 0
  • 前言 Blocks是C語言的擴充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了這...
    小人不才閱讀 3,778評論 0 23
  • 高興,快樂……都可以形容心中的喜悅,但還有一種喜悅是無法形容的。 ...
    平淡很華閱讀 208評論 0 0