本文要將block的以下機制关顷,并配合具體代碼詳細描述:
- block 與 外部變量
- block 的存儲域:棧塊议双、堆塊、全局塊
定義
塊與函數(shù)類似汞舱,只不過是直接定義在另一個函數(shù)里宗雇,和定義它的那個函數(shù)共享同一個范圍內(nèi)的東西。
訪問外部變量
堆塊內(nèi)部泌神,棧是紅燈區(qū)舞虱,堆是綠燈區(qū)矾兜。
根據(jù)塊的存儲位置,可將塊分為全局塊浑槽、棧塊配并、堆塊。這里先主要針對堆塊講解畸冲。
Block不允許修改外部變量的值。Apple這樣設計算行,應該是考慮到了block的特殊性苫耸,block也屬于“函數(shù)”的范疇褪子,變量進入block,實際就是已經(jīng)改變了作用域呀枢。在幾個作用域之間進行切換時笼痛,如果不加上這樣的限制缨伊,變量的可維護性將大大降低。又比如我想在block內(nèi)聲明了一個與外部同名的變量枷恕,此時是允許呢還是不允許呢紧唱?只有加上了這樣的限制漏益,這樣的情景才能實現(xiàn)。于是棧區(qū)變成了紅燈區(qū)铜犬,堆區(qū)變成了綠燈區(qū)轻庆。
幾種演算
- block調(diào)用 基本數(shù)據(jù)類型
{
NSLog(@"\n--------------------block調(diào)用 基本數(shù)據(jù)類型---------------------\n");
int a = 10;
NSLog(@"block定義前a地址=%p", &a);
void (^aBlock)() = ^(){
NSLog(@"block定義內(nèi)部a地址=%p", &a);
};
NSLog(@"block定義后a地址=%p", &a);
aBlock();
}
/*
結果:
block定義前a地址=0x7fff5bdcea8c
block定義后a地址=0x7fff5bdcea8c
block定義內(nèi)部a地址=0x7fa87150b850
*/
/*
流程:
1. block定義前:a在棧區(qū)
2. block定義內(nèi)部:里面的a是根據(jù)外面的a拷貝到堆中的余爆,不是一個a
3. block定義后:a在棧區(qū)
*/
{
NSLog(@"\n--------------------block調(diào)用 __block修飾的基本數(shù)據(jù)類型---------------------\n");
__block int b = 10;
NSLog(@"block定義前b地址=%p", &b);
void (^bBlock)() = ^(){
b = 20;
NSLog(@"block定義內(nèi)部b地址=%p", &b);
};
NSLog(@"block定義后b地址=%p", &b);
NSLog(@"調(diào)用block前 b=%d", b);
bBlock();
NSLog(@"調(diào)用block后 b=%d", b);
}
/*
結果:
block定義前b地址=0x7fff5bdcea50
block定義后b地址=0x7fa873b016d8
調(diào)用block前 b=10
block定義內(nèi)部b地址=0x7fa873b016d8
調(diào)用block后 b=20
*/
/*
流程:
1. 聲明 b 為 __block (__block 所起到的作用就是只要觀察到該變量被 block 所持有蛾方,就將“外部變量”在棧中的內(nèi)存地址放到了堆中上陕。)
2. block定義前:b在棧中释簿。
3. block定義內(nèi)部: 將外面的b拷貝到堆中硼莽,并且使外面的b和里面的b是一個。
4. block定義后:外面的b和里面的b是一個偏螺。
5. block調(diào)用前:b的值還未被修改矾瑰。
6. block調(diào)用后:b的值在block內(nèi)部被修改殴穴。
*/
{
NSLog(@"\n--------------------block調(diào)用 指針---------------------\n");
NSString *c = @"ccc";
NSLog(@"block定義前:c=%@, c指向的地址=%p, c本身的地址=%p", c, c, &c);
void (^cBlock)() = ^{
NSLog(@"block定義內(nèi)部:c=%@, c指向的地址=%p, c本身的地址=%p", c, c, &c);
};
NSLog(@"block定義后:c=%@, c指向的地址=%p, c本身的地址=%p", c, c, &c);
cBlock();
NSLog(@"block調(diào)用后:c=%@, c指向的地址=%p, c本身的地址=%p", c, c, &c);
}
/*
c指針本身在block定義中和外面不是一個采幌,但是c指向的地址一直保持不變震桶。
1. block定義前:c指向的地址在堆中, c指針本身的地址在棧中磨取。
2. block定義內(nèi)部:c指向的地址在堆中柴墩, c指針本身的地址在堆中(c指針本身和外面的不是一個江咳,但是指向的地址和外面指向的地址是一樣的)。
3. block定義后:c不變爹土,c指向的地址在堆中胀茵, c指針本身的地址在棧中挟阻。
4. block調(diào)用后:c不變呵哨,c指向的地址在堆中孟害, c指針本身的地址在棧中挪拟。
*/
{
NSLog(@"\n--------------------block調(diào)用 指針并修改值---------------------\n");
NSMutableString *d = [NSMutableString stringWithFormat:@"ddd"];
NSLog(@"block定義前:d=%@, d指向的地址=%p, d本身的地址=%p", d, d, &d);
void (^dBlock)() = ^{
NSLog(@"block定義內(nèi)部:d=%@, d指向的地址=%p, d本身的地址=%p", d, d, &d);
d.string = @"dddddd";
};
NSLog(@"block定義后:d=%@, d指向的地址=%p, d本身的地址=%p", d, d, &d);
dBlock();
NSLog(@"block調(diào)用后:d=%@, d指向的地址=%p, d本身的地址=%p", d, d, &d);
}
/*
d指針本身在block定義中和外面不是一個玉组,但是d指向的地址一直保持不變。
在block調(diào)用后朝巫,d指向的堆中存儲的值發(fā)生了變化石景。
*/
{
NSLog(@"\n--------------------block調(diào)用 __block修飾的指針---------------------\n");
__block NSMutableString *e = [NSMutableString stringWithFormat:@"eee"];
NSLog(@"block定義前:e=%@, e指向的地址=%p, e本身的地址=%p", e, e, &e);
void (^eBlock)() = ^{
NSLog(@"block定義內(nèi)部:e=%@, e指向的地址=%p, e本身的地址=%p", e, e, &e);
e = [NSMutableString stringWithFormat:@"new-eeeeee"];
};
NSLog(@"block定義后:e=%@, e指向的地址=%p, e本身的地址=%p", e, e, &e);
eBlock();
NSLog(@"block調(diào)用后:e=%@, e指向的地址=%p, e本身的地址=%p", e, e, &e);
}
/*
從block定義內(nèi)部使用__block修飾的e指針開始潮孽,e指針本身的地址由棧中改變到堆中往史,即使出了block,也在堆中挨决。
在block調(diào)用后订歪,e在block內(nèi)部重新指向一個新對象,e指向的堆中的地址發(fā)生了變化陌粹。
*/
{
NSLog(@"\n--------------------block調(diào)用 retain cycle---------------------\n");
View *v = [[View alloc] init];
v.tag = 1;
v.frame = CGRectMake(100, 100, 100, 100);
[self.view addSubview:v]; //self->view->v
void (^block)() = ^{
v.backgroundColor = [UIColor orangeColor]; //定義內(nèi)部:block->v
};
v.block = block; //v->block
block();
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//預計3秒后釋放v對象。
[v removeFromSuperview];
});
}
/*
結果:
不會輸出 dealloc.
*/
/*
流程:
1. self->view->v
2. block定義內(nèi)部:block->v 因為block定義里面調(diào)用了v
3. v->block
結論:
引起循環(huán)引用的是block->v->block或舞,切斷其中一個線即可解決循環(huán)引用映凳,跟self->view->v這根線無關
*/
{
NSLog(@"\n--------------------block調(diào)用self---------------------\n");
View *v = [[View alloc] init];
v.tag = 2;
v.frame = CGRectMake(100, 220, 100, 100);
[self.view addSubview:v]; //self->view->v
void (^block)() = ^{
self.view.backgroundColor = [UIColor redColor]; //定義內(nèi)部:block->self
_count ++; //調(diào)用self的實例變量诈豌,也會讓block強引用self。
};
v.block = block; //v->block
block();
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//預計3秒后釋放self這個對象彤蔽。
AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
appDelegate.window.rootViewController = nil;
});
}
/*
結果:
不會輸出 dealloc.
*/
/*
流程:
1. self->view->v
2. v->block
3. block->self 因為block定義里面調(diào)用了self
結論:
在block內(nèi)引用實例變量庙洼,該實例變量會被block強引用油够。
引起循環(huán)引用的是self->view->v->block->self,切斷一個線即可解決循環(huán)引用揩悄。
*/
棧塊鬼悠、堆塊厦章、全局塊
塊本身也是對象,由isa指針、塊對象正常運轉所需的信息群发、捕獲到的變量
組成发乔。
根據(jù)Block創(chuàng)建的位置不同,Block有三種類型起愈,創(chuàng)建的Block對象分別會存儲到棧抬虽、堆纵菌、全局數(shù)據(jù)區(qū)域咱圆。
上面講了塊會把它所捕獲的所有變量都拷貝一份功氨,這些拷貝放在 descriptor 變量后面捷凄,捕獲了多少個變量围来,就要占據(jù)多少內(nèi)存空間。請注意钦铁,拷貝的并不是對象本身才漆,而是指向這些對象的指針變量醇滥。
1. 在全局數(shù)據(jù)區(qū)的Block對象
{
NSLog(@"\n--------------------block的存儲域 全局塊---------------------\n");
void (^blk)(void) = ^{
NSLog(@"Global Block");
};
blk();
NSLog(@"%@", [blk class]);
}
/*
結果:輸出 __NSGlobalBlock__
*/
/*
結論:
全局塊:這種塊不會捕捉任何狀態(tài)(外部的變量)鸳玩,運行時也無須有狀態(tài)來參與。塊所使用的整個內(nèi)存區(qū)域颓帝,在編譯期就已經(jīng)確定窝革。
全局塊一般聲明在全局作用域中虐译。但注意有種特殊情況,在函數(shù)棧上創(chuàng)建的block侮攀,如果沒有捕捉外部變量兰英,block的實例還是會被設置在程序的全局數(shù)據(jù)區(qū)蚪腐,而非棧上。
*/
2. 在堆上創(chuàng)建的Block對象
{
NSLog(@"\n--------------------block的存儲域 堆塊---------------------\n");
int i = 1;
void (^blk)(void) = ^{
NSLog(@"Malloc Block, %d", i);
};
blk();
NSLog(@"%@", [blk class]);
}
/*
結果:輸出 __NSMallocBlock__
*/
/*
結論:
堆塊:解決塊在棧上會被覆寫的問題家制,可以給塊對象發(fā)送copy消息將它拷貝到堆上颤殴。復制到堆上后,塊就成了帶引用計數(shù)的對象了杈绸。
在ARC中瞳脓,以下幾種情況棧上的Block會自動復制到堆上:
- 調(diào)用Block的copy方法
- 將Block作為函數(shù)返回值時(MRC時此條無效澈侠,需手動調(diào)用copy)
- 將Block賦值給__strong修飾的變量時(MRC時此條無效)
- 向Cocoa框架含有usingBlock的方法或者GCD的API傳遞Block參數(shù)時
上述代碼就是在ARC中哨啃,block賦值給__strong修飾的變量,并且捕獲了外部變量审姓,block就會自動復制到堆上祝峻。
*/
3. 在棧上創(chuàng)建的Block對象
{
NSLog(@"\n--------------------block的存儲域 棧塊---------------------\n");
int i = 1;
__weak void (^blk)(void) = ^{
NSLog(@"Stack Block, %d", i);
};
blk();
NSLog(@"%@", [blk class]);
}
/*
結果:輸出 __NSStackBlock__
*/
/*
結論:
棧塊:塊所占內(nèi)存區(qū)域分配在棧中莱找,編譯器有可能把分配給塊的內(nèi)存覆寫掉宋距。
在ARC中症脂,除了上面四種情況诱篷,并且不在global上,block是在棧中闸盔。
*/
內(nèi)存泄漏
堆塊訪問外部變量時會拷貝一份指針到堆中琳省,相當于強引用了指針所指的值。如果該對象又直接或間接引用了塊击费,就出現(xiàn)了循環(huán)引用蔫巩。
解決方法:要么在捕獲時使用__weak解除引用,要么在執(zhí)行完后置nil解除引用(使用后置nil的方式垃瞧,如果未執(zhí)行个从,則仍會內(nèi)存泄漏)截粗。
- 注意:使用__block并不能解決循環(huán)引用問題绸罗。
優(yōu)缺點
優(yōu)點:
- 捕獲外部變量
- 降低代碼分散程度
缺點:
- 循環(huán)引用引起內(nèi)存泄露
總結
在block內(nèi)部意推,棧是紅燈區(qū),堆是綠燈區(qū)珊蟀。
在block內(nèi)部使用的是將外部變量的拷貝到堆中的(基本數(shù)據(jù)類型直接拷貝一份到堆中菊值,對象類型只將在棧中的指針拷貝到堆中并且指針所指向的地址不變。)
__block修飾符的作用:是將block中用到的變量育灸,拷貝到堆中腻窒,并且外部的變量本身地址也改變到堆中。
循環(huán)引用:分析實際的引用關系磅崭,block中直接引用self也不一定會造成循環(huán)引用儿子。
__block不能解決循環(huán)引用,需要在block執(zhí)行尾部將變量設置成nil(但問題很多砸喻,比如block永遠不執(zhí)行,外面變量變了里面也變割岛,里面變了外面也變等問題)
__weak可以解決循環(huán)引用愉适,block在捕獲weakObj時,會對weakObj指向的對象進行弱引用癣漆。
使用__weak時维咸,可在block開始用局部__strong變量持有,以免block執(zhí)行期間對象被釋放。
塊的存儲域:全局塊癌蓖、棧塊瞬哼、堆塊
全局塊不引用外部變量,所以不用考慮费坊。
堆塊引用的外部變量倒槐,不是原始的外部變量,是拷貝到堆中的副本附井。
棧塊本身就在棧中讨越,引用外部變量不會拷貝到堆中。