136 – iOS中的block

05. 六月 2019 iOS 0

block作为iOS中很常用的函数实现方式,但是由于闭包的特性,可以捕获上下文,所以在使用时经常面临内存管理的问题:循环引用
本文就是想对block来一个系统性的整理,然后可以带着问题去探究block。
问题1: block三种类型是什么?
问题2: block中捕获的变量是值还是指针?
问题3: self->block中引用到_property_var会引起循环引用么?
问题4: block的循环引用

也希望这些问题的思考能让我们更加理解block的机制

1.三种类型block

我们使用block有两种方式,逃逸和非逃逸(借用swift中的说法)。
非逃逸:声明的block的生命周期就是声明所在的函数体的生命周期。我们在函数体中声明一个block,这个block会在函数体结束时释放。
逃逸:声明的block生命周期和声明所在的函数体无关了。我们在函数A中声明的block,在B中也可以调用。

我们又知道block的另一个特性就是可以捕获上下文(自动变量)。

自动变量(包含局部变量,函数,参数):自动变量,出了作用域后自动释放所以排列组合,根据方式和是否捕获变量来分类,block有四种使用模式:

  1. 不逃逸未捕获自动变量
  2. 不逃逸捕获自动变量
  3. 逃逸未捕获自动变量
  4. 逃逸捕获自动变量

这四种模式,就有了三种block类型

1.NSGlobalBlock (不捕获自动变量的类型或者捕获的是静态局部变量)

此处指的不捕获自动变量,变量不包含全局变量,因为全局变量的特殊生命周期,不需要捕获,也可以在block中访问。

2.NSStackBlock (捕获了变量,没有逃逸的block)

备注:因为iOS优化了block的使用方式,当有对象指针(包括id)指向我们所创建的block时,block会自动变为MallocBlock。
所以我们使用void *指针指向创建的block

3.NSMallocBlock (捕获了自动变量,且逃逸了的block)

2.block的变量捕获

我们已经了解了三种不同类型的block。是否逃逸我们已经了解。下面我们针对block捕获自动变量的方向来认识下三种block。

NSGlobalBlock

两种:
1.不捕获变量
^(){//code here...}
这时,block里没有任何的外部变量。

2.捕获全局变量或静态变量
全局变量,静态全局变量,静态局部变量,(关于const, static变量,会在另一篇中具体理解)
因为 这三种的 变量指针本身也在 数据区(.data区),所以,任何在数据区的指针,即使被block捕获,也不会让globalblock从 数据区(.data区) 转移到 栈区

NSStackBlock 和 NSMallocBlock 对 自动变量的捕获

堆block和栈block唯一的区别是逃逸(脱离定义时的函数体生命周期),所以我们就其捕获的变量(自动变量)一起讨论

1.值捕获:捕获的变量为其指针指向的值,或基础数据类型的值
指针指向的值:

基础数据类型的值:

block中值捕获

2.地址捕获:捕获的变量为其指针本身,或指向基础数据类型的指针

3.block的使用

我们了解了对于一些block的基础信息,我们现在更深入一些,了解block的调用和实质,在block内部到底是如何捕获变量的!
使用clang命令将我们的代码转为c++代码 xcrun -sdk iphonesimulator clang -rewrite-objc main.m
源码:

 

根据源码,我们了解了block函数体内的捕获的变量实质(block结构体中会定义一个相同的指针)。
所以,使用时,我们可以清晰的分析变量未加block的是值引用,加block的是指针引用。

在block的使用时,我们需要注意的就是我们对变量的操作,什么时候使用值捕获,什么时候该用地址捕获。

4.block的逃逸

block的一个很大的优势就是它是一个函数,而且是一个可以被当作变量的函数。
你可以定一个在A类中的block,在B类中使用,这也就是block的逃逸。

但是,这也给我们很多情况下造成了不便,结合变量捕获,比如:

我们在A中某个函数定义的变量someObject,被该函数内的block捕获了。
最后,block被丢到了B类中使用,someObject的生命周期也随着block的生命周期延长到了block被释放的时候。
当这个临时变量是你想要释放的对象,却没有办法控制。

如果,当我们处理这个逃逸的block是这个临时变量的一个属性呢?

这时,objcA的引用计数是2,一个来自objA *指针,一个来自block的捕获。
即使除了当前的函数,objA *指针释放,但是引用计数还是1,无法释放,objA没有释放,aBlock指针没有释放,block的结构体中还是有个objA *指针指向objA。

这个问题就是 循环引用,我们下面要讲的。

5.block的循环引用

什么是循环引用以及循环引用的实例上节已经讲到了。
就是block中对self的强持有问题。
只要打破强持有,就能破坏循环引用的问题。

所以,
1.我们使用__weak来将被捕获的对象转化为一个临时的弱引用变量。
__weak typeof(objA) weakObjA = objA
当我们再次使用block去捕获weakObjA时,应该捕获的是__weak AClass *对象。

使用clang我们将代码转为C++
clang中使用__weak: clang -rewrite-objc -fobjc-arc -stdlib=libc++ -mmacosx-version-min=10.14 -fobjc-runtime=macosx-10.14 -Wno-deprecated-declarations main.m
备注:报UIKit的错误时,删掉所有有关UIKit的引用。

可以看到在block的结构体声明中,我们可以看到:

由此我们也能更进一步的理解,
变量的捕获值类型的捕获,可以认为是将当前指向内存的指针,完全捕获(包括当前指针的修饰关键字),并且赋值!

2.我们使用__block,经过__block的修饰,我们捕获到的是一个指针(指向捕获对象的指针)
当执行完block时,将这个指针置为nil,也就将强引用打破了。

6.self中的block循环引用

为什么不在循环引用中讲,要单独拉出来呢?
因为当涉及到self的 属性block 捕获self或者其属性时, 会直接捕获self本身…
也就回到问题上,_ivar_property和property捕获时有什么不同?
答,没有不同,因为这两种都会直接捕获self本身,但是,
两者使用的取值方式不一样

7.总结

希望可以通过梳理一遍block能够更清晰的了解。
整篇文章其实很简单,几个点:
1.block的类型定义,三种类型是 是否逃逸是否捕获自动变量 两个方式来定义的
2.block的变量捕获(会在block结构体中生成相对应的指针),两种,值捕获,地址捕获(__block声明变量)
3.循环引用,是 因为block对对象的强引用,导致相互持有无法释放
4.解决循环引用,打破block对对象的强引用即可,两种方式:__weak对象,__block对象(需在block内将变量主动置空)
5.关于系统的block,这里提一嘴,你可以把系统持有的block,当作block是一个参数传到一个函数中,但是这个函数执行的时间不确定(有可能需要一段时间,有可能瞬间执行),所以有延迟的可能会导致你的对象无法及时释放,但是是肯定会释放的。这么理解

以上就是我对于block知识部分的梳理