Block的循環(huán)引用

本系列博文總結(jié)自《Pro Multithreading and Memory Management for iOS and OS X with ARC》

在上一篇文章中,我們講了很多關(guān)于 block 和基礎(chǔ)變量的內(nèi)存管理,接著我們聊聊 block 和對(duì)象的內(nèi)存管理,如 block 經(jīng)常會(huì)碰到的循環(huán)引用問(wèn)題等等。

獲取對(duì)象

照例先來(lái)段代碼輕松下,瞧瞧 block 是怎么獲取外部對(duì)象的

/********************** capturing objects **********************/

typedefvoid(^blk_t)(id obj);

blk_tblk;

-(void)viewDidLoad

{

[selfcaptureObject];

blk([[NSObjectalloc]init]);

blk([[NSObjectalloc]init]);

blk([[NSObjectalloc]init]);

}

-(void)captureObject

{

id array=[[NSMutableArrayalloc]init];

blk=[^(id obj){

[array addObject:obj];

NSLog(@"array count = %ld",[array count]);

}copy];

}

翻譯后的關(guān)鍵代碼摘錄如下

/* a struct for the Block and some functions */

struct__main_block_impl_0

{

struct__block_impl impl;

struct__main_block_desc_0*Desc;

id __strong array;

__main_block_impl_0(void*fp,struct__main_block_desc_0*desc,id __strong _array,intflags=0):array(_array)

{

impl.isa=&_NSConcreteStackBlock;

impl.Flags=flags;

impl.FuncPtr=fp;

Desc=desc;

}

};

staticvoid__main_block_func_0(struct__main_block_impl_0*__cself,id obj)

{

id __strong array=__cself->array;

[array addObject:obj];

NSLog(@"array count = %ld",[array count]);

}

staticvoid__main_block_copy_0(struct__main_block_impl_0*dst,__main_block_impl_0*src)

{

_Block_object_assign(&dst->array,src->array,BLOCK_FIELD_IS_OBJECT);

}

staticvoid__main_block_dispose_0(struct__main_block_impl_0*src)

{

_Block_object_dispose(src->array,BLOCK_FIELD_IS_OBJECT);

}

structstaticstruct__main_block_desc_0

{

unsignedlongreserved;

unsignedlongBlock_size;

void(*copy)(struct__main_block_impl_0*,struct__main_block_impl_0*);

void(*dispose)(struct__main_block_impl_0*);

}__main_block_desc_0_DATA={0,

sizeof(struct__main_block_impl_0),

__main_block_copy_0,

__main_block_dispose_0

};

/* Block literal and executing the Block */

blk_tblk;

{

id __strong array=[[NSMutableArrayalloc]init];

blk=&__main_block_impl_0(__main_block_func_0,

&__main_block_desc_0_DATA,

array,

0x22000000);

blk=[blk copy];

}

(*blk->impl.FuncPtr)(blk,[[NSObjectalloc]init]);

(*blk->impl.FuncPtr)(blk,[[NSObjectalloc]init]);

(*blk->impl.FuncPtr)(blk,[[NSObjectalloc]init]);

在本例中,當(dāng)變量變量作用域結(jié)束時(shí),array被廢棄,強(qiáng)引用失效,NSMutableArray類的實(shí)例對(duì)象會(huì)被釋放并廢棄。在這危難關(guān)頭,block 及時(shí)調(diào)用了copy方法,在_Block_object_assign中,將array賦值給 block 成員變量并持有。所以上面代碼可以正常運(yùn)行,打印出來(lái)的array count依次遞增。

總結(jié)代碼可正常運(yùn)行的原因關(guān)鍵就在于 block 通過(guò)調(diào)用copy方法,持有了 __strong 修飾的外部變量,使得外部對(duì)象在超出其作用域后得以繼續(xù)存活,代碼正常執(zhí)行。

在以下情形中, block 會(huì)從??截惖蕉眩?/p>

當(dāng) block 調(diào)用copy方法時(shí),如果 block 在棧上,會(huì)被拷貝到堆上;

當(dāng) block 作為函數(shù)返回值返回時(shí),編譯器自動(dòng)將 block 作為_(kāi)Block_copy函數(shù),效果等同于 block 直接調(diào)用copy方法;

當(dāng) block 被賦值給 __strong id 類型的對(duì)象或 block 的成員變量時(shí),編譯器自動(dòng)將 block 作為_(kāi)Block_copy函數(shù),效果等同于 block 直接調(diào)用copy方法;

當(dāng) block 作為參數(shù)被傳入方法名帶有usingBlock的 Cocoa Framework 方法或 GCD 的 API 時(shí)。這些方法會(huì)在內(nèi)部對(duì)傳遞進(jìn)來(lái)的 block 調(diào)用copy或_Block_copy進(jìn)行拷貝;

其實(shí)后三種情況在上篇文章block的自動(dòng)拷貝已經(jīng)做過(guò)說(shuō)明

除此之外,都需要手動(dòng)調(diào)用。

延伸閱讀:Objective-C 結(jié)構(gòu)體中的 __strong 成員變量

注意到__main_block_impl_0結(jié)構(gòu)體有什么異常沒(méi)?在 C 結(jié)構(gòu)體中出現(xiàn)了__strong關(guān)鍵字修飾的變量。

通常情況下, Objective-C 的編譯器因?yàn)闊o(wú)法檢測(cè) C 結(jié)構(gòu)體初始化和釋放的時(shí)間,不能進(jìn)行有效的內(nèi)存管理,所以 Objective-C 的 C 結(jié)構(gòu)體成員是不能用__strong、__weak等等這類關(guān)鍵字修飾。然而 runtime 庫(kù)是可以在運(yùn)行時(shí)檢測(cè)到 block 的內(nèi)存變化,如 block 何時(shí)從棧拷貝到堆,何時(shí)從堆上釋放等等,所以就會(huì)出現(xiàn)上述結(jié)構(gòu)體成員變量用__strong修飾的情況。

__block 變量和對(duì)象

__block 說(shuō)明符可以修飾任何類型的自動(dòng)變量。下面讓我們?cè)倏磦€(gè)小例子,啊,愉快的代碼時(shí)間又到啦。

/******* block 修飾對(duì)象 *******/

__block id obj=[[NSObjectalloc]init];

ARC 下,對(duì)象所有權(quán)修飾符默認(rèn)為_(kāi)_strong,即

__block id __strong obj=[[NSObjectalloc]init];

/******* block 修飾對(duì)象轉(zhuǎn)換后的代碼 *******/

/* struct for __block variable */

struct__Block_byref_obj_0

{

void*__isa;

__Block_byref_obj_0*__forwarding;

int__flags;

int__size;

void(*__Block_byref_id_object_copy)(void*,void*);

void(*__Block_byref_id_object_dispose)(void*);

__strong id obj;

};

staticvoid__Block_byref_id_object_copy_131(void*dst,void*src)

{

_Block_object_assign((char*)dst+40,*(void**)((char*)src+40),131);

}

staticvoid__Block_byref_id_object_dispose_131(void*src)

{

_Block_object_dispose(*(void**)((char*)src+40),131);

}

/* __block variable declaration */

__Block_byref_obj_0 obj={0,

&obj,

0x2000000,

sizeof(__Block_byref_obj_0),

__Block_byref_id_object_copy_131,

__Block_byref_id_object_dispose_131,

[[NSObjectalloc]init]

};

__block id __strong obj的作用和id __strong obj的作用十分類似。當(dāng)__block id __strong obj從棧上拷貝到堆上時(shí),_Block_object_assign被調(diào)用,block 持有obj;當(dāng)__block id __strong obj從堆上被廢棄時(shí),_Block_object_dispose被調(diào)用用以釋放此對(duì)象,block 引用消失。

所以,只要是堆上的__strong修飾符修飾的__block對(duì)象類型的變量,和 block 內(nèi)獲取到的__strong修飾符修飾的對(duì)象類型的變量,編譯器都能對(duì)它們的內(nèi)存進(jìn)行適當(dāng)?shù)墓芾怼?/p>

如果上面的__strong換成__weak,結(jié)果會(huì)怎樣呢?

/********************** capturing __weak objects **********************/

typedefvoid(^blk_t)(id obj);

blk_tblk;

-(void)viewDidLoad

{

[selfcaptureObject];

blk([[NSObjectalloc]init]);

blk([[NSObjectalloc]init]);

blk([[NSObjectalloc]init]);

}

-(void)captureObject

{

id array=[[NSMutableArrayalloc]init];

id __weak array2=array;

blk=[^(id obj){

[array2 addObject:obj];

NSLog(@"array2 count = %ld",[array2 count]);

}copy];

}

結(jié)果是:

array2 count=0

array2 count=0

array2 count=0

原因很簡(jiǎn)單,array2是弱引用,當(dāng)變量作用域結(jié)束,array所指向的對(duì)象內(nèi)存被釋放,array2指向 nil,向 nil 對(duì)象發(fā)送count消息就返回結(jié)果 0 了。

如果__weak再改成__unsafe_unretained呢?__unsafe_unretained修飾的對(duì)象變量指針就相當(dāng)于一個(gè)普通指針。使用這個(gè)修飾符有點(diǎn)需要注意的地方是,當(dāng)指針?biāo)赶虻膶?duì)象內(nèi)存被釋放時(shí),指針變量不會(huì)被置為 nil。所以當(dāng)使用這個(gè)修飾符時(shí),一定要注意不要通過(guò)懸掛指針(指向被廢棄內(nèi)存的指針)來(lái)訪問(wèn)已經(jīng)被廢棄的對(duì)象內(nèi)存,否則程序就會(huì)崩潰。

如果__unsafe_unretained再改成__autoreleasing會(huì)怎樣呢?會(huì)報(bào)錯(cuò),編譯器并不允許你這么干!如果你這么寫(xiě)

__block id __autoreleasing obj=[[NSObjectalloc]init];

編譯器就會(huì)報(bào)下面的錯(cuò)誤,意思就是__block和__autoreleasing不能同時(shí)使用。

error: __block variables cannot have __autoreleasing ownership __block id __autoreleasing obj = [[NSObject alloc] init];

循環(huán)引用

千辛萬(wàn)苦,重頭戲終于來(lái)了。block 如果使用不小心,就容易出現(xiàn)循環(huán)引用,導(dǎo)致內(nèi)存泄露。到底哪里泄露了呢?通過(guò)前面的學(xué)習(xí),各位童鞋應(yīng)該有個(gè)底了,下面就讓我們一起進(jìn)入這泄露地區(qū)瞧瞧,哪兒出了問(wèn)題!

愉快的代碼時(shí)間到

// ARC enabled

/************** MyObject Class **************/

typedefvoid(^blk_t)(void);

@interfaceMyObject:NSObject

{

blk_tblk_;

}

@end

@implementationMyObject

-(id)init

{

self=[superinit];

blk_=^{NSLog(@"self = %@",self);};

returnself;

}

-(void)dealloc

{

NSLog(@"dealloc");

}

@end

/************** main function **************/

intmain()

{

id myObject=[[MyObjectalloc]init];

NSLog(@"%@",myObject);

return0;

}

由于self是__strong修飾,在 ARC 下,當(dāng)編譯器自動(dòng)將代碼中的 block 從??截惖蕉褧r(shí),block 會(huì)強(qiáng)引用和持有self,而self恰好也強(qiáng)引用和持有了 block,就造成了傳說(shuō)中的循環(huán)引用。

由于循環(huán)引用的存在,造成在main()函數(shù)結(jié)束時(shí),內(nèi)存仍然無(wú)法釋放,即內(nèi)存泄露。編譯器也會(huì)給出警告信息

warning: capturing 'self' strongly in this block is likely to lead to a retain cycle [-Warc-retain-cycles]

blk_ = ^{NSLog(@"self = %@", self);};

note: Block will be retained by an object strongly retained by the captured object

blk_ = ^{NSLog(@"self = %@", self);};

為了避免這種情況發(fā)生,可以在變量聲明時(shí)用__weak修飾符修飾變量self,讓 block 不強(qiáng)引用self,從而破除循環(huán)。iOS4 和 Snow Leopard 由于對(duì) weak 的支持不夠完全,可以用__unsafe_unretained代替。

-(id)init

{

self=[superinit];

id __weak tmp=self;

blk_=^{NSLog(@"self = %@",tmp);};

returnself;

}

再看一個(gè)例子

@interfaceMyObject:NSObject

{

blk_tblk_;

id obj_;

}

@end

@implementationMyObject

-(id)init

{

self=[superinit];

blk_=^{NSLog(@"obj_ = %@",obj_);};

returnself;

}

...

...

@end

上面的例子中,雖然沒(méi)有直接使用 self,卻也存在循環(huán)引用的問(wèn)題。因?yàn)閷?duì)于編譯器來(lái)說(shuō),obj_就相當(dāng)于self->obj_,所以上面的代碼就會(huì)變成

blk_=^{NSLog(@"obj_ = %@",self->obj_);};

所以這個(gè)例子只要用__weak,在init方法里面加一行即可

id __weak obj=obj_;

破解循環(huán)引用還有一招,使用 __block 修飾對(duì)象,在 block 內(nèi)將對(duì)象置為 nil 即可,如下

typedefvoid(^blk_t)(void);

@interfaceMyObject:NSObject

{

blk_tblk_;

}

@end

@implementationMyObject

-(id)init

{

self=[superinit];

__block id tmp=self;

blk_=^{

NSLog(@"self = %@",tmp);

tmp=nil;

};

returnself;

}

-(void)execBlock

{

blk_();

}

-(void)dealloc

{

NSLog(@"dealloc");

}

@end

intmain()

{

idobject=[[MyObjectalloc]init];

[objectexecBlock];

return0;

}

這個(gè)例子挺有意思的,如果執(zhí)行execBlock方法,就沒(méi)有循環(huán)引用,如果不執(zhí)行就有循環(huán)引用,挺值得玩味的。一方面,使用 __block 挺危險(xiǎn)的,萬(wàn)一代碼中不執(zhí)行 block ,就造成了循環(huán)引用,而且編譯器還沒(méi)法檢查出來(lái);另一方面,使用 __block 可以讓我們通過(guò) __block 變量去控制對(duì)象的生命周期,而且有可能在一些非常老舊的 MRC 代碼中,由于不支持 __weak,我們可以使用此方法來(lái)代替 __unsafe_unretained,從而避免懸掛指針的問(wèn)題。

還有個(gè)值得一提的時(shí),在 MRC 下,使用 __block 說(shuō)明符也可以避免循環(huán)引用。因?yàn)楫?dāng) block 從棧拷貝到堆時(shí),__block 對(duì)象類型的變量不會(huì)被 retain,沒(méi)有 __block 說(shuō)明符的對(duì)象類型的變量則會(huì)被 retian。正是由于 __block 在 ARC 和 MRC 下的巨大差異,我們?cè)趯?xiě)代碼時(shí)一定要區(qū)分清楚到底是 ARC 還是 MRC。

盡管 ARC 已經(jīng)如此普及,我們可能已經(jīng)可以不用去管 MRC 的東西,但要有點(diǎn)一定要明白,ARC 和 MRC 都是基于引用計(jì)數(shù)的內(nèi)存管理,其本質(zhì)上是一個(gè)東西,只不過(guò) ARC 在編譯期自動(dòng)化的做了內(nèi)存引用計(jì)數(shù)的管理,使得系統(tǒng)可以在適當(dāng)?shù)臅r(shí)候保留內(nèi)存,適當(dāng)?shù)臅r(shí)候釋放內(nèi)存。

循環(huán)引用到此為止,東西并不多。如果明白了之前的知識(shí)點(diǎn),就會(huì)了解循環(huán)引用不過(guò)是前面知識(shí)點(diǎn)的自然延伸點(diǎn)罷了。

Copy 和 Release

在 ARC 下,有時(shí)需要手動(dòng)拷貝和釋放 block。在 MRC 下更是如此,可以直接用copy和release來(lái)拷貝和釋放

void(^blk_on_heap)(void)=[blk_on_stack copy];

[blk_on_heap release];

拷貝到堆后,就可以 用retain持有 block

[blk_on_heap retain];

然而如果 block 在棧上,使用retain是毫無(wú)效果的,因此推薦使用copy方法來(lái)持有 block。

block 是 C 語(yǔ)言的擴(kuò)展,所以可以在 C 中使用 block 的語(yǔ)法。比如,在上面的例子中,可以直接使用Block_copy和Block_release函數(shù)來(lái)代替copy和release方法

void(^blk_on_heap)(void)=Block_copy(blk_on_stack);

Block_release(blk_on_heap);

Block_copy的作用相當(dāng)于之前看到過(guò)的_Block_copy函數(shù),而且 Objective-C runtime 庫(kù)在運(yùn)行時(shí)拷貝 block 用的就是這個(gè)函數(shù)。同理,釋放 block 時(shí),runtime 調(diào)用了Block_release函數(shù)。

最后這里有一篇總結(jié) block 的文章的很不錯(cuò),推薦大家看看:http://tanqisen.github.io/blog/2013/04/19/gcd-block-cycle-retain/

最后編輯于
?著作權(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ù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Blocks Blocks Blocks 是帶有局部變量的匿名函數(shù) 截取自動(dòng)變量值 int main(){ ...
    南京小伙閱讀 1,080評(píng)論 1 3
  • Block基礎(chǔ)回顧 1.什么是Block? 帶有局部變量的匿名函數(shù)(名字不重要,知道怎么用就行),差不多就與C語(yǔ)言...
    Bugfix閱讀 6,923評(píng)論 5 61
  • block雖然好用,但是里面也有不少坑,最大的坑莫過(guò)于循環(huán)引用問(wèn)題。稍不注意,可能就會(huì)造成內(nèi)存泄漏。這節(jié),我將從源...
    CholMay閱讀 594評(píng)論 0 4
  • 前言 Blocks是C語(yǔ)言的擴(kuò)充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了這...
    小人不才閱讀 3,884評(píng)論 0 23
  • 《Objective-C高級(jí)編程》這本書(shū)就講了三個(gè)東西:自動(dòng)引用計(jì)數(shù)、block、GCD,偏向于從原理上對(duì)這些內(nèi)容...
    WeiHing閱讀 10,118評(píng)論 10 69

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