說明:閱讀本文,請參照之前的block文章加以理解; 一、迴圈引用的本質 //代碼——ARC環境 //列印 分析:main函數日誌輸出之前,Person實例對象就被銷毀了——因為在test1()方法中,強指針per持有[[Person alloc] init]對象會執行retain操作導致Perso ...
說明:閱讀本文,請參照之前的block文章加以理解;
一、迴圈引用的本質
//代碼——ARC環境
void test1() { Person *per = [[Person alloc] init]; per.age = 10; per.block = ^{ NSLog(@"-------1"); }; }
int main(int argc, const char * argv[]) { @autoreleasepool { test1(); // test2(); } NSLog(@"----"); return 0; }
#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN typedef void(^MyBlock)(void); @interface Person : NSObject @property (nonatomic, assign) int age; @property (nonatomic, copy) MyBlock block; @end NS_ASSUME_NONNULL_END #import "Person.h" @implementation Person - (void)dealloc { // [super dealloc]; NSLog(@"%s", __func__); } @end
//列印
2019-01-17 16:46:28.353740+0800 MJ_TEST[2990:240693] -[Person dealloc] 2019-01-17 16:46:28.354013+0800 MJ_TEST[2990:240693] ---- Program ended with exit code: 0
分析:main函數日誌輸出之前,Person實例對象就被銷毀了——因為在test1()方法中,強指針per持有[[Person alloc] init]對象會執行retain操作導致Person實例對象的retainCount值為2(此前alloc操作,其retainCount值就設置為1),當test1()方法結束時,per被存放在棧區也隨之銷毀,故per不會再持有Person實例對象即執行release操作導致該對象的retainCount指減1;當自動銷毀池autoreleasepool結束時,會自動向池中的所有對象再次發送一條release消息,那麼此時Person實例對象的retainCount值再次減1變成0,對象的引用計數一旦為0,其所占記憶體會被自動回收,因此Person實例對象就會銷毀;
補充:我們知道blcok的記憶體管理模式為copy策略(原因就不分析了),因為在ARC環境下強指針持有block對象,系統會自動將block對象copy到堆區中,所以ARC模式下,系統會自動幫助我們對block進行copy的管理策略,我們寫成strong的策略是沒有任何問題的——但是,MRC模式下必須是copy策略,系統不會幫你管理記憶體,只能手動;這點請註意!
至此,以上Person實例對象銷毀是正常的,那麼什麼情況下是不正常的?往下看:
//代碼
void test2() { Person *per = [[Person alloc] init]; per.age = 10; per.block = ^{ NSLog(@"-------%d", per.age); }; }
//列印
2019-01-17 15:00:31.859710+0800 MJ_TEST[2486:187534] ----- Program ended with exit code: 0
//clang
main.cpp
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; Person *__strong per; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__strong _per, int flags=0) : per(_per) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
Person.cpp
struct Person_IMPL { struct NSObject_IMPL NSObject_IVARS; int _age; MyBlock _Nonnull _block; }; struct NSObject_IMPL { Class isa; }; static void(* _I_Person_block(Person * self, SEL _cmd) )(){ return (*(MyBlock _Nonnull *)((char *)self + OBJC_IVAR_$_Person$_block)); } static void _I_Person_setBlock_(Person * self, SEL _cmd, MyBlock _Nonnull block) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _block), (id)block, 0, 1); }
分析:
<1>我們知道,oc對象編譯成C++後的本質就是一個結構體Person_IMPL,該結構體的第一個成員變數就是isa指針,指向類對象本身;同時,@property修飾的屬性,系統會自動生成一個結構體成員變數,還為之生成getter和setter方法——這些之前的文章已經說過,此處不再贅述!
<2>per實例對象結構體Person_IMPL中含有_block變數,通過setter法_I_Person_setBlock_將block對象(等號右邊)賦值給該_block變數,因此_block指向block對象(強引用);
<3>在__main_block_impl_0結構體中,我們看到Person *__strong per,所以,block對象本身對Person實例對象也是強引用;
綜上:block對象結構體__main_block_impl_0通過其內部成員指針變數Person *__strong per持有Person實例對象(強引用),而Person實例對象結構體Person_IMPL通過其內部成員指針變數_block持有block對象(強引用)——因此二者構成迴圈引用,當autoreleasepool大括弧結束時,block對象和Person實例對象所占記憶體依然沒有被系統回收,因為他們的引用計數依然大於0;
//圖解——註:self是一個auto型的局部變數,指向的是[[Person alloc] init]實例對象
補充:所以block迴圈引用造成的直接後果是記憶體泄露(即程式結束而記憶體沒有被回收——>根本原因是對象引用計數大於0(retain和release使用次數不對等)——>是因為強指針引用造成的);
引伸:當對象所占記憶體被回收時,指向對象的指針(強指針)應當被賦值於nil或者指向其他的合法記憶體,否則會導致野指針調用(亂指)程式崩潰——但是,用weak做記憶體管理策略(即修飾指針變數)時,為什麼系統會自動將指針變數置為nil?這點後面文章會提到!
二、解決方案
思路:
據上分析,打破迴圈引用,只需要將其中一個強引用變成弱引用即可,那麼要改變哪一個弱引用呢?Person實例對象內部擁有block屬性,當該實例對象銷毀時,其block屬性也會隨之銷毀,所以我們只需要將block對象中的Person類型指針變成弱指針即可——通常都是這樣做!
//圖解
1)ARC環境下
方案一:weak修飾
//代碼
void test3() { Person *per = [[Person alloc] init]; per.age = 10; __weak Person *weakPer = per; per.block = ^{ NSLog(@"-------%d", weakPer.age); }; }
//列印
2019-01-18 14:10:17.451718+0800 MJ_TEST[1458:103419] -[Person dealloc] 2019-01-18 14:10:17.452663+0800 MJ_TEST[1458:103419] ---- Program ended with exit code: 0
//clang
struct __test3_block_impl_0 { struct __block_impl impl; struct __test3_block_desc_0* Desc; Person *__weak weakPer; __test3_block_impl_0(void *fp, struct __test3_block_desc_0 *desc, Person *__weak _weakPer, int flags=0) : weakPer(_weakPer) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
分析:
<1>block對象中,Person指針變數類型變成了__weak類型,列印前person對象銷毀了;
<2>另外一種寫法:__weak typeof(per) weakPer = per <=> __weak Person *weakPer = per;(前者寫法居多)
方案二:__unsafe_unretained修飾
註:__weak修飾和__unsafe_unretained,二者有一個非常重要的區別:
經上分析,我們知道Person實例對象銷毀後,其內部的block屬性也會銷毀,那麼其也就不再指向block對象了,而此時一旦block對象沒有任何強引用,作用域結束後,其也會被銷毀,其成員變數Person指針也會被銷毀,這點沒問題!————但是,如果block對象還存在呢(被其他指針強引用),此時其內部成員變數Person指針也存在,但是依然會指向Person實例對象銷毀前所占的記憶體區域,但是該記憶體區域已經被系統回收了,Person指針指向的是不合法的記憶體區域——如果是weak修飾,系統會自動將指針置為nil(指向合法的記憶體區域);如果是__unsafe_unretained修飾,什麼也不會做,這樣就會導致野指針調用!
方案三:__block修飾
//代碼
void test4() { __block Person *per = [[Person alloc] init]; per.age = 10; per.block = ^{ NSLog(@"-------%d", per.age); per = nil; }; per.block(); }
//列印
2019-01-18 15:20:06.697898+0800 MJ_TEST[1810:136831] -------10 2019-01-18 15:20:06.698250+0800 MJ_TEST[1810:136831] -[Person dealloc] 2019-01-18 15:20:06.698300+0800 MJ_TEST[1810:136831] ---- Program ended with exit code: 0
//clang
struct __Block_byref_per_0 { void *__isa; __Block_byref_per_0 *__forwarding; int __flags; int __size; void (*__Block_byref_id_object_copy)(void*, void*); void (*__Block_byref_id_object_dispose)(void*); Person *__strong per; }; struct __test4_block_impl_0 { struct __block_impl impl; struct __test4_block_desc_0* Desc; __Block_byref_per_0 *per; // by ref __test4_block_impl_0(void *fp, struct __test4_block_desc_0 *desc, __Block_byref_per_0 *_per, int flags=0) : per(_per->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
分析:
<1>block對象持有__block對象,而__block對象又持有Person實例對象,而Person實例對象又持有block對象——如此,構成一個三角迴圈:
<2>通過block回調,Person實例對象指針被置為nil,而該指針本質是__block對象中的Person *__strong per指針,因此該指針不可能再指向Person實例對象了,所以,第2條持有就斷開了,打破了三角迴圈;
說明:但是該方案看起來比較麻煩,一旦忘記將指針置為nil,就會造成記憶體泄露;
註:以上分析不理解,請參考前述block文章,此處不再贅述!
2)MRC環境
說明:該環境下不支持__weak修飾;
方案一:__unsafe_unretained修飾
//代碼
void test5() { // __unsafe_unretained Person *per = [[Person alloc] init]; __block Person *per = [[Person alloc] init]; per.age = 10; per.block = [^{ NSLog(@"-------%d", per.age); } copy]; [per release]; per = nil; }
//列印
2019-01-18 16:42:49.970441+0800 MJ_TEST[2257:177470] -[Person dealloc] 2019-01-18 16:42:49.971587+0800 MJ_TEST[2257:177470] ---- Program ended with exit code: 0
分析:
<1>根據習慣,MRC環境下,我們通常會將block對象(等號右邊)從棧區copy到堆區,以達到手動控制其記憶體銷毀的目的;
<2>原理:調用alloc創建對象時,系統會自動將該實例對象引用計數置為1,而該對象又會隨著block的copy而一起被copy到堆區,此時該對象的retainCount會加1(變成2),當對該對象發送release消息時,其retainCount自動減1(由2變成1),所以當程式結束時,Person實例對象retainCount為1(>0),其記憶體並不會被系統回收從而導致記憶體泄露;
那麼,__unsafe_unretained修飾後,無論後面有多少次retain或者copy操作,Person實例對象的retainCount始終為1,所以程式結束前release時,其retainCount值變為0,此時記憶體被回收,而不會導致記憶體泄露的問題;
方案二:__block修飾
分析:為什麼block可以?
<1>前述文章我們分析到,MRC環境下,__block修飾對象類型的auto局部變數,系統生成的__block對象並不會根據其記憶體成員變數Person指針變數(其實就是test5()方法中的per指針)是強指針類型而對Person實例對象([[Person alloc] init])進行retain操作(強引用);
<2>所以此時,__block的作用相當於__unsafe_unretained的作用,原理一樣;
補充一個問題:在ARC環境下,弱指針不能通過"->"形式來訪問對象的成員變數
原因:就是weakSelf很可能為為空(即有可能提前被釋放了),所以必須使用強指針來訪問
//代碼
Person.m
- (void) test6 { __weak typeof(self) weakSelf = self; self.block = ^{ __strong typeof(weakSelf) strongSelf = weakSelf; NSLog(@"-------%d", strongSelf->_age); }; }
main.m
int main(int argc, const char * argv[]) { @autoreleasepool { // test1(); // test2(); // test3(); // test4(); // test5(); Person *per = [[Person alloc] init]; [per test6]; } NSLog(@"----"); return 0; }
//列印
2019-01-18 17:43:41.010848+0800 MJ_TEST[2557:208583] -[Person dealloc] 2019-01-18 17:43:41.011346+0800 MJ_TEST[2557:208583] ---- Program ended with exit code: 0
//clang
Person.m
struct __Person__test6_block_impl_0 { struct __block_impl impl; struct __Person__test6_block_desc_0* Desc; Person *const __weak weakSelf; __Person__test6_block_impl_0(void *fp, struct __Person__test6_block_desc_0 *desc, Person *const __weak _weakSelf, int flags=0) : weakSelf(_weakSelf) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
分析:block對象的內部成員變數weakSelf依然是weak類型,並不受block代碼塊內部的__strong轉化,該轉化只是為了騙取編譯器通過編譯而已;
三、結論
1)原因:block對象與OC對象相互持有(強引用)——OC對象有block屬性,block代碼塊中用到了該實例對象;
2)危害:程式結束時,相互強應用(對象的引用計數>0)導致實例對象所占記憶體不能及時被系統回收——即記憶體泄露;
3)解決:
<1>ARC:__weak修飾(常用)、__unsafe_unretained(會引起野指針調用,不推薦)、__block(過於繁瑣,不推薦);
<2>MRC:__unsafe_unretained和__block;