天天看點

Block筆記(4)—— Block的類型

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筆記(4)—— Block的類型

圖中的資訊表明,該block的

isa

指向的class為

_NSConcreteStackBlock

奇怪,難道這裡

isa

指向的

class

不應該和程式運作時列印出來的

class

一緻嗎?

這裡補充一個細節:目前來說,LLVM編譯器生成的中間檔案不再是C++ 形式了,而我們在指令行裡面,實際上是通過clang生成的C++ 檔案,在文法細節上這兩者是有差别的,但是大部分的邏輯和原理還是相近的,是以通過clang生成的C++ 中間代碼,僅供我們作為參考,最終還是必須以運作時的結果為準,因為Runtime還是會在程式運作的時候,對之前編譯過後的中間碼進行一定的處理和調整的。

Block的類型

Block有3種類型

Block筆記(4)—— Block的類型

下面我們來一一解析,首先我們在回顧一下程式的記憶體布局

  • 代碼段 占用空間很小,一般存放在記憶體的低位址空間,我們平時編寫的所有代碼,就是放在這個區域
  • 資料段 用來存放全局變量
  • 堆區 是動态配置設定記憶體的,用來存放我們代碼中通過

    alloc

    生成的對象,動态配置設定記憶體的特點是需要程式員申請記憶體和管理記憶體。例如OC中

    alloc

    生成的對象需要調用

    releas

    方法釋放【MRC下】,C中通過

    malloc

    生成的對象必須要通過

    free()

    去釋放。
  • 棧區 系統自動配置設定和銷毀記憶體,用于存放函數内生成的局部變量

下面借助一個經典的圖例,來看一看不同類型的block到底存儲在哪裡!

Block筆記(4)—— Block的類型
(1) NSGlobalBlock(也就是_NSConcreteGlobalBlock)
如果一個block内部沒有使用/通路自動變量(auto變量),那麼它的類型即為

__NSGlobalBlock__

,它會被存儲在應用程式的 資料段

我們用代碼來驗證一下

Block筆記(4)—— Block的類型
Block筆記(4)—— Block的類型
Block筆記(4)—— 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筆記(4)—— Block的類型

列印結果如下

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

咦?怎麼這裡的結果是

__NSMallocBlock__

?不應該是

__NSStaticBlock__

嗎?原因在于目前處于ARC環境下,ARC機制已經為我們做過了一些處理,為了看清本質,我們先關掉ARC

Block筆記(4)—— Block的類型

再跑一邊代碼,輸出結果如下

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)

__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筆記(4)—— Block的類型

對每一種類型的block調用copy後的結果如下

Block筆記(4)—— Block的類型

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);

//推薦

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

ARC下關鍵字

copy

strong

block屬性

的作用是一樣的,因為

__strong

指針指向

bloc

k的時候,ARC會自動對

block

進行copy操作,但是為了保持代碼的一緻性,建議還是使用

copy

關鍵字來修飾。
Block系列文章—————————————

Block筆記(1)—— 基本認識

Block筆記(2)—— 底層結構

Block筆記(3)—— 基礎類型的變量捕獲

Block筆記(4)—— Block的類型

Block筆記(5)—— 對象類型的auto變量捕獲

Block筆記(6)—— __block的深入分析

————————————————————