探尋Block的本質(2)—— 底層結構markdown
探尋Block的本質(6)—— __block的深刻分析post
前面的章節裏面,咱們瞭解到Block也是一個OC對象,由於它的底層結構中也有isa
指針。例以下面這個block:ui
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
//Block的定義
void (^block)(void) = ^(){
NSLog(@"Hello World");
};
NSLog(@"%@", [block class]);
NSLog(@"%@", [block superclass]);
NSLog(@"%@", [[block superclass] superclass]);
NSLog(@"%@", [[[block superclass] superclass] superclass]);
}
return 0;
}
*********************** 運行結果 **************************
2019-06-05 14:44:53.179548+0800 Interview03-block[16670:1570945] __NSGlobalBlock__
2019-06-05 14:44:53.179745+0800 Interview03-block[16670:1570945] __NSGlobalBlock
2019-06-05 14:44:53.179757+0800 Interview03-block[16670:1570945] NSBlock
2019-06-05 14:44:53.179767+0800 Interview03-block[16670:1570945] NSObject
Program ended with exit code: 0
複製代碼
上面的代碼中,咱們經過 [xxx class]
和 [xxx supperclass]
方法,打印出block
的類型以及父類的類型,能夠看繼承關係是這樣的 __NSGlobalBlock__
->__NSGlobalBlock
->NSBlock
->NSObject
這也能夠很好地證實block是一個對象,由於它的基類就是NSObject
。並且咱們也就知道了,block中的isa
成員變量確定是從NSObject
繼承而來的。atom
它的編譯後形式以下 圖中的信息代表,該block的isa
指向的class爲_NSConcreteStackBlock
。 奇怪,難道這裏isa指向的class不該該和程序運行時打印出來的class一致嗎?spa
這裏補充一個細節:目前來講,LLVM編譯器生成的中間文件再也不是C++形式了,而咱們在命令行裏面,其實是經過clang生成的C++文件,在語法細節上這二者是有差異的,可是大部分的邏輯和原理仍是相近的,因此經過clang生成的C++中間代碼,僅供咱們做爲參考,最終仍是必須以運行時的結果爲準,由於Runtime仍是會在程序運行的時候,對以前編譯事後的中間碼進行必定的處理和調整的。命令行
Block有3種類型 下面咱們來一一解析,首先咱們在回顧一下程序的內存佈局設計
- 代碼段 佔用空間很小,通常存放在內存的低地址空間,咱們平時編寫的全部代碼,就是放在這個區域
- 數據段 用來存放全局變量
- 堆區 是動態分配內存的,用來存放咱們代碼中經過alloc生成的對象,動態分配內存的特色是須要程序員申請內存和管理內存。例如OC中alloc生成的對象須要調用releas方法釋放【MRC下】,C中經過malloc生成的對象必需要經過free()去釋放。
- 棧區 系統自動分配和銷燬內存,用於存放函數內生成的局部變量
下面藉助一個經典的圖例,來看一看不一樣類型的block到底存儲在哪裏!
若是一個block內部沒有使用/訪問 自動變量(auto變量),那麼它的類型即爲
__NSGlobalBlock__
,它會被存儲在應用程序的 數據段
咱們用代碼來驗證一下 以上三個圖,展現了 除了auto變量外的其餘幾種變量被block訪問的狀況,打印的結果都是以下
2019-06-05 16:38:31.885797+0800 Interview03-block[17590:1712446] __NSGlobalBlock__
Program ended with exit code: 0
複製代碼
結果顯示block的類型都是__NSGlobalBlock__
。其實這種類型的block沒有太多的應用場景,因此出鏡率的不多,這裏僅做了解就行。
若是一個block有使用/訪問 自動變量(auto變量),那麼它的類型即爲
__NSStaticBlock__
,它會被存儲在應用程序的 棧區
咱們繼續驗證一波,以前代碼調整以下 打印結果以下
2019-06-05 16:45:25.990687+0800 Interview03-block[17648:1721701] __NSMallocBlock__
Program ended with exit code: 0
複製代碼
咦?怎麼這裏的結果是__NSMallocBlock__
?不該該是__NSStaticBlock__
嗎?緣由在於當前處於ARC環境下,ARC機制已經爲咱們作過了一些處理,爲了看清本質,咱們先關掉ARC再跑一邊代碼,輸出結果以下
2019-06-05 16:52:08.500787+0800 Interview03-block[17712:1730384] __NSStackBlock__
Program ended with exit code: 0
複製代碼
好,咱們看到,再沒有ARC的幫助下,這裏的block類型確實是__NSStackBlock__
。 其實咱們在不少場景下,都會用到這種類型的block,由於不少狀況下,咱們都會在block 中用到環境變量,而大部分的環境變量均可能是auto變量,思考一下,若是咱們不作任何處理,會碰到什麼麻煩嗎?(💡提醒:結合棧區內容的生命週期)
咱們再將生面的代碼調整以下
#import <Foundation/Foundation.h>
void (^block)(void);//全局變量block
void test(){
int a = 10;
block = ^(){
NSLog(@"a的值爲---%d",a);
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block();
}
return 0;
}
複製代碼
根據以上的代碼,你的預期打印結果是多少呢,a的值10能被正確打印出來嗎?看運行結果
2019-06-05 17:04:25.915160+0800 Interview03-block[17820:1746272] a的值爲----272632584
Program ended with exit code: 0
複製代碼
瞧,a
如今的值爲272632584
,很顯然,這樣的值用在咱們的程序裏面,確定就破壞了咱們原有的設計思路了。
那麼就來分析一下:
block
是一個定義在函數外的全局變量test()
內,代碼^(){ NSLog(@"a的值爲---%d",a); };
首先會爲咱們生成一個__NSStaticBlock__
類型的Block,它存儲與當前函數test()
的棧空間內,而後它的指針被賦值給了全局變量block
。main
函數中,首先調用函數test()
,全局變量block
就指向了test()
函數棧上的這個__NSStaticBlock__
類型的Block,而後test()
調用結束,棧空間回收block
被調用,問題就出在這裏,此時,test()
的棧空間都被系統回收去作其餘事情了,也就是說上面的那個__NSStaticBlock__
類型的Block的內存也被回收了。雖然經過對象block
(或者說block指針
),最終還可訪問原來變量a
的所指向的那塊內存,可是這裏面寸的值就沒法保證是咱們所須要的10
了,因此能夠看到打印結果是一個沒法預期的數字。❓❓那麼該怎麼解決這個問題呢?很天然的,咱們就會想到,須要將那個
__NSStaticBlock__
類型的Block轉移到堆區上面去,這樣它不會隨着函數棧區的回收而被銷燬,而能夠由程序員在使用完它以後再去銷燬它。
對
__NSMallocBlock__
調用copy
方法,就能夠轉變成__NSMallocBlock__
,它會被存儲在堆區上
把上面的代碼調整以下
#import <Foundation/Foundation.h>
void (^block)(void);//全局變量block
void test(){
int a = 10;
block = [^(){ NSLog(@"a的值爲---%d",a); } copy];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block();
NSLog(@"block的類型爲%@",[block class]);
}
return 0;
}
複製代碼
在給block
賦值前,先進行copy
操做,獲得以下打印結果
2019-06-05 17:44:16.940492+0800 Interview03-block[18166:1799723] a的值爲---10
2019-06-05 17:44:16.940752+0800 Interview03-block[18166:1799723] block的類型爲__NSMallocBlock__
Program ended with exit code: 0
複製代碼
能夠看到, 變量a
的打印值仍是10
,而且block
所指向的也確實是一個__NSMallocBlock__
。正是因爲copy
以後, [^(){ NSLog(@"a的值爲---%d",a); } copy];
所返回的Block是存放在堆上的,因此裏面a
的值還是被捕獲時後的值10
,所以打印結果不受影響。
你或許會好奇,若是對
__NSGlobalBlock__
調用copy
方法呢?這裏就直接告訴你,結果仍然是一個__NSGlobalBlock__
,有興趣能夠自行代碼走一波,這裏再也不贅述。
對每一種類型的block調用copy後的結果以下
上面的篇幅,咱們都是基於MRC環境下,對Block在內存中的存儲狀況進行討論。因爲咱們在平時代碼中生成的block都是在函數內建立的,也就是都是__NSStaticBlock__
類型的,而一般咱們須要將其保存下來,在未來的某個時候調用,可是那個時間點上每每該block所在的函數棧已經不存在了,所以在MRC環境下,咱們須要經過對其調用copy
方法,將__NSStaticBlock__
的內容複製到堆區內存上,使之成爲一個__NSMallocBlock__
,這樣纔不影響後續的使用,同時,做爲使用者,須要確保在使用完block以後而不在須要它的時候,對block調用release
方法將其釋放掉,這樣才能避免產生內存泄漏問題。
ARC的出現,爲咱們開發者作了不少繁瑣而細緻的工做,是咱們不用再內存管理方面耗費太多精力,其中,就包括了對block的copy處理。舉個例子,咱們對上一份代碼微調一下,把copy操做去掉,以下
#import <Foundation/Foundation.h>
void (^block)(void);//全局變量block
void test(){
int a = 10;
block = ^(){ NSLog(@"a的值爲---%d",a); };
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block();
NSLog(@"block的類型爲%@",[block class]);
}
return 0;
}
複製代碼
將ARC開關打開,運行程序咱們獲得以下結果
2019-06-05 20:29:31.503282+0800 Interview03-block[19472:1922021] ************10
2019-06-05 20:29:31.503652+0800 Interview03-block[19472:1922021] block的類型爲__NSMallocBlock__
Program ended with exit code: 0
複製代碼
能夠看到,這跟咱們在MRC下手動將block
進行copy
以後的結果同樣,說明ARC其實替咱們作了相應的copy
操做。
在ARC環境下,編譯器會根據狀況自動將棧上的block複製到堆上,例如如下的狀況
- block做爲函數參數返回的時候
- 將block賦值給
__strong
指針的時候- block做爲Cocoa API中方法名裏面含有
usingBlock
的方法參數時- block做爲GCD API的方法參數的時候
@property (nonatomic, copy) void(^block)(void);
@property (nonatomic, copy) void(^block)(void);
//推薦@property (nonatomic, strong) void(^block)(void);
ARC下關鍵字copy
和strong
對block屬性
的做用是同樣的,由於__strong
指針指向block
的時候,ARC會自動對block
進行copy操做,可是爲了保持代碼的一致性,建議仍是使用copy
關鍵字來修飾。