Block系列文章—————————————
Block筆記(1)—— 基本認識
Block筆記(2)—— 底層結構
Block筆記(3)—— 基礎類型的變量捕獲
Block筆記(4)—— Block的類型
Block筆記(5)—— 對象類型的auto變量捕獲
Block筆記(6)—— __block的深入分析
————————————————————
前面的章節裡面,我們了解到Block也是一個OC對象,因為它的底層結構中也有
isa
指針。例如下面這個
block
:
#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
繼承而來的。
它的編譯後形式如下
圖中的資訊表明,該block的
isa
指向的class為
_NSConcreteStackBlock
。
奇怪,難道這裡
isa
指向的
class
不應該和程式運作時列印出來的
class
一緻嗎?
這裡補充一個細節:目前來說,LLVM編譯器生成的中間檔案不再是C++ 形式了,而我們在指令行裡面,實際上是通過clang生成的C++ 檔案,在文法細節上這兩者是有差别的,但是大部分的邏輯和原理還是相近的,是以通過clang生成的C++ 中間代碼,僅供我們作為參考,最終還是必須以運作時的結果為準,因為Runtime還是會在程式運作的時候,對之前編譯過後的中間碼進行一定的處理和調整的。
Block的類型
Block有3種類型
下面我們來一一解析,首先我們在回顧一下程式的記憶體布局
- 代碼段 占用空間很小,一般存放在記憶體的低位址空間,我們平時編寫的所有代碼,就是放在這個區域
- 資料段 用來存放全局變量
- 堆區 是動态配置設定記憶體的,用來存放我們代碼中通過
生成的對象,動态配置設定記憶體的特點是需要程式員申請記憶體和管理記憶體。例如OC中
alloc
生成的對象需要調用
alloc
方法釋放【MRC下】,C中通過
releas
生成的對象必須要通過
malloc
去釋放。
free()
- 棧區 系統自動配置設定和銷毀記憶體,用于存放函數内生成的局部變量
下面借助一個經典的圖例,來看一看不同類型的block到底存儲在哪裡!
(1) NSGlobalBlock(也就是_NSConcreteGlobalBlock)
如果一個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沒有太多的應用場景,是以出鏡率的很少,這裡僅作了解就行。
(2) NSStaticBlock(也就是_NSConcreteStaticBlock)
如果一個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); };
類型的Block,它存儲與目前函數__NSStaticBlock__
的棧空間内,然後它的指針被指派給了全局變量test()
。block
- 在
函數中,首先調用函數main
,全局變量test()
就指向了block
函數棧上的這個test()
類型的Block,然後__NSStaticBlock__
調用結束,棧空間回收test()
- 然後
被調用,問題就出在這裡,此時,block
的棧空間都被系統回收去做其他事情了,也就是說上面的那個test()
類型的Block的記憶體也被回收了。雖然通過__NSStaticBlock__
(或者說對象block
),最終還可通路原來變量block指針
的所指向的那塊記憶體,但是這裡面寸的值就無法保證是我們所需要的a
了,是以可以看到列印結果是一個無法預期的數字。10
❓❓那麼該怎麼解決這個問題呢?很自然的,我們就會想到,需要将那個 __NSStaticBlock__
類型的Block轉移到堆區上面去,這樣它不會随着函數棧區的回收而被銷毀,而可以由程式員在使用完它之後再去銷毀它。
(3) NSMallocBlock(也就是_NSConcreteMallocBlock)
對調用
__NSStaticBlock__
方法,就可以轉變成
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後的結果如下
ARC環境下Block的copy問題
上面的篇幅,我們都是基于MRC環境下,對block在記憶體中的存儲情況進行讨論。由于我們在平時代碼中生成的block都是在函數内建立的,也就是都是
__NSStaticBlock__
類型的,而通常我們需要将其儲存下來,在将來的某個時候調用,但是那個時間點上往往該block所在的函數棧已經不存在了,是以在MRC環境下,我們需要通過對其調用
copy
方法,将
__NSStaticBlock__
的内容複制到堆區記憶體上,使之成為一個
__NSMallocBlock__
,這樣才不影響後續的使用,同時,作為使用者,需要確定在使用完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的方法參數的時候
小細節–Block屬性的書寫方法
- MRC下Block 屬性的書寫建議
@property (nonatomic, copy) void(^block)(void);
- ARC下Block 屬性的書寫建議
//推薦
@property (nonatomic, copy) void(^block)(void);
ARC下關鍵字
@property (nonatomic, strong) void(^block)(void);
和
copy
對
strong
的作用是一樣的,因為
block屬性
指針指向
__strong
k的時候,ARC會自動對
bloc
進行copy操作,但是為了保持代碼的一緻性,建議還是使用
block
關鍵字來修飾。
copy
Block系列文章—————————————
Block筆記(1)—— 基本認識
Block筆記(2)—— 底層結構
Block筆記(3)—— 基礎類型的變量捕獲
Block筆記(4)—— Block的類型
Block筆記(5)—— 對象類型的auto變量捕獲
Block筆記(6)—— __block的深入分析