正確使用Block避免Cycle Retain和Crash

憶江南的部落格發表於2015-10-13

正確使用Block避免Cycle Retain和Crash

APR 19TH, 2013

本文只介紹了MRC時的情況,有些細節不適用於ARC。比如MRC下__block不會增加引用計數,但ARC會,ARC下必須用__weak指明不增加引用計數;ARC下block記憶體分配機制也與MRC不一樣,所以文中的一些例子在ARC下測試結果可能與文中描述的不一樣

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
2
3
4
5
6
7
8
9
10
11
// 宣告一個Block變數
long (^sum) (int, int) = nil;
// sum是個Block變數,該Block型別有兩個int型引數,返回型別是long。

// 定義Block並賦給變數sum
sum = ^ long (int a, int b) {
  return a + b;
};

// 呼叫Block:
long s = sum(1, 2);

定義一個例項函式,該函式返回Block:

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

// 呼叫Block
[self sumBlock](1,2);

是不是感覺很怪?為了看的舒服,我們把Block型別typedef一下

1
2
3
4
5
6
7
8
9
typedef long (^BlkSum)(int, int);

- (BlkSum) sumBlock {
    int base = 100;
    BlkSum blk = ^ long (int a, int b) {
      return base + a + b;
    }
    return [[blk copy] autorelease];
}

Block在記憶體中的位置

根據Block在記憶體中的位置分為三種型別NSGlobalBlock,NSStackBlock, NSMallocBlock。

  • NSGlobalBlock:類似函式,位於text段;
  • NSStackBlock:位於棧記憶體,函式返回後Block將無效;
  • NSMallocBlock:位於堆記憶體。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
BlkSum blk1 = ^ long (int a, int b) {
  return a + b;
};
NSLog(@"blk1 = %@", blk1);// blk1 = <__NSGlobalBlock__: 0x47d0>


int base = 100;
BlkSum blk2 = ^ long (int a, int b) {
  return base + a + b;
};
NSLog(@"blk2 = %@", blk2); // blk2 = <__NSStackBlock__: 0xbfffddf8>

BlkSum blk3 = [[blk2 copy] autorelease];
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
2
3
4
5
6
7
  int base = 100;
  base += 100;
  BlkSum sum = ^ long (int a, int b) {
    return base + a + b;
  };
  base++;
  printf("%ld",sum(1,2));

在Block內變數base是隻讀的,如果想在Block內改變base的值,在定義base時要用 __block修飾:__block int base = 100;

1
2
3
4
5
6
7
8
9
  __block int base = 100;
  base += 100;
  BlkSum sum = ^ long (int a, int b) {
    base += 10;
    return base + a + b;
  };
  base++;
  printf("%ld\n",sum(1,2));
  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
2
3
4
5
6
7
int base = 100;
BlkSum sum = ^ long (int a, int b) {
  // base++; 編譯錯誤,只讀
  return base + a + b;
};
base = 0;
printf("%ld\n",sum(1,2)); // 這裡輸出是103,而不是3
  • static變數、全域性變數。如果把上個例子的base改成全域性的、或static。Block就可以對他進行讀寫了。因為全域性變數或靜態變數在記憶體中的地址是固定的,Block在讀取該變數值的時候是直接從其所在記憶體讀出,獲取到的是最新值,而不是在定義時copy的常量。
1
2
3
4
5
6
7
8
9
static int base = 100;
BlkSum sum = ^ long (int a, int b) {
  base++;
  return base + a + b;
};
base = 0;
printf("%d\n", base);
printf("%ld\n",sum(1,2)); // 這裡輸出是3,而不是103
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void foo() {
  int base = 100;
  BlkSum blk = ^ long (int a, int b) {
    return  base + a + b;
  };
  NSLog(@"%@", blk); // <__NSStackBlock__: 0xbfffdb40>
  bar(blk);
}

void bar(BlkSum sum_blk) {
  NSLog(@"%@",sum_blk); // 與上面一樣,說明作為引數傳遞時,並不會發生copy

  void (^blk) (BlkSum) = ^ (BlkSum sum) {
    NSLog(@"%@",sum);     // 無論blk在堆上還是棧上,作為引數的Block不會發生copy。
    NSLog(@"%@",sum_blk); // 當blk copy到堆上時,sum_blk也被copy了一分到堆上上。
  };
  blk(sum_blk); // blk在棧上

  blk = [[blk copy] autorelease];
  blk(sum_blk); // blk在堆上
}
ObjC物件,不同於基本型別,Block會引起物件的引用計數變化。

先看下面程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@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物件的行為

1
2
3
4
5
6
@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就變成了孤島,誰也釋放不了誰。比如:

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

解決這個問題的辦法是使用弱引用打斷retain cycle:

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

request被持有者釋放後。request 的retainCount變成0,request被dealloc,request釋放持有的Block,導致Block的retainCount變成0,也被銷燬。這樣這兩個物件記憶體都被回收。

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

與上面情況類似的陷阱:

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

這裡self和myBlock迴圈引用,解決辦法同上:

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

self.myBlock = ^ {
  NSLog(@"%@", _someVer);
};

這裡在Block中雖然沒直接使用self,但使用了成員變數。在Block中使用成員變數,retain的不是這個變數,而會retain self。解決辦法也和上面一樣。

1
2
3
4
5
6
@property (nonatomic, retain) NSString* someVar;

__block MyClass* weakSelf = self;
self.myBlock = ^ {
  NSLog(@"%@", self.someVer);
};

或者

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

retain cycle不只發生在兩個物件之間,也可能發生在多個物件之間,這樣問題更復雜,更難發現

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

解決辦法同樣是用__block打破迴圈引用

1
2
3
4
5
6
7
ClassA* objA = [[[ClassA alloc] init] autorelease];

MyClass* weakSelf = self;
objA.myBlock = ^{
  [weakSelf doSomething];
};
self.objA = objA;

注意:MRC中__block是不會引起retain;但在ARC中__block則會引起retain。ARC中應該使用__weak__unsafe_unretained弱引用。__weak只能在iOS5以後使用。

Block使用物件被提前釋放

看下面例子,有這種情況,如果不只是request持有了Block,另一個物件也持有了Block。

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

這時如果request 被持有者釋放。

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

這時request已被完全釋放,但Block仍被objA持有,沒有釋放,如果這時觸發了Block,在Block中將訪問已經銷燬的request,這將導致程式crash。為了避免這種情況,開發者必須要注意物件和Block的生命週期。

另一個常見錯誤使用是,開發者擔心retain cycle錯誤的使用__block。比如

1
2
3
4
__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。

1
2
3
4
5
6
7
8
9
10
11
12
// 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

 Apr 19th, 2013


相關文章