本文只介紹了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其實包含兩個部分內容
-
Block執行的程式碼,這是在編譯的時候已經生成好的;
-
一個包含
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
。