前言
深究block可以說會涉及不少東西,筆者欲通過循序漸進的方式來談及block相關,略陳固陋。閱讀本文前,希望我們還是先一起來過一下幾個概念:
- 指針和對象,都是內存塊。一個大,一個小。一個在棧中,一個在堆中。
- iOS中,我們可以生命一個指針,也可以通過alloc獲取一塊內存。
- 我們可以直接消滅一個指針,將其置為nil,但是我們沒辦法直接消滅一塊對象內存。對于對象內存,我們永遠只能依靠系統去回收。即當這個對象不被任何指針所擁有時,系統就會收回該對象內存。
- 函數在棧區,函數調用完畢后其stack frame將被彈出結束其生命周期。
- Objective-C的對象在內存中是以堆的方式分配空間。
正文
1、ARC strong和weak指針
在講block之前呢先講一下strong和weak指針的問題,以便于更好的理解下面block中的循環引用以及變量截獲等問題。我們知道ARC消除了手動管理內存的煩瑣,編譯器會自動在適當的地方插入適當的retain、release、autorelease語句。規則很簡單,只要還有一個變量指向對象,對象就會保持在內存中。當指針指向新值,或者指針不再存在時,相關聯的對象就會自動釋放。如下圖動畫模擬引用計數回收器,紅色閃爍表示引用計數行為,引用計數的優勢在于垃圾會被很快檢測到,你可以看到紅色閃爍過后緊接著該區域變黑(圖片來自)。
- strong指針
比如在控制器上有個nameField屬性,我在文本框中輸入henvy,那么就可以說,nameField的text屬性是NSString對象的指針,也就是擁有者,該對象保存了文本輸入框的內容。
如果執行了NSString *name = self.nameField.text;
后,@“henvy”對象就有了多個擁有者,也就是有兩個指針指向同一個對象。
接下來我又在文本框中輸入了新的內容比如@"Leslie",此時nameFeild的text屬性就指向了新的NSString對象。但原來的NSString對象仍然還有一個所有者(name變量),因此會繼續保留在內存中。
當name變量獲得新值,或者不再存在時(如局部變量方法返回時、實例變量對象釋放時),原先的NSString對象就不再擁有任何所有者,retain計數降為0,這時對象會被釋放
如,給name變量賦予一個新值name = @"Eason"
時。
我們稱name和nameField.text指針為"Strong指針",因為它們能夠保持對象的生命。默認所有成員變量和局部變量都是Strong指針。
- weak指針
weak型的指針變量依然可以指向一個對象,但不屬于對象的擁有者,就像我是很喜歡你,但是卻得不到你一樣。依然是我們上面的例子在輸入框輸入henvy后執行__weak NSString *name = self.nameField.text;
后,雖然同時指向但name并不真正擁有henvy。
此時如果文本框內容重新輸入@“Leslie”,則原先的henvy對象就沒有擁有者,就會被釋放,此時name變量會自動變成nil,稱為空指針。weak型的指針變量自動變為nil避免了野指針的產生。
舉一個典型的weak指針的例子,即我們的代理模式,控制器ViewController強引用一個myTableView,myTableView的dataSource和delegate都是weak指針,指向你的ViewController。這也是cocoa設定的一個規則,即父對象建立子對象的強引用,而子對象只對父對象建立弱引用。
2、Block的類型
好吧原諒我前面ARC講了那么多,當然還是希望讀者能夠體會筆者的良苦用心。
- NSGlobalBlock
該類型的block存儲在程序的數據區域(text段),不引用外部變量,只對自己的參數做操作,自給自足的狀態,可以當做函數使用,例如:
typedef int (^GlobalBlock)(int);
GlobalBlock block = ^(int count){
return count;
}; //nslog:<__NSGlobalBlock__: 0x10d090200>
- NSStackBlock
該類型的block在非ARC模式存儲在棧區,內部引用外部變量,當棧block結束運行的時候會被請出棧,生命周期結束,再次調用當然crash掉,避免這一點可以通過手動copy將其拷貝到安全的堆上來,脫離棧的危險地帶,因為本身棧區就是過河拆橋、兔死狗烹的狀態。不像堆區講究循環利用,生死由天定(無指針擁有被系統回收)。當然在ARC模式完全不用擔心,ARC模式改寫了天規杜絕NSStackBlock狀況的發生,他會自動將block拷貝到堆上去(block作為方法或函數的參數傳遞時,編譯器不會自動調用copy方法),從而演變成了第三種NSMallocBlock,此時的堆上的block就會像一個ObjC對象一樣被放入autoreleasepool里面,從而保證了返回后的block仍然可以正確執行。因此在本該是NSStackBlock的情況下打印結果就會變成NSMallocBlock。
typedef void (^StackBlock)();
NSString *str = @"henvy";
StackBlock block = ^{
NSLog(@"%@",str);
}; //nslog :<__NSMallocBlock__: 0x7fe412d18790>
- NSMallocBlock
該類型的block存儲在堆區,引用外部變量,由NSStackBlock Block_copy()生成。在ARC模式下可以理解為只存在NSGlobalBlock和NSMallocBlock兩種類型。
3、Block對外部變量的存儲管理
我們都知道內存有堆和棧兩個部分,堆在高地址向下走,棧在低地址向上走。在每個函數調用的時候,系統都會為其生成一個棧的stack frame,該函數結束后這個frame被彈出去;然而堆對象的生存不從屬于某個函數,即便是創建這個堆對象的函數結束了,堆對象也可以繼續存在,因此內存泄漏都是堆對象惹的禍,ObjC里的引用計數就是用來管理堆對象這個東西,由于arc中沒有引用計數的概念,只有強引用和弱引用的概念。當一個變量沒有指針指向它時,就會被系統釋放。因此我們通過下面的代碼分別來測試。
-
靜態變量、全局變量、全局靜態變量
- (void)testStaticObj { static NSString *staticString = nil; staticString = @"henvy"; printf("%p\n", &staticString);//0x10b0d6138 printf("%p\n", staticString);//0x10b0d5290 void (^testBlock)() = ^{ printf("%p\n", &staticString);//0x10b0d6138 printf("%p\n", staticString);//0x0 NSLog(@"%@", staticString);//null }; staticString = nil; testBlock(); }
我這里只放上靜態變量的測試代碼,同全局變量、全局靜態變量。我們發現staticString對象在block的外部和內部對象地址、指針地址都不變,且都在堆區。全局變量和全局靜態變量由于作用域在全局,所以在block內訪問和讀寫這兩類變量和普通函數沒什么區別,而靜態變量作用域在block之外,靜態變量通過指針傳遞,將變量傳遞到block內,進而來修改變量的值,即所謂的地址傳遞。
-
局部變量
- (void)testLocalObj { NSString *localString = nil; localString = @"henvy"; printf("%p\n", &localString); //0x7fff569cca48 printf("%p\n", localString); //0x109234290 void (^testBlock)(void) = ^{ printf("%p\n", &localString); //0x7fcd20511100 printf("%p\n", localString); //0x109234290 NSLog(@"%@", localString); //henvy }; localString = nil; testBlock(); printf("%p\n", &localString); //0x7fff569cca48 printf("%p\n", localString); //0x0 }
我們發現局部變量在block定義前在棧上開辟指針空間,在堆上開辟對象空間,當然遵循ObjC對象的規則,在block內部指針位置發生了變化,對象位置不變,在block定義后同定義前。因而我們發現block對于局部變量只對其對象的值進行了拷貝,并不關心局部變量在外面的死活,跟block內部沒有半點關系,正所謂的值傳遞。
-
block變量
- (void)testBlockObj { __block NSString *blockString = @"henvy"; printf("%p\n", &blockString); //0x7fff54507a38 printf("%p\n", blockString); //0x10b6f9290 void (^testBlock)(void) = ^{ printf("%p\n", &blockString); //0x7feb79c1e4b8 printf("%p\n", blockString); //0x10b6f9290 NSLog(@"%@", blockString);//henvy }; testBlock(); printf("%p\n", &blockString); //0x7feb79c1e4b8 printf("%p\n", blockString); //0x10b6f9290 }
我們發現__block
修飾符的變量在block內部指針地址發生了變化,在block定義后地址徹底改為了新的地址,也就是說值徹底發生了變化,此時的blockString已經不是當年的那個blockString了。
總結一下:靜態變量、全局變量和全局靜態變量是通過指針傳遞,將變量傳遞到block內,進而來修改變量值。而外部變量是通過值傳遞,自然沒法對獲取到的外部變量進行修改,當我們需要修改外部變量時,可以用__block
標記變量,也就是說沒有__block
標記的變量,其值會被復制一份到block私有內存區,而有__block
標記的變量,其地址會被記錄一份在block私有內存區。
4、Block循環引用
了解了強弱引用之后循環引用的問題就很好理解了,在ARC下,copy到堆上的block會強引用進入到該block中的外部變量,這因而導致循環引用的問題,一旦出現循環引用那么對象就會常駐內存,這顯然是誰都不想看到的結果。此時需要用到__weak
來打破這個閉合的環。
- __weak
ViewController控制器內有兩個屬性:
@property (nonatomic, copy)NSString *string;
@property (nonatomic, copy)void(^myBlock)();
在先分析下面的代碼:
self.string = @"henvy";
self.myBlock = ^{
NSLog(@"%@",self.string);
};
self.myBlock();
首先self強引用myBlock,當myBlock被copy到堆上時,myBlock開始強引用self.string,myBlock的擁有者self在Block作用域內部又引用了自己,因此導致了Block的擁有者永遠無法釋放內存,就出現了循環引用的內存泄漏。解決辦法是__weak
:
#define HLWeakSelf(type) __weak typeof(type) weak##type = type
self.string = @"henvy";
HLWeakSelf(self);
self.myBlock = ^{
NSLog(@"%@",weakself.string);
};
self.myBlock();
__weak
就在Block內部對擁有者使用弱引用,通過這種方式告訴block,不要在block內部對self進行強制strong引用了。
- weak-strong dance
在有些特殊情況下,我們在block中又使用__strong
來修飾這個在block外剛剛用__weak
修飾的變量。這么做其實是為了避免在block的執行過程中,突然出現self被釋放的尷尬情況而導致crash,官方說法weak-strong dance。列舉經典到發光的AFNetworking中AFNetworkReachabilityManager.m
的一段代碼:
__weak __typeof(self)weakSelf = self;
AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
__strong __typeof(weakSelf)strongSelf = weakSelf;
strongSelf.networkReachabilityStatus = status;
if (strongSelf.networkReachabilityStatusBlock) {
strongSelf.networkReachabilityStatusBlock(status);
}
};
為了驗證weak-strong dance下面我在一個HLBlockVC類中做如下實驗,實驗目的在于觀察block中的weakSelf到底有沒有釋放,在該類中會并發兩個線程,一個for循環到50后將weakSelf指針置空,另一個線程繼續for循環到100,實驗可能存在兩種結果,一個是for循環到50block結束運行即失敗,另一種情況block仍然繼續輸出到100即實驗成功,下面代碼說話:
在HLBlockVC類的viewDidLoad方法中加載一個線程:
__block HLBlockVC *block = [[HLBlockVC alloc]init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[block toPrintNum];
});
for (int i = 0; i < 51; i ++) {
if (i == 50) {
block = nil;
NSLog(@"BLOCK WAS NIL");
}
}
添加toPrintNum方法,此時單單用weakSelf:
- (void) toPrintNum{
typedef void (^testBlock)();
__weak __typeof(self)weakSelf = self;
testBlock block = ^{
for (int i = 0; i < 100; i ++) {
[weakSelf printNum:i];
}
};
block();
}
-(void)printNum:(int)number{
NSLog(@"%d",number);
}
2016-12-30 17:02:23.791 blockDemo[8520:343178] 48
2016-12-30 17:02:23.791 blockDemo[8520:343098] BLOCK WILL NIL
2016-12-30 17:02:23.792 blockDemo[8520:343178] 49
2016-12-30 17:02:23.792 blockDemo[8520:343098] BLOCK WILL NIL
2016-12-30 17:02:23.792 blockDemo[8520:343178] 50
2016-12-30 17:02:23.792 blockDemo[8520:343098] BLOCK WAS NIL
代碼很清晰,看上面的打印在循環到50的時候block被干掉了,執行結束,weakSelf下沒問題。接下來換上weak-strong dance:
- (void) toPrintNum{
typedef void (^testBlock)();
__weak __typeof(self) weakSelf = self;
testBlock block = ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
for (int i = 0; i < 100; i ++) {
[strongSelf printNum:i];
}
};
block();
}
2016-12-30 18:03:42.934 blockDemo[8752:353486] 97
2016-12-30 18:03:42.935 blockDemo[8752:353486] 98
2016-12-30 18:03:42.935 blockDemo[8752:353486] 99
通過打印的數據可以看出__strong已經安全的保護了block中的weakSelf使之運行至block結束。可以說weak-strong dance是一種強引用 --> 弱引用 --> 強引用的變換過程,可能會被誤解為繞了一圈什么都沒做,其實不然,前者的強變弱是為了打破閉環的僵局,后者弱變強是為了block能夠一直持有弱引用的對象生命,而strongSelf是一個自動變量會在函數執行完釋放。
5、寫在最后
回想一下或許很多時候我們遇到的問題很小,確實,就像文中的weak-strong dance,小到我們連遇到他犯錯的機會都甚少,但堅持把小事做透、以小見大方能防微杜漸,步步為營!
夜深人靜,除了鍵盤聲,就是耳機里傳來的歌聲【旅行團--生命是場馬拉松】夾雜著深夜瑣碎的思緒,希望每一次發文都是對自己的一次洗禮。最后,晚安。