面试题:
- block的原理是怎样的?本质是什么?
__block
的作用是什么?有什么使用注意点?- block的属性修饰词为什么是copy?使用block有哪些注意点?
- block在修改NSMutableArray,需不需要添加
__block
?
一、block的本质
block其实就是一个封装代码的代码块。
1 | int main(int argc, const char * argv[]) { |
1.1. block的内部实现逻辑
我们把上面的代码转换为C++看一下block被转成了什么:
1 | int main(int argc, const char * argv[]) { |
1 | // block对象的具体信息 |
block的定义:定义的block被转成了__main_block_impl_0
,它的内部有一个结构体变量__block_impl
,在__block_impl
内部有一个isa指针,用来描述block的类型信息,*FuncPtr
是一个函数指针,指向block执行逻辑的函数地址。在__main_block_impl_0
内部,指针*Desc
是__main_block_desc_0
类型指针,里面主要描述了block占用内存大小等信息。__main_block_impl_0()
是一个构造函数。
block的执行:执行block的代码被封装成结构体__main_block_func_0
。这个结构体被当做参数传给了构造函数__main_block_impl_0
,在构造函数__main_block_impl_0
的内部__main_block_func_0
被__block_impl
的指针FuncPtr
引用,最终使用这个指针进行block的调用。
因此block本质上也是一个OC对象(它内部有isa指针,函数调用地址、占用内存大小、外部变量等信息)。总结一句话就是block是封装了函数调用以及函数调用环境的OC对象。
block的底层结构如下图所示:
二、block的变量捕获
为了保证block内部能够正常访问外部的变量,block有个变量捕获机制。
知识补充:
在C语言中定义一个局部变量时,有三个关键字:
auto
、static
、register
。
auto
:局部变量在定义的时候默认是auto
,所以可以忽略不写,例:auto int age = 10;
可以直接把auto
忽略写成int age = 10;
。auto
是自动变量的意思,也就是离开当前作用域(距离最近的大括号)就会自动销毁。
static
:存放在静态区,程序结束才会释放内存。但如果使用static
修饰局部变量,仅仅是访问权限受到限制而已,只能在当前所在函数操作static
修饰的局部变量。
register
:把局部变量的值使用寄存器进行存储,和auto
一样都是随着函数调用而结束内存,平时开发时用的很少,所以不展开讨论。
捕获到block内部:在block内部会专门新增一个成员用来存储外面传入的值,这个操作叫做捕获(capture)。
2.1. auto变量(局部变量)
示例代码:
1 | int main(int argc, const char * argv[]) { |
为什么上面代码中age的输出是20?我们把代码转为C++一探究竟。
1 | int main(int argc, const char * argv[]) { |
从上面的代码中可以看到,在最开始定义block的时候,已经把age的值当做参数传入到block内部进行存储了(值传递)。即使在block外部修改age的值,也不影响block内部age的值。
通俗一点讲,由于使用的是auto修饰,所以block会先把局部变量的值保存起来,防止局部变量被随时销毁。
2.2. static变量(局部变量)
示例代码:
1 | int main(int argc, const char * argv[]) { |
转C++后的代码:
1 | int main(int argc, const char * argv[]) { |
发现使用static
修饰的局部变量,在block中使用的是地址传递(指针传递),所以在外部修改static
修饰的局部变量存储的值时会影响block内部访问时的值,因为在外部修改和在block内部访问的都是同一个变量内存地址。
由于局部变量是用
static
修饰的,存储在静态区,程序结束后才会释放,且只能在局部变量所在函数中访问,防止跨函数访问局部变量时所在函数执行结束而找不到static
修饰的局部变量,所以block内部使用的是地址传递。
2.3. 全局变量
示例代码:
1 | int age_ = 10; |
转C++后的代码:
1 | int main(int argc, const char * argv[]) { |
全局变量不管是否使用static
修饰,在block中并没有捕获全局变量的值或地址,都是直接访问变量。
全局变量存储在全局区,程序结束后才会释放内存,并且和block同处一个文件访问权限,可以随意访问,所以不需要捕获到block内部。
2.4. self
1 | @interface DBPerson : NSObject |
疑问:test
函数中的self
会不会被block捕获?
转C++代码:
1 | // test函数中block的实现 |
通过上面的代码发现,self
会被捕获,而且self
是当做局部变量被捕获到block内部的。每一个对象方法都有两个默认参数:self
和_cmd
,所以可在方法中直接使用self
进行类的实例对象操作和_cmd
打印当前方法信息。从这也可以看出,在对象方法中能够使用self
,是因为每一个方法都会传入self
和_cmd
两个默认参数,这样才能知道不同对象的self
指向的是哪个实例对象。
如果在block中访问类的成员变量,会被捕获到block内部么?
1 | - (void)test { |
转C++:
1 | // test函数中block的实现 |
发现并没有捕获_name
,而是使用self
访问的name
。因为self
已经被捕获,而且可以通过self
直接访问成员变量,所以就没有必要再次捕获成员变量。
其实在编写代码的时候,编译器已经提示我们:block已经捕获了self
变量,只需要表明现在访问的成员变量是self
的成员变量就行了。
1 | - (void)test { |