说过了KVO
这次我们来说一个一对一的消息传递方式,Block。
Block是什么
Block,很多语言中翻译做闭包,用《Objective-C高级编程》中的话说:
Blocks是C语言的扩充功能。可以用一句话来表示Blocks的扩充功能:带有自动变量(局部变量)的额匿名函数。
所以,Block就是一个带有自动变量的匿名函数。
匿名函数
顾名思义,匿名函数就是没有名称的函数,C语言是不允许出现没有函数名的函数的,但是因为实际上调用函数也是调用指向函数的函数指针,但是没有函数名,就没办法获取到函数的指针。那么block具体是怎么来实现的呢?我们先往下看。
自动变量
在栈上声明一个变量如果不是静态变量或全局变量,是不可以在这个栈内声明的匿名函数中使用的,但是在block中却可以。
Block结构
在Xcode里,我们敲入快捷键inlineBlock
就会看到有这样一个block的样式提供给我们。
|
|
其中第二个returnType
是我加上去的,为了能看的明显一点。
但是一下子看到一个这样的东西还是有点乱,我们把它拆分成声明部分和实现部分来看就会清楚很多了。
声明Block
|
|
声明block中包括了返回类型、^、block名称、参数列表。
实现Block
|
|
实现block中包括了返回类型(可省略)、参数列表(可省略)、实现代码。
其中返回值类型可以省略。
|
|
参数列表也可以省略。
|
|
Block的使用
因为block的传入参数和返回值都可以为空,所以Block的使用可以分为4中模式:
- 1.无参数、无返回值。
- 2.有参数、无返回值。
- 3.无参数、有返回值。
- 4.有参数、有返回值。
接下来我们就来举例子看看这几种方式的使用。
无参数、无返回值
|
|
有参数、无返回值
|
|
无参数、有返回值
|
|
有参数、有返回值
|
|
使用typedef定义
除了上边的常规操作之外,block还可以作为OC中的一个参数,这时候可以用到typedef来定义一个block,然后在函数调用时进行参数传递。
比如先定义一个block参数:
|
|
然后声明一个函数中带有此变量
|
|
这时候调用此方法,在回调的方法中就可以获取到传递过来的值。
|
|
Block与外界变量
默认情况
通常情况下,对于block外的变量引用,block默认是将其复制到block的数据结构中实现访问的,也就是说只有block中用到的变量,block才会把他自动截获进来,而且因为截取的是瞬时值,所以之后在外部改变变量的值也不会改变值得大小。因为截获自动变量会存储在block内部,所以会导致block体积变大。
另外需要注意的一点就是block内部只能调用getter方法,不可以调用setter方法,所以是没办法修改外部变量的值的。
比如:
|
|
这段代码最后会输出100,因为在定义block时,他已经把number的值复制到block中了,所以再改变他,对block中的值也不会有影响。
另外,在block中对number赋值时,编译器会直接报错。
__block
对于这种情况,OC提供了block(两个下划线_)来修饰外部变量,使用了block修饰的外部变量,block内部是复制其引用地址来实现访问数据的,所以block内部可以修改block外部的变量值。
|
|
那为什么在加了 __block修饰符之后就可以访问了呢?后边我们会详细说明,我们先往下看。
Block的循环引用
Block是很好用,但是用不好的时候就容易出现循环引用,比如在某各类将block作为自己的变量,然后又在这个block的方法中使用了这个类自己的东西,这时候两者互相持有就会发生循环引用,引起内存泄漏的问题。比如如下代码:
|
|
但是苹果也给出了相应的解决方案来处理block下的循环引用。
__weak修饰
可以直接用__weak(有两个下划线)来修饰,来打破block中的循环,使用__weak修饰解决循环引用一共有三种实现的方式。
- 使用__weak ClassName
|
|
- 使用__weak typeof(self)
|
|
- 使用Reactive Cocoa中的@weakify和@strongify
|
|
@weakify, @strongify的具体使用可以看这里
__block
在MRC下,可以直接使用__block进行修饰。
也可以先用__block修饰,然后在block方法中使用完将其设为nil,但是要注意就是block必须要被调用一次。
|
|
将self作为参数传递
也可以直接将self作为一个参数传递到block中。
|
|
Block的实现
block实际上是用C语言源码来处理的,含有block的源码首先被转换成C语言编译器能够处理的源码,再作为C进行编译。
Clang
使用LLVM编译器的clang
可以将OC的代码翻译成C++的源代码,说是C++的代码,但是实际上也就是C语言的源代码。
使用的方式就是打开Terminal,cd到源代码文件目录,输入:
|
|
比如这样一段代码(这里没有引用其他OC的框架,因为引入之后clang出来的cpp文件会巨大,有好几千行):
|
|
这段简单的block代码clang之后就会变成如下源码(这里删除了部分代码,只显示了重要的部分):
|
|
|
|
乍一看,我天,这都是什么鬼啊 = =。 没事我们一部分一部分来看。
__cself
这里边的参数__cself
就相当于C++中指向自身的变量this
,在OC中就是self
,即参数__cself
就是指向block值的变量。
__block_impl
__block_impl
是我们要介绍的第一个block中的成员变量,他是一个结构体,其结构如下:
|
|
- isa指针,所有对象都有改指针,用于实现对象相关的功能。
- Flags,用于按bit位表示一些block的附加信息。
- Reserved,保留变量。
- FuncPtr,函数指针,指向block要执行的函数,即__main_block_func_0。
__main_block_desc_0
__main_block_desc_0
是我们要介绍的第二个block中的成员变量,也是一个结构体,其结构如下:
|
|
- reserved,结构体信息保留字段。
- Block_size,block的大小。
初始化__main_block_func_0
另外一部分就是__main_block_func_0的初始化,用到的就是之前介绍的两个结构体。
|
|
实现__main_block_func_0
这里主要就是我们在block中要实现的代码。
|
|
实现main函数
另外main函数的源码在这里。
|
|
截获自动变量
看完了block的基本实现方式,那我们再看看他是如何截获自动变量呢?
我们先定义一个number
:
|
|
这时候clang之后发现:
|
|
我们看到number
被直接加到了__main_block_impl_0
结构体中。
__block
这时候我们再用__block来修饰一下number看看:
|
|
clang之后发现:
|
|
我们只是加了一个__block
,结果代码一下子增加了巨多!
这时候仔细看代码,就能发现多了一个段代码:
|
|
找到这个结构体的声明:
|
|
那如果这时候我们给number
赋一个新的值会怎么样呢?
|
|
clang后发现多了这里变化(就不贴全部代码了):
|
|
我们看到在向block变量赋值时,block的main_block_impl_0结构体实例持有指向block变量的Block_byref_number_0结构体实例的指针。
Block_byref_number_0结构体的实例的成员变量forwarding持有指向该实例自身的指针。并通过成员变量__forwarding访问成员变量val。
那么__forwarding又是什么呢?别急,后边我们再说他。
Block的存储域
在__main_block_func_0的初始化时,我们看到了有一行代码是:
|
|
从名字应该可以判断出来,这行代码的意思是这个block是存储在栈上的。
那么除了栈,block还存储在哪些地方呢?
类 | 设置对象的存储域 |
---|---|
_NSConcreteStackBlock | 栈块 |
_NSConcreteGlobalBlock | 全局块 |
_NSConcreteMallocBlock | 堆块 |
从名字就能看出来:
- 栈块存在栈内存中,超出作用域后就会马上销毁。
- 全局块在全局内存中,和全局变量一样。
- 堆块存在堆内存中,是一个带有引用计数的对象,需要自行管理内存。
那么我们怎么样能够知道block是保存在哪里呢?
全局块
一般情况下,当满足以下情况时,block为_NSConcreteGlobalBlock类对象,也就是放在全局数据区。
- 1.记录全局变量的地方有block语法时。
- 2.block语法的表达式中不使用应截获的自动变量时。
栈块
理论上,除了在全局块条件之外的情况下,block都为_NSConcreteStackBlock类对象,也就是设置在栈区。
堆块
那么如果这样说起来,岂不是没有block会在堆上了么?
这就要说到一个问题,就是ARC和MRC下block的不同情况,MRC下访问外界变量的block默认就是存储在栈中了,但是ARC下,block会自动被从栈区拷贝到堆区,然后自动释放。
那为什么ARC下,访问外部变量的block会自动从栈区拷贝到堆区呢?
block中的copy
在栈上的block,如果所在的作用域结束,block和block中的__block变量都会被废弃掉。
所以,我们需要将Block复制到堆中,延长其生命周期,这样即使是block所在的作用域结束,block还是可以在堆中继续存在。
开启了ARC时,大多数情况下编译器会恰当的判断是否有需要将block从栈复制到堆,如果有,自动生成将block从栈上复制到堆上的代码,block复制执行的是copy实例方法,只要调用了copy方法,栈块就会变成堆块,一般在如下情况时,block会自动copy到堆上。
- 1.调用Block的copy方法。
- 2.将Block作为函数返回值时(MRC下需要手动调用copy,否则无效)。
- 3.将Block赋值给__strong修改变量时(MRC时无效)。
- 4.向Cocoa框架中含有usingBlock的方法或者GCD的API传递Block参数时。
|
|
block的复制操作执行的是copy实例方法,不同类型的block使用copy方法的效果如下:
block 的类 | 副本源的配置存储域 | 复制效果 |
---|---|---|
_NSConcreteStackBlock | 栈块 | 从栈复制到堆 |
_NSConcreteGlobalBlock | 全局块 | 什么也不做 |
_NSConcreteMallocBlock | 堆块 | 引用计数增加 |
不管block配置在何处,用copy方法复制都不会引起任何问题,在不确定是调用copy即可。
|
|
__block变量的存储域
之前只说到了block,那block变量又会有什么影响呢?使用block变量的block从栈复制到堆上时,__block变量也会受到影响。
__block变量的配置存储域 | block从栈复制到堆时的影响 |
---|---|
栈 | 从栈复制到堆并被block持有 |
堆 | 被block持有 |
那么栈上的block变量复制到堆上之后,block是可以同时访问栈上的block变量和堆上的block变量,但是具体访问时到底是访问栈上的还是堆上的呢?这时候还记得我们之前说的**forwarding**变量么?
通过forwarding, 无论是在block中还是 block外访问block变量, 也不管该变量在栈上或堆上, 都能顺利地访问同一个__block变量。
Block的实践
说了这么多,我们来看看block在实际开发中比较常见的使用方法吧。
一般情况下,block会用来作为方法回调的功能,和代理的方法比较相似,处理一些比较耗时的操作比如网络数据的下载,在下载好之后直接调用block回调,返回正确或错误的信息。
block会使得代码结构紧凑,逻辑清晰,接下来我们就看一个简单的🌰:
首先我们先声明一个typedef block
|
|
然后在下载函数中传入block作为参数,并在下载结束后调用block。
|
|
最后调用这个函数:
|
|
这样,一个简单的利用block实现网络加载回调的功能就做好了。
最后
好了,这就是block的全部内容了。说是写消息传递,好像越来越跑偏了。。。
另外以上内容仅供个人学习使用,大部分内容来自《Objective-C高级编程 iOS与OS X多线程和内存管理》。如果有什么地方不对,还请大佬们多多指教。