iOS-block1-底層結構、變量捕獲、類型

如下代碼就是個block,block不會主動調用

^{
    NSLog(@"this is a block!");
};

運行之后,發現并沒有打印。
如果在block后面加個(),發現block就立馬調用了:

^{
    NSLog(@"this is a block!");
}();

運行后:
this is a block!

一般我們都把block保存起來,在需要的時候才調用。

一. block的底層結構(block的本質)

block是封裝了函數調用以及函數調用環境的OC對象

下面我們驗證上面這句話。
寫個簡單的block,其中block內部使用了block外部的age變量:

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        int age = 20;
        //block定義
        void (^block)(int, int) =  ^(int a , int b){
            NSLog(@"this is a block! -- %d", age);
        };
        age = 30;
        //block調用
        block(10, 10);
    }
    return 0;
}

block調用之后打印:this is a block! -- 20 。為什么不是30?

源碼分析

通過“xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 輸出的CPP文件”指令,將上面代碼轉成C++代碼之變成這樣:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        int age = 20;
        //block底層定義
        void (*block)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
        //block底層調用
        ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10);
    }
    return 0;
}

但是由于底層的代碼添加了許多強轉,我們簡化代碼,如下:

//block底層定義
void (*block)(void) = &__main_block_impl_0(
                                           __main_block_func_0,
                                           &__main_block_desc_0_DATA
                                           );
//block底層調用
block->FuncPtr(block, 10, 10);

一共就兩行代碼,我們就一個一個研究

① block底層定義
//block底層定義
void (*block)(void) = &__main_block_impl_0(
                                           __main_block_func_0,
                                           &__main_block_desc_0_DATA
                                           );

先看__main_block_impl_0這個函數,我們發現它被定義在一個同名結構體里面,這個__main_block_impl_0結構體就是block的底層實現

struct __main_block_impl_0 {
  struct __block_impl impl; 
  struct __main_block_desc_0* Desc; 
  int age;
  // 構造函數(類似于OC的init方法),返回結構體對象
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock; //isa指向類對象,就比如str的isa指向NSString
    impl.Flags = flags;
    impl.FuncPtr = fp; //外面的__main_block_func_0函數地址傳進來,保存在這里
    Desc = desc; //外面的__main_block_desc_0結構體地址傳進來,保存在這里
  }
};

先看結構體里面的__main_block_impl_0函數,其實這個函數在C++中是構造方法,類似于OC的init方法,返回結構體對象。函數的第一個參數是fp指針,第二個參數是desc指針。

接下來再看看__main_block_impl_0結構體,它第一個成員是個結構體:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

發現這個結構體里面第一個成員就是isa,驗證了block本質上也是一個OC對象。

第二個成員是指向__main_block_desc_0結構體的指針:

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

可以發現,這個結構體被重新命名為__main_block_desc_0_DATA,默認傳入了兩個值0和sizeof(struct __main_block_impl_0),block的底層就是__main_block_impl_0結構體,所以這個結構體第二個值保存的是block的大小。

接下來我們看一下__main_block_impl_0函數的參數,第一個參數是指向__main_block_func_0函數的指針,如下:

//封裝了block執行邏輯的函數
//第一個參數是block,后面是block調用的時候傳入的參數
void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
  int age = __cself->age; // bound by copy

  NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_3f4c4a_mi_0, age);
}

可以發現,我們寫的代碼塊里面的NSLog被封裝成了__main_block_func_0函數,它引用了block外面的成員變量age。

第二個參數就是上面我們說的__main_block_desc_0結構體的地址。

現在我們知道了,首先__main_block_impl_0函數有兩個參數,第一個參數是__main_block_func_0函數的地址(這個函數里面封裝了我們block里面執行的代碼),第二個參數是__main_block_desc_0結構體的地址(這個結構體里面有保存block的大小),整個函數的返回值是個__main_block_impl_0結構體,block底層就是__main_block_impl_0結構體,最后再獲取__main_block_impl_0結構體的地址,賦值給左邊的“block”變量,然后我們拿到“block”變量就可以做其他事情了,至此,block定義完成。

② block底層調用
//block底層調用 
block->FuncPtr(block, 10, 10);

這句代碼就很簡單了,直接取出block里面的FuncPtr函數,傳入參數進行調用。

這里你可能會有個小疑問,不應該是通過“block-> impl->FuncPtr(block, 10, 10)”來拿到FuncPtr嗎?
其實我們在簡化之前,代碼是這樣的:

((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10);

可以發現系統把block強轉成__block_impl類型的了,由于impl又是__main_block_impl_0結構體的第一個成員,所以impl的地址和__main_block_impl_0結構體的地址是一樣的,強轉之后可以直接獲取到FuncPtr。

根據如上分析,驗證了,block是封裝了函數調用以及函數調用環境的OC對象

總結:

block的本質.png

如上圖所示,block底層就是一個__main_block_impl_0結構體,它由三個部分組成:

  1. 第一部分是impl,它是個結構體,里面有isa指針和FuncPtr指針,FuncPtr指針指向__main_block_func_0函數,這個函數里面封裝了block需要執行的代碼。
  2. 第二部分是desc,它是個指針,指向__main_block_desc_0結構體,它里面有一個Block_size用來保存block的大小。
  3. 第三部分是age,它把外面訪問的成員變量age封裝到自己里面了。

關于block的本質,網上還有一張圖,可自己參考:

block的本質.png

二. block的變量捕獲(capture)

在上面我們留了一個問題,block調用之后打印:this is a block! -- 20 。為什么不是30?

局部變量和全局變量的捕獲

捕獲機制.png
  1. 如果是被auto修飾的局部變量,會被捕獲,是值傳遞
  2. 如果是被static修飾的局部變量,會被捕獲,是指針傳遞
  3. 如果是全局變量,不會被捕獲,因為可以直接訪問

auto自動變量,離開作用域就銷毀,默認省略auto。比如我們常見的 int age = 10,其實就是默認省略了auto,本來應該是auto int age = 10

我們執行如下代碼:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
       auto int age = 10; 
       static int height = 10;

    void (^block)(void) = ^{
       // age的值捕獲進來
       // height的指針捕獲進來
       NSLog(@"age is %d, height is %d", age, height);
    };

      age = 20;
      height = 20;

      block();

    }
    return 0;
}

打印:

age is 10, height is 20

這就解釋了上面的疑問,因為age是值捕獲,所以修改外面的age值不會影響block里面的age值。

① 源碼分析

為了探究block內部是怎么做到的,我們將上面的代碼轉成C++代碼,抽取關鍵的代碼,如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  int *height;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy
  int *height = __cself->height; // bound by copy

  NSLog((NSString *)&__NSConstantStringImpl__var_folders_2w_t9gvrhjs7gv_m4kb_8q3r_980000gn_T_main_12eb7d_mi_1, age, (*height));
}

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

    auto int age = 10;
    static int height = 10;

    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height));

    age = 20;
    height = 20;

    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }

    return 0;
}

可以看出age和height都被捕獲了,age是值捕獲,height是指針捕獲。

  1. 我們定義block,其實就是初始化__main_block_impl_0結構體,定義block的時候會把age和&height傳進去,這個結構體里面有一個age一個height指針用于接收傳進去的值,這行代碼“age(_age), height(_height)”就是保證外面的變量改變的時候實時改變結構體里面的age和height指針的值。
  2. 我們調用block的時候,其實就是執行__main_block_func_0函數,這個函數會獲取__main_block_impl_0結構體中age和height指針的值,所以打印的時候就會把age和*height的值打印出來。
  3. 所以執行完block之后age的值沒改變,因為是值傳遞,height的值改變了,因為是指針傳遞。

② 為什么auto變量是值傳遞,static變量是指針傳遞呢?

因為auto變量在{}結束之后就會被銷毀,被銷毀之后變量的內存就消失了,將來在其他地方執行block的時候就不可能再去訪問auto變量的內存了。
但是static變量不一樣,被static修飾的變量一直在內存中,只要捕獲它的指針就可以隨時訪問它的內存了。

③ 為什么全局變量不需要捕獲呢?

下面我們驗證下,如下代碼:

int age_ = 10;
static int height_ = 10;

void (^block)(void);

void test()
{
    auto int a = 10;
    static int b = 10;
    block = ^{
        NSLog(@"age is %d, height is %d", a, b);
    };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        test();
        block();
    }
    return 0;
}

轉成C++文件之后,代碼如下:

int age_ = 10;
static int height_ = 10;

void (*block)(void);

struct __test_block_impl_0 {
  struct __block_impl impl;
  struct __test_block_desc_0* Desc;
  int a;
  int *b;
  __test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int _a, int *_b, int flags=0) : a(_a), b(_b) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy
  int *b = __cself->b; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_fd2a14_mi_0, a, (*b));
    }

void test()
{
    auto int a = 10;
    static int b = 10;
    block = ((void (*)())&__test_block_impl_0((void *)__test_block_func_0, &__test_block_desc_0_DATA, a, &b));
}

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        test();
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

可以看出age_和height_沒被捕獲,a和b被捕獲了,相信不用解釋上面的代碼也能看懂。

總結

為什么局部變量需要捕獲,全局變量不需要捕獲呢?
因為作用域的問題。局部變量的作用域在{}中,當在其他函數中想要訪問局部變量的值,肯定要把它捕獲啊(auto變量是值捕獲,static變量是指針捕獲)。對于全局變量,任何地方都可以訪問,所以沒必要捕獲。

④ self的捕獲

下面代碼,創建MJPerson對象:
MJPerson.h

#import <Foundation/Foundation.h>

@interface MJPerson : NSObject

@property (copy, nonatomic) NSString *name;

- (void)test;

- (instancetype)initWithName:(NSString *)name;

@end

MJPerson.m

#import "MJPerson.h"

@implementation MJPerson

- (void)test
{
    void (^block)(void) = ^{
        NSLog(@"-------%d", [self name]);
    };
    block();
}

- (instancetype)initWithName:(NSString *)name
{
    if (self = [super init]) {
        self.name = name;
    }
    return self;
}

@end

如上代碼,在test方法里面訪問name屬性,那么self會被捕獲嗎?name會被捕獲嗎?

將MJPerson.m轉成C++代碼:

struct __MJPerson__test_block_impl_0 {
  struct __block_impl impl;
  struct __MJPerson__test_block_desc_0* Desc;
  MJPerson *self;
  __MJPerson__test_block_impl_0(void *fp, struct __MJPerson__test_block_desc_0 *desc, MJPerson *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __MJPerson__test_block_func_0(struct __MJPerson__test_block_impl_0 *__cself) {
  MJPerson *self = __cself->self; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_MJPerson_1027e6_mi_0, ((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("name")));
    }

static void _I_MJPerson_test(MJPerson * self, SEL _cmd) {
    void (*block)(void) = ((void (*)())&__MJPerson__test_block_impl_0((void *)__MJPerson__test_block_func_0, &__MJPerson__test_block_desc_0_DATA, self, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

static instancetype _I_MJPerson_initWithName_(MJPerson * self, SEL _cmd, NSString *name) {
    if (self = ((MJPerson *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MJPerson"))}, sel_registerName("init"))) {
        ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)self, sel_registerName("setName:"), (NSString *)name);
    }
    return self;
}

可以看出self被捕獲了,并且是指針捕獲,既然被捕獲,就說明self是局部變量。

為什么self是局部變量呢?
其實每個方法都兩個隱式參數,一個是self一個是_cmd,self是方法調用者,_cmd是方法名,既然self被當做參數了,那self肯定是局部變量了,也可以在上面的代碼中進行驗證,如下:

void _I_MJPerson_test(MJPerson * self, SEL _cmd)

對于[self name],在上面的代碼可以看出是給self發送消息,如下:

objc_msgSend((id)self, sel_registerName("name"))

所以,block會捕獲self,如果想要訪問self中的成員變量就給self發送消息就好了(self都被捕獲了,肯定可以獲取到self中的其他信息了)。

總結:局部變量會捕獲,全局變量不會捕獲。

Demo地址:block的本質和變量捕獲

三. block的類型

接下來我們講的都是在MRC環境下。

block有3種類型,可以通過調用class方法或者isa指針查看具體類型,最終都是繼承于NSBlock類型

__NSGlobalBlock__ ( _NSConcreteGlobalBlock )
__NSMallocBlock__ ( _NSConcreteMallocBlock )
__NSStackBlock__ ( _NSConcreteStackBlock )

1. 驗證block是OC對象

前面我們說了,block是OC對象,既然是OC對象就可以調用class方法查看類型

下面驗證block是OC對象,運行代碼:

void (^block)(void) = ^{
            NSLog(@"Hello");
        };
        
NSLog(@"%@", [block class]);
NSLog(@"%@", [[block class] superclass]);
NSLog(@"%@", [[[block class] superclass] superclass]);
NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);

打印結果:

__NSGlobalBlock__
__NSGlobalBlock
NSBlock
NSObject

可以看出上面的block繼承關系是:__NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject
上面的block最終繼承于NSObject,說明block是OC對象。現在我們也明白了,block內部的isa就是從NSObject中獲取的,而且isa指向block的類對象。

2. 三種block在內存中的分布

iOS程序的內存布局

剛才說了block有三種類型,這三種block在內存中的分布如下圖:

三種block內存分配.png
  1. 上圖的text區就是代碼段,我們編寫的代碼都在代碼段,代碼段內存地址比較小,上圖從上往下,內存地址越來越大。
  2. data區就是數據段,數據段一般都放一些全局變量。
  3. 堆:動態分配內存,需要程序員自己申請內存,也需要程序員自己管理內存。比如[NSObject alloc]或者malloc()創建的對象就是存放在堆,需要我們自己管理內存(只不過ARC不需要你管了)。
  4. 棧:系統自動分配內存,自動銷毀內存。存放局部變量,系統會在{}結束之后銷毀局部變量。棧的內存地址最大。

驗證內存地址由低到高:

int age = 10;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    int a = 10;
    NSLog(@"數據段:age %p", &age);
    NSLog(@"堆:obj %p", [[NSObject alloc] init]);
    NSLog(@"棧:a %p", &a);
    NSLog(@"數據段:class %p", [MJPerson class]);
    }
    return 0;
}

打印:

數據段:age 0x100002488
堆:obj 0x10061b5e0
棧:a 0x7ffeefbff46c
數據段:class 0x100002438

如上,如果想知道MJPerson類對象放在哪里,可以看它的地址和哪個比較接近,比較地址可知,放在數據段。

現在我們知道了三種block在內存中的分布:

__NSGlobalBlock__是放在數據段的,也就是和全局變量放在一起。
__NSMallocBlock__是放在堆區的,和一般的OC對象一樣的,需要我們自己管理內存。
__NSStackBlock__是放在棧區的,和局部變量是一樣的,系統自動管理內存。

但是什么樣的block才是這三種block的某一種類型呢?

先看結論:

block類型 環境
__NSGlobalBlock__ 沒有訪問auto變量
__NSMallocBlock__ __NSStackBlock__調用了copy
__NSStackBlock__ 訪問了auto變量

GlobalBlock沒有訪問auto變量,這種類型的block都可用方法代替,不常用。

StackBlock訪問了auto變量,放在棧區,這時候block捕獲了auto變量的值,然后存儲在block結構體內部,棧區是系統自動管理的,所以在代碼塊結束之后,block內存會被銷毀,這時候block結構體內部的值就是亂七八糟的了,block就會有問題,如下:

void (^block)(void);
void test2()
{
    // NSStackBlock
    int age = 10;
    block = ^{
        NSLog(@"block---------%d", age);
    };

    NSLog(@"%@", [block class]);
}
執行方法:
test2();
block();

打印:
__NSStackBlock__
block---------2634434;

可以看出打印age的值,就是亂的。

那么如何解決這個問題呢?
可以把block從棧放到堆里面,每一種類型的block調用copy后的結果如下所示:

block類型 副本源的配置存儲域 復制效果
__NSGlobalBlock__ 程序的數據區段 什么也不做
__NSMallocBlock__ 引用計數器增加
__NSStackBlock__ 從棧復制到堆

比如,將上面NSStackBlock加個copy變成NSMallocBlock,打印就是正確的,如下:

void test2()
{
    // NSStackBlock ->NSMallocBlock
    int age = 10;
    block = [^{
        NSLog(@"block---------%d", age);
    } copy];

    NSLog(@"%@", [block class]);
   [block release]; //如果是MAC,由于放在堆區了,要自己release
}

打印:

__NSMallocBlock__
block---------10

關于NSGlobalBlock的copy操作和NSMallocBlock的copy操作的結果可自行驗證。

Demo地址:block的類型

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容