探尋Block的本質(4)—— Block的類型

Block傳送門🦋🦋🦋

探尋Block的本質(1)—— 基本認識程序員

探尋Block的本質(2)—— 底層結構markdown

探尋Block的本質(3)—— 基礎類型的變量捕獲函數

探尋Block的本質(5)—— 對象類型的變量捕獲佈局

探尋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的類型

Block有3種類型Block的類型 下面咱們來一一解析,首先咱們在回顧一下程序的內存佈局設計

  • 代碼段 佔用空間很小,通常存放在內存的低地址空間,咱們平時編寫的全部代碼,就是放在這個區域
  • 數據段 用來存放全局變量
  • 堆區 是動態分配內存的,用來存放咱們代碼中經過alloc生成的對象,動態分配內存的特色是須要程序員申請內存和管理內存。例如OC中alloc生成的對象須要調用releas方法釋放【MRC下】,C中經過malloc生成的對象必需要經過free()去釋放。
  • 棧區 系統自動分配和銷燬內存,用於存放函數內生成的局部變量

下面藉助一個經典的圖例,來看一看不一樣類型的block到底存儲在哪裏!block的存放區域

(1) NSGlobalBlock(也就是_NSConcreteGlobalBlock)

若是一個block內部沒有使用/訪問 自動變量(auto變量),那麼它的類型即爲__NSGlobalBlock__,它會被存儲在應用程序的 數據段

咱們用代碼來驗證一下block沒有訪問任何變量block訪問了局部static變量block訪問全局變量 以上三個圖,展現了 除了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__,它會被存儲在應用程序的 棧區

咱們繼續驗證一波,以前代碼調整以下 block訪問了auto變量打印結果以下

2019-06-05 16:45:25.990687+0800 Interview03-block[17648:1721701] __NSMallocBlock__
Program ended with exit code: 0
複製代碼

咦?怎麼這裏的結果是__NSMallocBlock__?不該該是__NSStaticBlock__嗎?緣由在於當前處於ARC環境下,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轉移到堆區上面去,這樣它不會隨着函數棧區的回收而被銷燬,而能夠由程序員在使用完它以後再去銷燬它。

(3) NSMallocBlock(也就是_NSConcreteMallocBlock)

__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的類型總結 對每一種類型的block調用copy後的結果以下

ARC環境下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的方法參數的時候

小細節--Block屬性的書寫方法

  • MRC下Block 屬性的書寫建議

@property (nonatomic, copy) void(^block)(void);

  • ARC下Block 屬性的書寫建議

@property (nonatomic, copy) void(^block)(void);//推薦 @property (nonatomic, strong) void(^block)(void);

ARC下關鍵字copystrongblock屬性的做用是同樣的,由於__strong指針指向block的時候,ARC會自動對block進行copy操做,可是爲了保持代碼的一致性,建議仍是使用copy關鍵字來修飾。

Block傳送門🦋🦋🦋

探尋Block的本質(1)—— 基本認識

探尋Block的本質(2)—— 底層結構

探尋Block的本質(3)—— 基礎類型的變量捕獲

探尋Block的本質(5)—— 對象類型的變量捕獲

探尋Block的本質(6)—— __block的深刻分析

相關文章
相關標籤/搜索