Block中的循環(huán)引用

在講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ú)效。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容