iOS

深入浅出理解Block、Block原理、Block内存管理以及为

2019-03-12  本文已影响51人  jasondevios

1.Block的类型

根据Block的内存地址,系统把Block分为3类:NSGlobalBlockNSStackBlockNSMallocBlock;

NSGlobalBlock :全局block,位于内存全局区

NSMallocBlock :堆区block,位于内存堆区 

NSStackBlock :栈区block,位于内存栈区

而block内存在哪里,主要是看block内部是否引用了局部变量,我们来看下面的输出例子

从输出的例子中我们可以看到,如果block内部没有引用局部变量,那么block就是一个全局的block,存放位置就是在全局区,如果block引用了局部变量,那么block就一个是堆区block,存放的位置是堆区。其实上面的block在声明的时候其实默认隐藏了参数,默认参数是__strong ,对应的是__weak,在ARC环境下,如果用__weak修饰的block,就会生成栈区block(NSStackBlock),当然,如果说block内部没有引用外部变量,那么它还是全局的block,比如说下面的例子:

有局部变量就有全局变量,如果说block内部引用了全局变量,那block是什么类型?我们继续看输出

由此可见,block是什么类型,主要是由两个因素决定的,一个是是否引用了局部变量,一个是是否是使用strong修饰的,ARC环境下strong修饰过的block会被拷贝到堆区,变成堆区block,所以MRC环境下有三种block,但是ARC环境下只有两种block,NSStackBlock在创建的时候就会被拷贝到堆区。

2.Block的内存管理

block的内存管理分为两类,一类是block本身的内存管理,一个是block引用的变量的内存管理,我们先来理解block本身的内存管理。

对于Block,系统专门提供了两个函数用来处理block的拷贝和释放 分别是:

Block_copy相当于的copy, Block_release相当于release,虽然有copy和release,但是不同类型的block,这两个方法会有不同的表现:

NSGlobalBlock : block的内存在全局区,使用retain,copy, release都对其无效,block依旧存在全局区,且没有释放, 使用copy和retian只是返回block的指针,引用计数器仍然是1;

NSStackBlock : block的内存在栈区,使用retain,release操作都无效,栈区block会在方法返回后将block空间回收,类似局部变量,方法结束之后就会被回收。

NSMallocBlock : block的内存在堆区,支持retian,release,但是输出block的retainCount的计数始终是1,尽管如此内存中还是会对引用计数进行管理,使用retain引用+1, release引用-1; 对于NSMallocBlock使用copy之后不会产生新的block,只是增加了一次引用,类似于使用retian。

2.Block引用外部变量

block引用外部变量,这个要说起来比较复杂,我们先来分析一下,block到底是什么,首先写一个block,然后我们通过clang (clang -rewrite-objc  file.m)进行编译,生成.cpp文件,看看里面预编译之后是什么东西

从上面的预编译文件可以看出,其实block本质上就是一个结构体,预编译的时候,首先是生成一个__main_block_impl_0这样一个结构体,这个结构体的名字其实是有规则的 main表示的是当前所在的函数,block_imp固定变量,0当前是第几个block,公式:__(当前函数名)_block_impl_(所在函数的block的位置),比如

结构体 1. __block_impl 

 这个是编译器自动生成的结构体,结构体的定义如下

struct __block_impl {

  void *isa;         //指针

 int Flags;          // 系统变量

  int Reserved;       //系统变量

  void *FuncPtr;       //函数指针,指向结构体实现函数的指针。这里指向的是__main_block_func_0

};

结构体 2. ___main_block_func_0

这个结构体是block内部的实现方法,编译器根据block的代码实现生成的全局态函数,会被赋值给 __block_impl .FuncPtr。

结构体3. __main_block_desc_0 

这个结构体是对block代码生成的结构体的描述,主要作用是获取一下__main_block_impl_0结构体的size,也就是大小。__main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)}; 这行代码的作用就是获取结构体__main_block_impl_0的大小,并且把这个变量用 __main_block_desc_0_DATA 保存下来。

结构体4 __main_block_impl_0:

编译器给我们在main函数中定义的block,void (^block)() = ^()};

生成的对应的结构体

struct __main_block_impl_0 {

struct __block_impl impl;          //__block_impl 变量impl

struct __main_block_desc_0* Desc;    //__main_block_desc_0 指针,指向编译器给我们生成的结构体变量__main_block_desc_0_DATA __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {  //结构体的构造函数

    impl.isa = &_NSConcreteStackBlock;  //说明block是栈block impl.Flags = flags;

    impl.FuncPtr = fp;

    Desc = desc;}};

总结:Block的本质其实就是一个结构体,首先生成的__main_block_impl_0结构体,用于保存结构体的大小和一个指向函数实现的指针,使用___main_block_func_0结构体来保存函数的实现。

参考资料:探索 Block (一) (手把手讲解Block 底层实现原理) - chenxianming - 博客园

上一篇下一篇

猜你喜欢

热点阅读