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阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容