37

Block的本质

 5 years ago
source link: http://www.cocoachina.com/ios/20180910/24846.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Block在iOS开发中的用途非常广,今天我们就来一起探索一下Block的底层结构。

1. Block的底层结构

下面是一个没有参数和返回值的简单的Block:

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

        void (^block)(void) = ^{

            NSLog(@"Hello World!");
        };

        block();

        return 0;
    }
}

为了探索Block的底层结构,我们将main.m文件转化为C++的源码、我们打开命令行。cd到包含main.m文件的文件夹,然后输入: clang -rewrite-objc main.m ,这个时候在该文件夹的目录下会生成main.cpp文件。

这个文件非常长,我们直接拉到文件的最下面,找到main函数:

int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
         //定义block
        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
         //调用block
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

        return 0;
    }
}

这第一行代码是定义一个block变量,第二行代码是调用block。这两行代码看起来非常复杂。但是我们可以去简化一下,怎么简化呢?

变量前面的()一般是做强制类型转换的,比如在调用block这一行, block 前面有一个()是(__block_impl *),这就是进行了一个强制类型转换,将其转换为一个 _block_impl 类型的结构体指针,那像这样的强制类型转换非常妨碍我们理解代码,我们可以暂时将这些强制类型转换去掉,这样可以帮助我们理解代码。

化简后的代码如下:

//定义block
void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
//调用block
block->FuncPtr(block);

这样化简后的代码就要清爽多了。我们一句一句的看,先看第一句:

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

这句代码的意思好像就是调用 _main_block_impl_0 这个函数,给这个函数传进两个参数 _main_block_func_0&_main_block_desc_0_DATA ,然后得到这个函数的返回值,取函数返回值的地址,赋值给block这个指针。

我们在稍微上一点的位置可以找到 _main_block_impl_0 这个结构:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
//构造函数,类似于OC的init方法
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__block_impl这个结构体的结构我们可以command+f在main.cpp文件中搜索得到:

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

_main_block_desc_0结构体的结构在main.cpp文件的最下面可以找到:

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

这是一个C++的结构体。而且在这个结构体内还包含一个函数,这个函数的函数名和结构体名称一致,这在C语言中是没有的,这是C++特有的。

在C++的结构体包含的函数称为结构体的构造函数,它就相当于是OC中的init方法,用来初始化结构体。OC中的init方法返回的是对象本身,C++的结构体中的构造方法返回的也是结构体对象。

那么我们就知道了, __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA); 返回的就是 _main_block_impl_0 这个结构体对象,然后取结构体对象的地址赋值给block指针。换句话说,block指向的就是初始化后的 _main_block_impl_0 结构体对象。

我们再看一下初始化 _main_block_impl_0 结构体传进去的参数:

  • 第一个参数是 _main_block_func_0 ,这个参数的结构在上面一点的位置也能找到:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {


  NSLog((NSString *)&__NSConstantStringImpl__var_folders_74_wk04zv690mz36wn0g18r5nxm0000gn_T_main_3b803f_mi_0);
 }

这个函数其实就是把我们Block中要执行的代码封装到这个函数内部了。我们可以看到这个函数内部就一行代码,就是一个NSlog函数,这也就是 NSLog(@"Hello World!"); 这句代码。

把这个函数指针传给 _main_block_impl_0 的构造函数的第一个参数,然后用这个函数指针去初始化 _main_block_impl_0 这个结构体的第一个成员变量 impl 的成员变量 FuncPtr 。也就是说 FuncPtr 这个指针指向 _main_block_func_0 这个函数。

  • 第二个参数是 &_main_block_desc_0_DATA

    我们看一下这个结构:

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

在结构体的构造函数中,0赋值给了reserved, sizeof(struct __main_block_impl_0) 是赋值给了Block_size,可以看出这个结构体存放的是 _main_block_impl_0 这个结构体的信息。在 _main_block_impl_0 的构造函数中我们可以看到, _main_block_desc_0 这个结构体的地址被赋值给了 _main_block_impl_0 的第二个成员变量 Desc 这个结构体指针。也就是说Desc这个结构体指针指向 _main_block_desc_0_DATA 这个结构体。

那么我们总结一下:

Avymu2I.png!web 所以第一句定义block

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

总结起来就是:

  • 1.创建一个函数 _main_block_func_0 ,这个函数

    的作用就是将我们block中要执行的代码封装到函数内部,方便调用。

  • 2.创建一个结构体 _main_block_desc_0 ,这个结构体中主要包含 _main_block_impl_0 这个结构体占用的存储空间大小等信息。

  • 3.将1中创建的 _main_block_func_0 这个函数的地址,和2中创建的 _main_block_desc_0 这个结构体的地址传给 _main_block_impl_0 的构造函数。

  • 4.利用 _main_block_func_0 初始化 _main_block_impl_0 结构体的第一个成员变量 impl 的成员变量 FuncPtr 。这样 _main_bck_impl_0 这个结构体也就得到了block中那个代码块的地址。

  • 5.利用 _mian_block_desc_0_DATA 去初始化 _mian_block_impl_0 的第二个成员变量 Desc

    下面我们再看第二步调用block:

block->FuncPtr(block);

我们知道,block实质上就是指向 _main_block_impl_0 这个结构体的指针,而FuncPtr是 _main_block_impl_0 的第第一个成员变量 impl 的成员变量,正常来讲,block想要调用自己的成员变量的成员变量的成员变量,应该像下面这样调用:

block->impl->FuncPtr

然而事实却不是这样,这是为什么呢?

原因就在于之前我们把所有的强制类型转换给删掉了,之前block前面的()是(__block_impl *),为什么可以这样强制转换呢?因为block指向的是 _main_block_impl_0 这个结构体的首地址,而 _main_block_impl_0
的第一个成员变量是 struct __block_impl impl; ,所以impl和 _main_block_impl_0 的首地址是一样的,因此指向 _main_block_impl_0 的首地址的指针也就可以被强制转换为指向impl的首地址的指针。

之前说过,FuncPtr这个指针在构造函数中是被初始化为指向 _mian_block_func_0 这个函数的地址。因此通过 block->FuncPtr 调用也就获取了 _main_block_func_0 这个函数的地址,然后对 _main_block_func_0 进行调用,也就是执行block中的代码了。这中间block又被当做参数传进了 _main_block_func_0 这个函数。

2.变量捕获-auto变量

auto变量是声明在函数内部的变量,比如 int a = 0; 这句代码声明在函数内部,那a就是auto变量,等价于 auto int a = 0; auto变量时分配在栈区,当超出作用域时,其占用的内存会被系统自动销毁并生成。 下面看一段代码:

int a = 10;

        void (^block)(void) = ^{

            NSLog(@"%d", a);
        };

        a = 20;

        block();

这是一个很简单的Block捕获自动变量的例子,我们看一下打印结果:

2018-09-04 20:39:45.436534+0800 copytest[17163:477148] 10

自动变量a的值明明已经变为了20,为什么输出结果还是10呢?我们把这段代码转化为C++的源码看看。

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

        int a = 10;

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

        a = 20;

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

        return 0;
    }
}

我们还是把代码化简一下来看:

int a = 10;

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

        a = 20;

        block->FuncPtr)(block);

对比一下上面分析的没有捕获自动变量的源代码, 我们发现这里_main_block_impl_0中传入的参数多了一个a。 然后我们往上翻看看_main_block_impl_0的结构:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a; //这是新加入的成员变量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

在_main_block_impl_0这个结构体中我们发现多了一个int类型的成员变量a,在结构体的构造函数中多了一个参数int _a,并且用这个int _a去初始化成员变量a。

所以在 void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a); 中传入了自动变量a用来初始化_main_block_impl_0的成员变量a。那这个时候_main_block_impl_0的成员变量a就被赋值为10了。

由于上面这一步是值传递,所以当执行 a = 20 时,_main_block_impl_0结构体的成员变量a的值是不会随之改变的,仍然是10。

然后我们再来看一下_main_block_func_0的结构有何变化:

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

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_74_wk04zv690mz36wn0g18r5nxm0000gn_T_main_fb5f0d_mi_0, a);
        }

可以看到,通过传入的_main_block_impl_0这个结构体获得其成员变量a的值。

3.变量捕获-static变量

上面讲的捕获的是自动变量,在函数内部声明的变量默认为自动变量,即默认用auto修饰。那么如果在函数内部声明的变量用static修饰,又会带来哪些不同呢? static变量和auto变量的不同之处在于变量的内存的回收时机。auto变量在其作用域结束时就会被系统自动回收,而static变量在变量的作用域结束时并不会被系统自动回收。

先看一段代码:

static int a = 10;

        void (^block)(void) = ^{

            NSLog(@"%d", a);
        };

        a = 20;

        block();

我们看一下打印结果:

2018-09-04 21:09:40.440020+0800 copytest[17949:499740] 20

结果是20,这个和2中的打印结果不一样,为什么局部变量从auto变成了static结果会不一样呢?我们还是从源码来分析:

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

       static int a = 10;

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

        a = 20;

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

        return 0;
    }
}

我们把代码化简一下:

static int a = 10;

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

        a = 20;

        block->FuncPtr(block);

和2不一样的是,这里传入_main_block_impl_0的是&a,也即是a这个变量的地址值。那么这个&a是赋值给谁了呢?我们上翻找到_main_block_impl_0的结构:

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

这里我们可以看到结构体多了一个指针类型的成员变量int *a,然后在构造函数中,将传递过来的&a,赋值给这个指针变量。也就是说,在_main_block_impl_0这个结构体中多了一个成员变量,这个成员变量是指针,指向a这个变量。所以当a变量的值发生变化时,能够得到最新的值。

4.变量捕获-全局变量

2和3分析了两种类型的局部变量,auto局部变量和static局部变量。这一部分则分析全局变量。全局变量会不会像局部变量一样被block所捕获呢?我们还是看一下实例:

int height = 10;
static int weight = 20;

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

        void (^block)(void) = ^{

            NSLog(@"%d %d", height, weight);
        };

        height = 30;
        weight = 40;

        block();

        return 0;
    }
}

打印结果:

2018-09-04 21:41:19.016278+0800 copytest[18774:524773] 30 40

我们还是查看一下源码:

int height = 10;
static int weight = 20;
int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

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

        height = 30;
        weight = 40;

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

        return 0;
    }
}

这里我们可以看到,height和weight这两个全局变量没有作为参数传入_main_block_impl_0中去。然后我们再查看一下_main_block_impl_0的结构:

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

可以看到,_main_block_impl_0中并没有增加成员变量。然后我们再看_main_block_func_0的结构:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {


            NSLog((NSString *)&__NSConstantStringImpl__var_folders_74_wk04zv690mz36wn0g18r5nxm0000gn_T_main_46c51b_mi_0, height, weight);
        }

可以看到,这个地方在调用的时候是直接调用的全局变量height和weight。

所以我们可以得出结论, block并不会不会全局变量。

总结:

变量类型 是否捕获到block内部 访问方式 局部变量auto 是 值传递 局部变量static 是 指针传递 全局变量 否 直接访问

思考 为什么对于不同类型的变量,block的处理方式不同呢?

这是由变量的生命周期决定的。对于自动变量,当作用域结束时,会被系统自动回收,而block很可能是在超出自动变量作用域的时候去执行,如果之前没有捕获自动变量,那么后面执行的时候,自动变量已经被回收了,得不到正确的值。对于static局部变量,它的生命周期不会因为作用域结束而结束,所以block只需要捕获这个变量的地址,在执行的时候通过这个地址去获取变量的值,这样可以获得变量的最新的值。gao'mi而对于全局变量,在任何位置都可以直接读取变量的值。

5.变量捕获-self变量

看下面一段代码:

@implementation Person

- (void)test{

    void(^block)(void) = ^{

        NSLog(@"%@", self);
    };
}

@end

这个Person类中只有一个东西,就是test这个函数,那么这个block有没有捕获self变量呢?

要搞清这个问题,我们只需要知道搞清楚这里self变量是局部变量还是全局变量,如果是局部变量,那么是一定会捕获的,而如果是全局变量,则一定不会被捕获。

我们把这个Person.m文件转化为c++的源码,然后找到test函数在c++中的表示:

static void _I_Person_test(Person * self, SEL _cmd) {

    void(*block)(void) = ((void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344));
}

我们可以看到,本来Person.m中,这个test函数我是没有传任何参数的,但是转化为c++的代码后,这里传入了两个参数,一个是self参数,一个是_cmd。self很常见,_cmd表示test函数本身。所以我们就很清楚了,self是作为参数传进来,也就是局部变量,那么block应该是捕获了self变量,事实是不是这样呢?我们只需要查看一下_Person_test_block_impl_0的结构就可以知道了。

_Person_test_block_impl_0的结构:

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

可以看到,self确实是作为成员变量被捕获了。

6.Block的类型

前面已经说过了,Block的本质就是一个OC对象,既然它是OC对象,那么它就有类型。

在搞清楚Block的类型之前,先把ARC关掉,因为ARC帮我们做了太多的事,不方便我们观察结果。关掉ARC的方法在Build Settings里面搜索Objective-C Automatic Reference Counting,把这一项置为NO。

int height = 10;
static int weight = 20;

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

        int age = 10;

        void (^block)(void) = ^{

            NSLog(@"%d %d", height, age);
        };

        NSLog(@"%@\n %@\n %@\n %@", [block class], [[block class] superclass], [[[block class] superclass] superclass], [[[[block class] superclass] superclass] superclass]);

        return 0;
    }
}

上面的代码的打印结果是:

 __NSStackBlock__
 __NSStackBlock
 NSBlock
 NSObject

这说明上面定义的这个block的类型是NSStackBlock,并且它最终继承自NSObject也说明Block的本质是OC对象。

Block有三种类型,分别是NSGlobalBlock,MallocBlock,NSStackBlock。

这三种类型的Block对象的存储区域如下:

类 对象的存储域 NSStackBlock 栈 NSGlobalBlock 程序的数据区域(.data区) NSMallocBlock 堆

截获了自动变量的Block是NSStackBlock类型,没有截获自动变量的Block则是NSGlobalStack类型,NSStackBlock类型的Block进行copy操作之后其类型变成了NSMallocBlock类型。

Block的类型 副本的配置存储域 复制效果 NSStackBlock 栈 从栈复制到堆 NSGlobalStack 程序的数据区域 什么也不做 NSMallocBlock 堆 引用计数增加

下面我们一起分析一下NSStackBlock类型的Block进行copy操作后Block对象从栈复制到了堆有什么道理,我们首先来看一段代码:

void (^block)(void);

void test() {

    int age = 10;

    block = ^{

        NSLog(@"age=%d", age);
    };
}

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

        test();

        block();

        return 0;
    }
}

不出意外的话,打印结果应该是10,那么结果是不是这样呢?我们打印看一下:

age=-411258824

很奇怪,打印了一个这么奇怪的数字。这是为什么呢?

block使用了自动变量age,所以它是NSStackBlock类型的,因此block是存放在栈区,age是被捕获作为结构体的成员变量,其值也是被保存在栈区。所以当test这个函数调用完毕后,它栈上的东西就有可能被销毁了,一旦销毁了,age值就不确定是多少了。通过打印结果也可以看到,确实是影响到了block的执行。

如果我们对block执行copy操作,结果会不会不一样呢?

void (^block)(void);

void test() {

    int age = 10;

    block = [^{

        NSLog(@"age=%d", age);
    } copy];
}

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

        test();

        block();

        return 0;
    }
}

打印结果:

age=10

这个时候得出了正确的输出。

因为对block进行copy操作后,block从栈区被复制到了堆区,它的成员变量age也随之被复制到了堆区,这样test函数执行完之后,它的栈区被销毁并不影响block,因此能得出正确的输出。

7.ARC环境下自动为Block进行copy操作的情况

6中讲的最后一个例子:

void (^block)(void);

void test() {

    int age = 10;

    block = ^{

        NSLog(@"age=%d", age);
    };
}

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

        test();

        block();

        return 0;
    }
}

这种使用方式其实非常常见,我们在使用的时候也没有发现有什么问题,那为什么在MRC环境下就有问题呢? 因为在ARC环境下编译器为我们做了很多copy操作。其中有一个规则就是如果Block被强指针指着,那么编译器就会对其进行copy操作。 我们看到这里:

^{

        NSLog(@"age=%d", age);
    };

这个Block块是被强指针指着,所以它会进行copy操作,由于其使用了自动变量,所以是栈区的Block。经过复制以后就到了堆区,这样由于Block在堆区,所以就不受Block执行完成的影响,随时可以获取age的正确值。

总结一下ARC环境下自动进行copy操作的情况一共有以下几种:

  • block作为函数返回值时。

  • 将block赋值给__strong指针时。

  • block作为Cocoa API中方法名含有usingBlock的方法参数时。

  • GCD中的API。

block作为函数返回值时

typedef void(^PDBlock)(void);

PDBlock test() {

    int age = 10;

    return ^{

        NSLog(@"age=%d", age);
    };


}

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

        PDBlock block = test();
        block();

        return 0;
    }
}

test函数的返回值是一个block,那这种情况的时候,在栈区的

^{

        NSLog(@"age=%d", age);
    };

这个block会被复制到堆区

将block赋值给强指针时

7中第一个例子就是将block赋值给强指针时,进行了copy操作的情况。

block作为Cocoa API中方法名含有usingBlock的方法参数时

比如说遍历数组的函数:

NSArray *array = [[NSArray alloc] init];
[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            NSLog(@"%d", idx);
        }];

enumerateObjectsUsingBlock: 这个函数中的block会进行copy操作

GCD中的API

GCD中的很多API的参数都有block,这个时候都会对block进行一次copy操作,比如下面这个dispatch_after函数:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

            NSLog(@"wait");
        });

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK