Block的理解与研究

前言

    一直在使用block,但殊不知道block是什么。本篇文章用以学习并记录。html


目录

  • Block的声明
  • Block的内部实现
  • Block循环引用的理解
  • block的类型,为何要用copy修饰

Block的声明

声明一个blockios

返回类型 (^名称)(形参列表) = ^(形参列表) {
    内容
}

int (^addBlock)(int, int) = ^(int a, int b) {
    return (a + b);
};

clipboard.png

执行一个blockc++

addBlock(1, 2);

Block的内部实现

1.简单block内部实现

咱们先来写一个简单的block数据结构

// main.m
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    
    void(^blockA)(void) = ^(void) {
        NSLog(@"1");
    };
    blockA();
    
    return 0;
}

经过clang的命令能够将oc代码转换为c++代码
在命令行中进入到main.m文件目录,并输入:
(ps:该命令有相关参数,可在转换时引入框架等功能,当命令执行报错时能够尝试下,参数百度能搜获得。)框架

clang -rewrite-objc main.m

同级目录下会获得一个main.cpp文件,打开它以后能够看到很是很是多的内容,上面的一堆是Foundation框架转换后的内容。咱们直接拖到最下面,能够找到咱们的main函数。异步

为方便理解,我加了注释函数

clipboard.png

1.1 咱们先来看main函数中的内容
咱们能够看到,本来main函数中写的block定义和执行的地方被转换成告终构体和函数指针的调用。blockA被转换成了2个结构体__main_block_impl_0、__main_block_desc_0,其匿名调用转换成了静态函数__main_block_func_0。
由此咱们能够得知block本质上是结构体,而block的匿名调用本质上是静态函数。源码分析

1.2 再来看blockA转换获得的结构体__main_block_impl_0学习

// __main_block_impl_0
struct __main_block_impl_0 {
  struct __block_impl impl; // __block_impl是系统的block结构体,包含block的基础信息
  struct __main_block_desc_0* Desc; // block详细信息
    
  // 构造函数
  __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,包含了block的基础信息。其定义在main.cpp中搜索一下就能够找到
struct __block_impl {
  void *isa; // 和全部oc对象同样有一个isa指针,指向block的类型
  int Flags; // 标识
  int Reserved; // 保留值
  void *FuncPtr; // 函数指针,指向block匿名调用对应的静态函数
};

从结构体的定义咱们能够知道:测试

1. __main_block_impl_0对应blockA的本体。

2. 其中__block_impl类型的变量impl包含了每一个block结构体都有的基本属性。这有点相似于面向对象的思想,相似于__main_block_impl_0继承了__block_impl。

3. __block_impl中的isa指针,表示block其实是一个OC对象。类比NSObject的isa指针,相同的是它们都指向对象的前8字节。不一样的是NSObject及派生类对象的isa指针指向Class的元类,而block的isa指针指向“block的类型”

4. __block_impl中的函数指针FuncPtr,指向block的匿名调用对应的静态函数。

5. 对于block的isa指针指向“block的类型”的解释。block的类型有三种:_NSContreteGlobalBlock、_NSContreteStackBlock、_NSContreteMallocBlock,这三种类型都是由OC中的类__NSGlobalBlock__、__NSMallocBlock__、__NSStackBlock__转换而来的,经过一个小测试咱们能够看到这三种类型。

clipboard.png

关于block的类型的区别后面会具体去解释。

6. block匿名调用对应的静态函数,其函数指针保存在FuncPtr中

1.3 那么回过来再看main函数中的内容
main函数中的两句代码为了方便理解我作了拆分。

clipboard.png

block的定义和实现其实就是:经过构造函数建立了一个__main_block_impl_0类型的结构体变量,并将其首地址赋值给函数指针blockA。
block的执行其实就是:经过blockA这个指针去执行FuncPtr指向的静态函数

扩展:
在探究过程当中发现上图中的两步强转类型有些不太对劲。

// 构造__main_block_impl_0变量
__main_block_impl_0 block_impl_0 = __main_block_impl_0(fp, desc);
// 强转类型
void(*blockA)(void) = (void (*)())&block_impl_0;
// 强转类型
__block_impl *block_impl = (__block_impl *)blockA;

其实代码合并起来至关于:
__block_impl *block_impl = &block_impl_0;

问题是:__main_block_impl_0和__block_impl的类型都不一样,为何赋值后使用却不会出错。
不是应该这样写吗?

__block_impl *block_impl = block_impl_0.impl;

后来忽然想到了缘由,这是由于内存分配顺序的缘由。由于结构体__main_block_impl_0中impl的定义是写在最前面的,因此该结构体变量在分配内存时会先从impl开始。而上述的block_impl指针指向block_impl_0的首地址,也就至关于恰好指向其中的impl。
好比:__main_block_impl_0的大小是100位,__block_impl的大小是50位,那么使用block_impl指针时操做的是__main_block_impl_0里的前50位内存地址,恰好是__main_block_impl_0里面的impl。
若是将struct __block_impl impl定义写在struct __main_block_desc_0* Desc后面则会出错。
猜想这样写的缘由多是为了代码简洁。= =!

2.block中使用外部参数状况的内部实现

首先来看一个简单的例子
clipboard.png

转换成c++代码后
clipboard.png
clipboard.png

2.1 咱们能够看到普通变量a_int和a_number在block中的传值过程:

1. 结构体__main_block_impl_0的构造函数接收变量a_int和a_number的值,且传参形式为值传递。
2. 结构体__main_block_impl_0中新增了与之对应的成员变量a_int和a_number用来保存这两个值。
3. 静态函数中使用的a_int和a_number是从新定义的局部变量,其值为结构体中保存的值。
4. 在block中使用的变量实际上是静态函数中的局部变量

2.2 经过上述的传值过程,咱们应该可以明白两个问题:

1.为何block中使用的普通外部参数,外部修改其值不会影响到block内部?
    这是由于在构造结构体__main_block_impl_0时,a_int和a_number是值传递。在外部修改a_int的值,或是修改a_number指针的指向,是不会影响到结构体中保存的值的,因此固然也没法改变在静态函数中使用时的值。

2.为何在block内部没法修改外部变量的值?
    咱们能够得知,因为是值传递,block内部a_int、a_number和外部的a_int、a_number并非同一个变量,因此在block内部是并不能获取到外部的变量的,固然也不能在block内部修改他们的值。(ps:其实即便是真的在block内部修改a_int也应该能够,只不过修改的是静态函数内部的局部变量a_int,至于为何编译器设定这样写会报错?我想多是为了便于理解,防止数据紊乱吧。)

过程以下图所示:
clipboard.png
clipboard.png

另外下图能够证明,block内部的变量和外部的变量并非同一个。
clipboard.png
clipboard.png

2.3 可是有几种状况下能够在block中修改外部变量的值

1.全局变量
2.静态变量
3.变量使用__block修饰

看看对应的实现代码

clipboard.png

转换成C++代码后:

clipboard.png
从图中能够看出,用__block修饰的变量block_int、block_number被包装成告终构体,这个结构体实际上是OC对象。

clipboard.png
从图中能够看出,blockA对应的结构体中新增了3个指针变量static_int、block_int、block_number。

clipboard.png
从图中能够看出,blockA匿名调用对应的静态函数,里面使用的block_int和block_number也都是从blockA中获取的包装变量。OC代码中在block内部修改值至关于修改该封装变量中保存的值。

clipboard.png
图中的__main_block_copy_0函数和__main_block_disopse_0是用来处理内存,相似于retain和release。__main_block_desc_0为blockA的扩展信息结构体。

clipboard.png
最后咱们在main函数中能够看到,定义一个__block修饰的变量,实际上是定义了一个包装它的结构体变量。在结构体__main_block_impl_0的构造函数传参中,做为参数传递的就是这个结构体,且传的是地址。另外静态变量static_int传的是地址,全局变量因为全局都能获取到的,因此不用传参。

经过上述的代码咱们能够知道为何这三种变量能够在block内部修改其值:

1. 全局变量因为全局均可以获取到并修改它的值,因此能够在内部进行修改。而且在block中使用不会有任何特殊处理。
2. 使用静态变量与普通变量相同的是都会在blockA结构体中有定义,不一样的地方是传递的是地址而不是值,因此它能够在内部进行修改。
3. 使用__block修饰的变量会被包装成一个对象,且传参时传递的是该包装对象的引用。所以这个变量不论是在block内部仍是在block外部都是以包装对象的形式存在,而且修改该变量的值其实修改的是包装对象内部持有的该变量的值(ps:咱们写代码只能操做该值,操做不了包装对象)。所以该变量在block内外都是同一个对象,因此能够修改。

Block循环引用的理解

经过上述的探究过程咱们能够了解到block的结构和实现以及使用各类类型变量的状况,不过通常来讲咱们也不太常会用到,可是了解这些可让咱们明白为何block会致使循环引用。

咱们先来了解一下什么是循环引用:
因为iOS系统下使用引用计数的方式来管理内存,因此一个对象是否须要被释放是由引用计数是否为0决定的,可是若是出现了A类的实例强引用B类的实例,B类的实例又强引用了A类的实例(这里我简写为:A->B->A),或者是相似A->B->C->A这种状况,这样就会出现闭环,这几个实例的引用计数都将没法变为0,所以应用运行期间也将没法被释放。这就是循环引用,它会致使内存泄漏的问题(由于有一块内存一直没法释放,而且除了他们之间相互引用的指针以外没有其余指针指向他们,因此也没办法再操做他们了)。

以下图,闭环为ak47->gamer->ak47
clipboard.png

解决循环引用的方法就不详细说了,只须要将闭环中某一个成员属性用weak修饰便可打破循环。

那么block为何会致使循环引用呢?
首先咱们知道block是一个对象,经过上面对block内部结构的研究咱们还知道,一个不加修饰的对象在block内使用时,block会定义相应的成员变量,并使用外部变量对其赋值,这就至关于block强引用持有这些对象。因此在使用block时,只要出现A->block->B->...->A这种闭环的形式,就有可能因循环引用的问题致使内存泄漏。

以下图:闭环为self -> gun -> block -> self
clipboard.png

可是以下图就不会出现循环引用,由于没有产生闭环。
持有关系为gun -> block -> self
clipboard.png

如何解决block的循环引用问题?
使用__weak打破闭环便可。
持有关系为self -> gun -> block -X> self,由于block持有弱引用指针weakself不会改变其引用计数,所以打断了block强引用持有self的关系。
clipboard.png

那么为何咱们常常会看到与weakself成对出现的strongself呢?strongself有什么做用?
由于若是是异步执行block,或者在block中有异步执行的代码,那么有可能会出如今block执行到其中某一句代码时weakself会忽然变成nil。

以下图,我将代码写到了一个控制器类中,当执行到[weakself handleFire]这句代码时我将控制器pop,控制器将会被销毁,在3秒后执行到[weakself peopleDead]这句代码时能够看到weakself变成了nil。
clipboard.png

这就会产生一个问题,明明开了枪人却没死。类比一下,好比当你作某个本地存储的功能,若是由于操做过快引发了了上述的情况,致使数据处理好了,状态也更新了,可是数据没有落库,这种问题发生也仍是蛮可怕的。

因此说strongself其实就是确保了block执行时self一直是为nil或一直有值,而不会出现前一半代码self有值,后一半代码self为nil的状况。

block的类型,为何要用copy修饰

在说block的类型以前咱们先来了解一下内存的划分:

1. 栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量的值等。操做方式相似于数据结构中的栈。
2. 堆区(heap):通常经过代码分配释放,若未释放,则程序结束时由系统回收。操做方式相似于链表。
3. 全局区(静态区static):存储全局变量和静态变量,初始化的全局变量和静态变量在一块区域(.data),未初始化的全局变量和未初始化的静态变量在相邻的另外一块区域(.bss)。程序结束后由系统释放。
4. 文字常量区:常量字符串就是放在这里的(.rodata)。程序结束后由系统释放。
5. 程序代码区:存放函数体的二进制代码(.text)。

如今来看block,在上面的探究过程当中咱们知道了block实际上是对象,其类型有三种:

1. __NSGlobalBlock__:定义在.data区,block内部未使用任何外部变量。
咱们知道在block内部使用的外部变量会在block对应的结构体中有所定义,而全局类型的block内部没有使用外部变量,不管如何执行都不依赖于执行状态,所以定义在全局区。例如

^(void) {
}

2. __NSStackBlock__:定义在栈区,使用了外部变量的block默认是建立在栈上的
3. __NSMallocBlock__:定义在堆区,当block执行copy方法时会自动从栈拷贝到堆上

那么为何block类型的成员变量须要用copy修饰呢?

1.咱们先来看一下block在MRC下的使用:

因为block默认建立在栈上(此默认的说法先不考虑全局block的状况),其生命周期即为建立时所在方法的做用域,当方法执行完以后block就会被自动释放,指向它的指针也都会变成野指针。若是想在超出block定义时的生命周期范围以外使用,那么须要执行block的copy方法将其复制到堆上。

以下图,在MRC环境下没有对block拷贝就直接返回,在block离开getABlock方法的做用域以后被释放,main函数中的block指针变为野指针,因此发生了崩溃。
clipboard.png
以下图,当执行了copy方法以后,block被拷贝到堆上,生命周期延长,程序正常执行。
clipboard.png

另外再看一个例子:
Gun类的成员变量fireBlock使用了retain修饰,在load方法中将block赋值给fireBlock时并未执行copy方法,因此咱们能够看到在赋值了以后block仍在栈上,load方法做用域结束block被释放,因此main函数中使用fireBlock就会报野指针。
clipboard.png

使用copy修饰,就不会出错了。
clipboard.png

结论:在MRC下对block类型的成员属性修饰最好用copy,而不要用retain,因为使用retain修饰只会改变引用计数而不会执行copy方法将block复制到堆上。此外block是一个对象就更不可能用assign修饰了。

(其实明白了block在内存中的存储位置和规则,若是真的要较真的话在MRC下用retain也是能够的,不过须要在block建立后调一下copy方法,若是是成员属性须要重写其set方法,并在set方法中调用copy,以达到将block复制到堆上的目的,对此我只能说何须呢)。
clipboard.png

2.咱们再来看一下block在ARC下的使用:

clipboard.png

颇有意思的是在ARC环境下,只要将block赋值就会自动拷贝到堆上。那么ARC环境下什么状况block会被copy到堆上呢?

1.执行copy方法。
2.做为方法返回值。
3.将Block赋值给非weak修饰的变量
4.做为UsingBlock或者GCD的方法入参时。(暂没法验证)

例子以下:
clipboard.png

结论:通常来讲咱们使用block都是会有赋值操做的,因为有上述条件的存在,因此基本上不会遇到在栈上的block的状况。因此在ARC环境下,block类型的成员属性使用strong或copy修饰其实均可以,可是为了延续MRC的习惯,另外避免真的出现一些奇怪问题的状况,一般仍是使用copy修饰。

总结:在MRC环境下须要用copy修饰,由于若是不对block执行copy操做,它在所在方法执行完成后会被释放。但ARC环境下因为有内部机制因此能够免去麻烦,但延续习惯也用copy修饰。


参考文章

深究Block的实现
iOS Block源码分析系列(三)————隐藏的三种Block本体以及为何要使用copy修饰符
深刻研究Block用weakSelf、strongSelf、@weakify、@strongify解决循环引用

相关文章
相关标签/搜索