在講block的循環(huán)引用問(wèn)題之前,我們需要先了解一下iOS的內(nèi)存管理機(jī)制和block的基本知識(shí)
iOS的內(nèi)存管理機(jī)制
Objective-C在iOS中不支持GC(垃圾回收)機(jī)制,而是采用的引用計(jì)數(shù)的方式管理內(nèi)存。
引用計(jì)數(shù)(Reference Count)
在引用計(jì)數(shù)中,每一個(gè)對(duì)象負(fù)責(zé)維護(hù)對(duì)象所有引用的計(jì)數(shù)值。當(dāng)一個(gè)新的引用指向?qū)ο髸r(shí),引用計(jì)數(shù)器就遞增,當(dāng)去掉一個(gè)引用時(shí),引用計(jì)數(shù)就遞減。當(dāng)引用計(jì)數(shù)到零時(shí),該對(duì)象就將釋放占有的資源。
我們通過(guò)開(kāi)關(guān)房間的燈為例來(lái)?說(shuō)明引用計(jì)數(shù)機(jī)制。

引用《Pro Multithreading and Memory Management for iOS and OS X》中的圖片
圖中,“需要照明的人數(shù)”即對(duì)應(yīng)我們要說(shuō)的引用計(jì)數(shù)值。
第一個(gè)人進(jìn)入辦公室,“需要照明的人數(shù)”加1,計(jì)數(shù)值從0變?yōu)?,因此需要開(kāi)燈;
之后每當(dāng)有人進(jìn)入辦公室,“需要照明的人數(shù)”就加1。如計(jì)數(shù)值從1變成2;
每當(dāng)有人下班離開(kāi)辦公室,“需要照明的人數(shù)”加減1如計(jì)數(shù)值從2變成1;
最后一個(gè)人下班離開(kāi)辦公室時(shí),“需要照明的人數(shù)”減1。計(jì)數(shù)值從1變成0,因此需要關(guān)燈。
在Objective-C中,”對(duì)象“相當(dāng)于辦公室的照明設(shè)備,”對(duì)象的使用環(huán)境“相當(dāng)于進(jìn)入辦公室的人。上班進(jìn)入辦公室的人對(duì)辦公室照明設(shè)備發(fā)出的動(dòng)作,與Objective-C中的對(duì)應(yīng)關(guān)系如下表
對(duì)照明設(shè)備所做的動(dòng)作對(duì)Objective-C對(duì)象所做的動(dòng)作
開(kāi)燈生成對(duì)象
需要照明持有對(duì)象
不需要照明釋放對(duì)象
關(guān)燈廢棄對(duì)象
使用計(jì)數(shù)功能計(jì)算需要照明的人數(shù),使辦公室的照明得到了很好的管理。同樣,使用引用計(jì)數(shù)功能,對(duì)象也就能得到很好的管理,這就是Objective-C內(nèi)存管理,如下圖所示

引用《Pro Multithreading and Memory Management for iOS and OS X》中的圖片
MRC(Manual Reference Counting)中引起應(yīng)用計(jì)數(shù)變化的方法
Objective-C對(duì)象方法說(shuō)明
alloc/new/copy/mutableCopy創(chuàng)建對(duì)象,引用計(jì)數(shù)加1
retain引用計(jì)數(shù)加1
release引用計(jì)數(shù)減1
dealloc當(dāng)引用計(jì)數(shù)為0時(shí)調(diào)用
[NSArray array]引用計(jì)數(shù)不增加,由自動(dòng)釋放池管理
[NSDictionary dictionary]引用計(jì)數(shù)不增加,由自動(dòng)釋放池管理
自動(dòng)釋放池
關(guān)于自動(dòng)釋放,不是本文的重點(diǎn),這里就不講了。
ARC(Automatic Reference Counting)中內(nèi)存管理
Objective-C對(duì)象所有權(quán)修飾符說(shuō)明
__strong對(duì)象默認(rèn)修飾符,對(duì)象強(qiáng)引用,在對(duì)象超出作用域時(shí)失效。其實(shí)就相當(dāng)于retain操作,超出作用域時(shí)執(zhí)行release操作
__weak弱引用,不持有對(duì)象,對(duì)象釋放時(shí)會(huì)將對(duì)象置nil。
__unsafe_unretained弱引用,不持有對(duì)象,對(duì)象釋放時(shí)不會(huì)將對(duì)象置nil。
__autoreleasing自動(dòng)釋放,由自動(dòng)釋放池管理對(duì)象
block的基本知識(shí)
block的基本知識(shí)這里就不細(xì)說(shuō)了。
循環(huán)引用問(wèn)題
兩個(gè)對(duì)象相互持有,這樣就會(huì)造成循環(huán)引用,如下圖所示

兩個(gè)對(duì)象相互持有
圖中,對(duì)象A持有對(duì)象B,對(duì)象B持有對(duì)象A,相互持有,最終導(dǎo)致兩個(gè)對(duì)象都不能釋放。
block中循環(huán)引用問(wèn)題
由于block會(huì)對(duì)block中的對(duì)象進(jìn)行持有操作,就相當(dāng)于持有了其中的對(duì)象,而如果此時(shí)block中的對(duì)象又持有了該block,則會(huì)造成循環(huán)引用。如下,
typedef void(^block)();
@property (copy, nonatomic) block myBlock;
@property (copy, nonatomic) NSString *blockString;
- (void)testBlock {
self.myBlock = ^() {
//其實(shí)注釋中的代碼,同樣會(huì)造成循環(huán)引用
NSString *localString = self.blockString;
//NSString *localString = _blockString;
//[self doSomething];
};
}
注:以下調(diào)用注釋掉的代碼同樣會(huì)造成循環(huán)引用,因?yàn)椴还苁峭ㄟ^(guò)self.blockString還是_blockString,或是函數(shù)調(diào)用[self doSomething],因?yàn)橹灰?block中用到了對(duì)象的屬性或者函數(shù),block就會(huì)持有該對(duì)象而不是該對(duì)象中的某個(gè)屬性或者函數(shù)。
當(dāng)有someObj持有self對(duì)象,此時(shí)的關(guān)系圖如下。

當(dāng)someObj對(duì)象release self對(duì)象時(shí),self和myblock相互引用,retainCount都為1,造成循環(huán)引用

解決方法:
__weak typeof(self) weakSelf = self;
self.myBlock = ^() {
NSString *localString = weakSelf.blockString;
};
使用__weak修飾self,使其在block中不被持有,打破循環(huán)引用。開(kāi)始狀態(tài)如下

當(dāng)someObj對(duì)象釋放self對(duì)象時(shí),Self的retainCount為0,走dealloc,釋放myBlock對(duì)象,使其retainCount也為0。

其實(shí)以上循環(huán)引用的情況很容易發(fā)現(xiàn),因?yàn)榇藭r(shí)Xcode就會(huì)報(bào)警告。而發(fā)生在多個(gè)對(duì)象間的時(shí)候,Xcode就檢測(cè)不出來(lái)了,這往往就容易被忽略。
//ClassB
@interface ClassB : NSObject
@property (strong, nonatomic) ClassA *objA;
- (void)doSomething;
@end
//ClassA
@property (strong, nonatomic) ClassB *objB;
@property (copy, nonatomic) block myBlock;
- (void)testBlockRetainCycle {
ClassB* objB = [[ClassB alloc] init];
self.myBlock = ^() {
[objB doSomething];
};
objB.objA = self;
}

解決方法:
- (void)testBlockRetainCycle {
ClassB* objB = [[ClassB alloc] init];
__weak typeof(objB) weakObjB = objB;
self.myBlock = ^() {
[weakObjB doSomething];
};
objB.objA = self;
}
將objA對(duì)象weak,使其不在block中被持有
注:以上使用__weak打破循環(huán)的方法只在ARC下才有效,在MRC下應(yīng)該使用__block
或者,在block執(zhí)行完后,將block置nil,這樣也可以打破循環(huán)引用
- (void)testBlockRetainCycle {
ClassB* objB = [[ClassB alloc] init];
self.myBlock = ^() {
[objB doSomething];
};
objA.objA = self;
self.myBlock();
self.myBlock = nil;
}
這樣做的缺點(diǎn)是,block只會(huì)執(zhí)行一次,因?yàn)閎lock被置nil了,要再次使用的話,需要重新賦值。
一些不會(huì)造成循環(huán)引用的block
在開(kāi)發(fā)工程中,發(fā)現(xiàn)一些同學(xué)并沒(méi)有完全理解循環(huán)引用,以為只要有block的地方就會(huì)要用__weak來(lái)修飾對(duì)象,這樣完全沒(méi)有必要,以下幾種block是不會(huì)造成循環(huán)引用的。
大部分GCD方法
dispatch_async(dispatch_get_main_queue(), ^{
[self doSomething];
});
因?yàn)閟elf并沒(méi)有對(duì)GCD的block進(jìn)行持有,沒(méi)有形成循環(huán)引用。目前我還沒(méi)碰到使用GCD導(dǎo)致循環(huán)引用的場(chǎng)景,如果某種場(chǎng)景self對(duì)GCD的block進(jìn)行了持有,則才有可能造成循環(huán)引用。
block并不是屬性值,而是臨時(shí)變量
- (void)doSomething {
[self testWithBlock:^{
[self test];
}];
}
- (void)testWithBlock:(void(^)())block {
block();
}
- (void)test {
NSLog(@"test");
}
這里因?yàn)閎lock只是一個(gè)臨時(shí)變量,self并沒(méi)有對(duì)其持有,所以沒(méi)有造成循環(huán)引用
block使用對(duì)象被提前釋放
看下面例子,有這種情況,如果不只是ClassA持有了myBlock,ClassB也持有了myBlock。

當(dāng)ClassA被someObj對(duì)象釋放后

此時(shí),ClassA對(duì)象已經(jīng)被釋放,而myBlock還是被ClassB持有,沒(méi)有釋放;如果myBlock這個(gè)時(shí)被調(diào)度,而此時(shí)ClassA已經(jīng)被釋放,此時(shí)訪問(wèn)的ClassA將是一個(gè)nil對(duì)象(使用__weak修飾,對(duì)象釋放時(shí)會(huì)置為nil),而引發(fā)錯(cuò)誤。
另一個(gè)常見(jiàn)錯(cuò)誤使用是,開(kāi)發(fā)者擔(dān)心循環(huán)引用錯(cuò)誤(如上所述不會(huì)出現(xiàn)循環(huán)引用的情況),使用__weak。比如
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf doSomething];
});
因?yàn)閷lock作為參數(shù)傳給dispatch_async時(shí),系統(tǒng)會(huì)將block拷貝到堆上,而且block會(huì)持有block中用到的對(duì)象,因?yàn)閐ispatch_async并不知道block中對(duì)象會(huì)在什么時(shí)候被釋放,為了確保系統(tǒng)調(diào)度執(zhí)行block中的任務(wù)時(shí)其對(duì)象沒(méi)有被意外釋放掉,dispatch_async必須自己retain一次對(duì)象(即self),任務(wù)完成后再release對(duì)象(即self)。但這里使用__weak,使dispatch_async沒(méi)有增加self的引用計(jì)數(shù),這使得在系統(tǒng)在調(diào)度執(zhí)行block之前,self可能已被銷(xiāo)毀,但系統(tǒng)并不知道這個(gè)情況,導(dǎo)致block執(zhí)行時(shí)訪問(wèn)已經(jīng)被釋放的self,而達(dá)不到預(yù)期的結(jié)果。
注:如果是在MRC模式下,使用__block修飾self,則此時(shí)block訪問(wèn)被釋放的self,則會(huì)導(dǎo)致crash。
該場(chǎng)景下的代碼
// ClassA.m
- (void)test {
__weak MyClass* weakSelf = self;
double delayInSeconds = 10.0f;
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);
});
}
// ClassB.m
- (void)doSomething {
NSLog(@"do something");
ClassA *objA = [[ClassA alloc] init];
[objA test];
}
運(yùn)行結(jié)果
[5988:435396] do something
[5988:435396] self:(null)
解決方法:
對(duì)于這種場(chǎng)景,就不應(yīng)該使用__weak來(lái)修飾對(duì)象,讓dispatch_after對(duì)self進(jìn)行持有,保證block執(zhí)行時(shí)self還未被釋放。
block執(zhí)行過(guò)程中對(duì)象被釋放
還有一種場(chǎng)景,在block執(zhí)行開(kāi)始時(shí)self對(duì)象還未被釋放,而執(zhí)行過(guò)程中,self被釋放了,此時(shí)訪問(wèn)self時(shí),就會(huì)發(fā)生錯(cuò)誤。
對(duì)于這種場(chǎng)景,應(yīng)該在block中對(duì) 對(duì)象使用__strong修飾,使得在block期間對(duì) 對(duì)象持有,block執(zhí)行結(jié)束后,解除其持有。
- (void)testBlockRetainCycle {
ClassA* objA = [[ClassA alloc] init];
__weak typeof(objA) weakObjA = objA;
self.myBlock = ^() {
__strong typeof(weakObjA) strongWeakObjA = weakObjA;
[strongWeakObjA doSomething];
};
objA.objA = self;
}
注:此方法只能保證在block執(zhí)行期間對(duì)象不被釋放,如果對(duì)象在block執(zhí)行執(zhí)行之前已經(jīng)被釋放了,該方法也無(wú)效。