Block的理解与研究

前言

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


目录

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

Block的声明

声明一个block

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

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

clipboard.png

执行一个block

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解决循环引用


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值