關(guān)于block的存儲域
一、 block變量存儲域
1. ARC和MRC不同的存儲情況
通過對block本質(zhì)的探究,發(fā)現(xiàn)block內(nèi)部也是有一個isa指針指向它所屬的類,所以block其實也是一個對象。
// ARC 環(huán)境下
void (^blk)() = ^{
};
NSLog(@"%@",[blk class]);
其打印結(jié)果為 " _ _NSGlobalBlock _ _ ",可想而知block是屬于一個叫NSGlobalBlock類的對象。其實 block的類有三種。分別為 NSGlobalBlock ,NSStackBlock ,NSMallocBlock。從它們的名字能猜測到,這三種類型的block對應(yīng)著block不同的存儲區(qū)域。相繼為 global(全局?jǐn)?shù)據(jù)區(qū)域) stack (棧區(qū)) malloc(堆區(qū))。
分別在ARC和MRC的情況下看看哪些情況下對應(yīng)的block存儲區(qū)域,首先是ARC的情況下
NSLog(@"未賦值 未引用外部變量 %@", [^void() {
} class]);
void (^blk)() = ^{
};
NSLog(@"賦值 未引用外部變量 %@",[blk class]);
int a = 1;
void (^blk1)() = ^{
a;
};
NSLog(@"賦值 引用外部變量 %@",[blk1 class]);
NSLog(@"未賦值 引用外部變量 %@", [^void() {
a;
} class]);
最后的輸出結(jié)果為:
2017-01-08 21:49:15.389 block存儲域[10956:1104789] 未賦值 未引用外部變量 __NSGlobalBlock__
2017-01-08 21:49:15.389 block存儲域[10956:1104789] 賦值 未引用外部變量 __NSGlobalBlock__
2017-01-08 21:49:15.389 block存儲域[10956:1104789] 賦值 引用外部變量 __NSMallocBlock__
2017-01-08 21:49:15.390 block存儲域[10956:1104789] 未賦值 引用外部變量 __NSStackBlock__
可以對ARC的情況下做一個總結(jié):
- NSGlobalBlock === 在block未引用外部變量的時候
- NSMallocBlock === 在block引用了外部變量的時候,且將block賦值給了block類型的變量
- NSStackBlock === 在block引用了外部變量的時候, 沒有將block賦值給block類型的變量時。
接下來是對在MRC的情況下,block的不同存儲區(qū)域
NSLog(@"未賦值 未引用外部變量 %@", [^void() {
} class]);
void (^blk)() = ^{
};
NSLog(@"賦值 未引用外部變量 %@",[blk class]);
int a = 1;
void (^blk1)() = ^{
a;
};
NSLog(@"賦值 引用外部變量 %@",[blk1 class]);
NSLog(@"未賦值 引用外部變量 %@", [^void() {
a;
} class]);
輸出結(jié)果為:
2017-01-08 21:56:21.789 block存儲域[10994:1108230] 未賦值 未引用外部變量 __NSGlobalBlock__
2017-01-08 21:56:21.790 block存儲域[10994:1108230] 賦值 未引用外部變量 __NSGlobalBlock__
2017-01-08 21:56:21.790 block存儲域[10994:1108230] 賦值 引用外部變量 __NSStackBlock__
2017-01-08 21:56:21.791 block存儲域[10994:1108230] 未賦值 引用外部變量 __NSStackBlock__
我們可以看到無論是否引用外部變量和是否賦值給了block類型的變量,都沒有出現(xiàn)mallocBlock,那么MRC的情況下如何才會生成mallocBlock呢。這時我們需要調(diào)用對象的copy方法。
void (^blk)() = ^{
};
NSLog(@"copy 未引用外部變量 %@",[[blk copy] class]);
int a = 1;
void (^blk1)() = ^{
a;
};
NSLog(@"copy 引用外部變量 %@",[[blk1 copy] class]);
2. ARC情況下什么時候會進(jìn)行copy
block引用了外部變量就會存儲在棧區(qū),在ARC環(huán)境下大多數(shù)情況會自動copy操作后復(fù)制到堆區(qū)。但是有些情況下編譯器不會自動進(jìn)行copy操作:
-
block作為函數(shù)參數(shù)的時候不會自動進(jìn)行copy操作,除了以下情況之外:
(1) Cocoa框架的方法切方法名中又usingBlock。
(2) GCD的API。
通過以下代碼驗證:
- (void)viewDidLoad {
[super viewDidLoad];
NSArray *tempArray = [self getBlockArray];
blk_t blk = tempArray[0];
blk();
}
- (id)getBlockArray {
int a = 10;
return [[NSArray alloc] initWithObjects:^{
NSLog(@"blk1 %d",a);
},^{
NSLog(@"blk2 %d",a);
}, nil];
}
因為作為參數(shù)傳遞的block的存儲區(qū)域為棧區(qū),而出了 getBlockArray 函數(shù)后,block的作用域結(jié)束了,當(dāng)調(diào)用函數(shù)取得數(shù)組時,訪問已經(jīng)釋放了的block,就會造成了crash。要是想正確訪問block ,可以做以下修改,對作為參數(shù)傳遞的block進(jìn)行copy操作。
- (id)getBlockArray {
int a = 10;
return [[NSArray alloc] initWithObjects:[^{
NSLog(@"blk1 %d",a);
} copy],[^{
NSLog(@"blk2 %d",a);
}copy], nil];
}
二、 __block變量存儲域
我們知道對于block內(nèi)部使用的外部變量,不允許在block內(nèi)部修改外部變量的值。以下代碼是編譯不過的。
int a = 10;
void (^blk)() = ^(){
a = 20;
} ;
blk();
因為在block進(jìn)行copy的同時,會將block內(nèi)使用的外部變量也進(jìn)行一次copy操作,如果這個變量是在棧區(qū)的,則會被拷貝到堆區(qū),并被block持有。而如果之前這個變量是在堆區(qū)的則不會有影響,只是被block持有。
int a = 10;
NSLog(@"定義block前 %p",&a);
void (^blk)() = ^(){
NSLog(@"block內(nèi)部 %p",&a);
} ;
blk();
NSLog(@"定義block后 %p",&a);
打印結(jié)果為:
2017-02-08 14:20:06.716 block存儲域_test[23117:147534] 定義block前 0x7fff5b635a4c
2017-02-08 14:20:06.717 block存儲域_test[23117:147534] block內(nèi)部 0x60800005a1b0
2017-02-08 14:20:06.717 block存儲域_test[23117:147534] 定義block后 0x7fff5b635a4c
16進(jìn)制轉(zhuǎn)換為10進(jìn)制,block外部為 140734726625868 ,block內(nèi)部使用的時候為 106102872449456,2者相差1532868764個字節(jié),轉(zhuǎn)換為1461 mb。因為堆地址遠(yuǎn)小于棧中的地址,又因為iOS中一個進(jìn)程的棧區(qū)內(nèi)存只有 1mb,所以在block內(nèi)部 的變量已經(jīng)在堆區(qū)內(nèi)了。
__block 變量
對于block內(nèi)部使用 _ _block變量時,和普通的變量一樣,如果這個變量是在棧區(qū)的,則會被拷貝到堆區(qū),并被block持有。而如果之前這個變量是在堆區(qū)的則不會有影響,只是被block持有。但是為什么能在block內(nèi)部修改 _ _block修飾的變量。
__block int a = 10;
NSLog(@"定義block前 %p",&a);
void (^blk)() = ^(){
a++;
NSLog(@"block內(nèi)部 %p",&a);
} ;
NSLog(@"定義block后 %p",&a);
blk();
打印的結(jié)果為:
2017-02-08 14:44:01.933 block存儲域_test[26207:167214] 定義block前 0x7fff569eca48
2017-02-08 14:44:01.934 block存儲域_test[26207:167214] 定義block后 0x60800003eb38
2017-02-08 14:44:01.934 block存儲域_test[26207:167214] block內(nèi)部 0x60800003eb38
與普通的外部變量不同的是,定義block后,變量的地址也指向了堆中的那個地址。而不是定義之前的棧中地址。還記得__block內(nèi)部那個指向 變量自身的那個forwading指針嗎,當(dāng) _ _block變量賦值到堆區(qū)時,棧區(qū)的變量的forwading指針也改為了指向堆中的那個變量的地址,所以在block外部使用變量時,它訪問的是堆中的那個變量
a++ <===> a._forwarding ->a)++
三、 截獲的對象存儲域
通常的一個變量,如下,當(dāng)它超過了作用域之后,就會被釋放了。
- (void)test5 {
NSMutableArray *tempArray = [NSMutableArray array];
}
但是對于block中使用的外部變量,在某些情況下,卻能超過變量的作用域存在。
(該代碼 是在ARC環(huán)境下運(yùn)行)
blk_t globalblk;
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
globalblk();
}
- (void)test5 {
NSMutableArray *tempArray = [NSMutableArray array];
globalblk = ^{
id obj = [[NSObject alloc] init];
[tempArray addObject:obj];
NSLog(@"count == %ld a == %d", tempArray.count,a);
};
}
該代碼中,tempArray變量出了 test5這個函數(shù),就超出了作用域,但是該代碼卻能正確的打印出來。說明: 在ARC環(huán)境下,當(dāng)給block賦值給一個block變量時,將會默認(rèn)的對block進(jìn)行一次copy操作。如果在MRC環(huán)境下,也就是沒有對block進(jìn)行手動copy的話,這段代碼就會crash。
四、 __block和對象
關(guān)于__block修飾的對象,被block使用的時候,在ARC和MRC情況下,是有很大的不同的。當(dāng)在ARC環(huán)境下,block內(nèi)部使用 _ _block修飾的對象的時候,它和正常的外部變量一樣,當(dāng)block從棧區(qū)copy到堆區(qū)時,會被block所持有,因此能超出變量總用域存在。而在MRC的情況下,當(dāng) _ _block修飾的變量被block使用時,不會隨著block從棧區(qū)copy到堆區(qū)而被block所持有。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
globalblk();
}
- (void)test5 {
__block NSMutableArray *tempArray = [NSMutableArray array];
globalblk = [^{
id obj = [[NSObject alloc] init];
[tempArray addObject:obj];
NSLog(@"count == %ld", [tempArray count]);
} copy];
}
以上的代碼,在ARC環(huán)境下能正常運(yùn)行,在MRC情況下會因為野指針的訪問而crash。
五、 block的循環(huán)引用
block持有_ _strong修飾的外部變量時,block會持有該對象,而如果該對象又持有block時,就會造成循環(huán)引用的問題。
typedef void (^blk_t)();
@interface Person ()
{
blk_t blk;
}
@end
@implementation Person
- (instancetype)init {
if (self = [super init]) {
blk = [^(){
NSLog(@"%@",self);
} copy];
}
return self;
}
- (void)dealloc {
NSLog(@"dealloc");
}
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc] init];
}
該段代碼的dealloc一定不會被打印,因為在調(diào)用 Person的init函數(shù)是,成員變量blk使用了 self,使得blk對 self 持有。而blk作為 Person對象的成員變量,Person對象就對 blk持有。造成了這種互相持有的關(guān)系后,該對象就無法釋放了。

解決方法(1): 通過對block內(nèi)部使用的變量,改為weak修飾。
- (instancetype)init {
if (self = [super init]) {
__weak typeof(self) weakSelf = self;
blk = [^(){
NSLog(@"%@",weakSelf);
} copy];
}
return self;
}
解決方法(2): 之前說過,在MRC環(huán)境下,用_ _block修飾變量時,block不會對該變量進(jìn)行持有。
//僅對MRC環(huán)境下有效
- (instancetype)init {
if (self = [super init]) {
__block tempSelf = self;
blk = [^(){
NSLog(@"%@",tempSelf);
} copy];
}
return self;
}