思考:
- block 的原理是怎样的?本质是什么?
__block
的作用是什么?有什么使用注意点?- block 的属性修饰词为什么是 copy?使用 block 有哪些使用注意?
- block 在修改 NSMutableArray,需不需要添加
__block
?
基本认识
block
|
|
block 可以封装一块代码,在将来需要执行的地方通过“()”进行调用:
或者
打印结果:
block 的 C++ 代码
查看 block 的 C++ 代码。找到 main.m 文件,在终端输入:
block 的 C++ 代码(精简版):
__main_block_impl_0
__main_block_impl_0
是 block 在 C++ 中的结构体实现。第一个参数 __block_impl 中有一个 isa 指针,具备 OC 对象特征,说明 block 本质上也是一个 OC 对象。
__main_block_impl_0
省略 __block_impl
和 __main_block_desc_0
后可以看成:
即:
FuncPtr
FuncPtr 是一个指针,指向 block 封装的代码块的函数地址。
在断点1处打印 FuncPtr 地址:
|
|
在断点2处,选择 Debug -> Debug Workflow -> Always Show Disassembly:
可以看到,block 里的开始地址值 100000f00 等于 FuncPtr 的地址值。说明 block 里的代码块的地址值被保存在了 __block_impl
里的 FuncPtr 中(函数调用),另外 __main_block_impl_0
里保存了外部变量 int a(调用环境),说明 block 是封装了函数调用以及函数调用环境的 OC 对象。
block 的本质
block 本质上是封装了函数调用以及函数调用环境的 OC 对象,它内部也有个 isa 指针。
定义 block:
|
|
查看 block 的 C++ 代码:
__main_block_impl_0
、__block_impl
和 __main_block_desc_0
三者之间的关系:
有参数的 block
|
|
查看 C++ 代码,可以看到 __main_block_func_0
函数发生了变化:
小结
- block的原理是怎样的?本质是什么?
block 本质上是封装了函数调用以及函数调用环境的 OC 对象,它内部也有个 isa 指针。
变量捕获(capture)
为了保证 block 内部能够正常访问外部的变量,block 有个变量捕获机制。
变量捕获机制:block 内部会生成对应的成员变量或指针,存储被捕获变量的值或地址值。
ps:局部变量还有一个 register 变量(定义 int age = 10,尽量使用寄存器寄存变量 age)。
局部变量
auto 变量捕获
- auto 变量的作用域在当前“{}”内,离开作用域就销毁。
- auto 变量的捕获方式是值传递。
平时定义的局部变量 int age = 10 默认就是 auto 变量,auto 省略不写:
定义 block:
打印结果:
因为 auto 变量的捕获方式是值传递,即 block 捕获的是 age 的值(10),而不是 age 的地址值,所以在 block 捕获了 age 的值(10)后,再通过指针 age 修改指向的地址里的值(20),block 捕获到的值(10)不变。所以打印结果是 10。
查看 C++ 代码:
block 的结构体 __main_block_impl_0 内部新增了成员变量 age,就是用来捕获外部 auto 变量 age 用的。
static 变量捕获
- static 变量会一直保存在内存里。
- static 变量的捕获方式是指针传递。
static 声明的局部变量只初始化一次,其内存分配在静态存储区(数据区域),在程序中只有一份内存,并且在整个程序执行期间都存在不会释放。虽然 static 变量的内存不会释放,但是其作用域并没有改变。
定义 block:
查看 C++ 代码:
因为 static 变量的捕获方式是指针传递,即 block 捕获的是 height 的地址值,所以在 block 捕获了 height 地址值后,再通过指针 height 修改地址里的值(20),block 捕获到的地址里的值就是 20 了,所以打印结果是 20。
指针传递 & 值传递
|
|
因为 age 是 auto 变量,所以在 test() 执行后 age 就被被销毁了。因为 block 在执行时会访问 age,而 age 地址对应的内存已经被销毁不能被访问,所以 block 在捕获 age 时只能捕获 age 的值不能捕获 age 的地址值。因此 block 在捕获 auto 变量时采取的策略的是值传递。
因为 height 是 static 变量,会一直保存在内存里,所以 block 在执行时依然能成功访问 height 的地址。因此 block 在捕获 static 变量时采取的策略的是指针传递。
self 的捕获方式
定义 Person:
查看 Person.m 的 C++ 实现:
-(void)test 方法的 C++ 代码:
-(void)test 方法的 C++ 实现时有两个默认参数,类对象 self 和 test 方法的指针 _cmd。因为参数都是局部变量,所以作为参数出入的 self 和 _cmd 是局部变量。
block 的 C++ 代码:
因为局部变量都会被 block 捕获,所以 self 以参数的形式传入后,block 结构体 Persontest_block_impl_0 里新增了一个变量 Person *self 用来捕获 self 的地址值。
成员变量 _name 的捕获方式
|
|
block 内部调用 _name 的方式等同于 self->_name,即 block 还是先捕获 self 再通过 self->_name 获取 _name。
查看 C++ 代码:
self.name 的捕获方式
self.name 等同于 [self name],在调用时通过向捕获的 self 发送“name”消息调用,objc_msgSend(self, sel_registerName(“name”))。
全局变量
- 全局变量的内存存放在数据区域,在整个程序执行期间都存在不会释放。
- 全局变量不会被 block 捕获,而是直接访问。
定义 block:
打印结果:
查看 C++ 代码:
全局变量不会被 block 捕获,因为全局变量的内存存放在全局(静态)存储区,任何函数都可以访问,所以在 __main_block_func_0 方法执行时,不需要通过 block 获取变量,而是直接访问。
block 捕获局部变量的原因
局部变量之所以会被捕获,是因为局部变量的作用域的限制。为了防止在 block 调用时,局部变量因为超出作用域而无法访问了,block 会记住需要用到的局部变量,在调用 block 执行 __main_block_func_0 函数时,再从 block 取出局部变量:
查看 C++ 代码:
局部变量 age 和 height 的作用域是 test() 函数的“{}”内,而调用局部变量 age 和 height 是在 __test_block_func_0
函数里,为了实现跨函数调用局部变量,使用 block 捕获变量机制。在 __test_block_func_0
函数内可以通过 block 获取到被捕获的局部变量 age 的值和局部变量 height 的地址值。
block 的继承
|
|
打印结果:
从打印结果可以看出,block 的继承关系是:
block 最终继承自 NSObject,block 里的 isa 指针来自 NSObject,也说明了 block 是一个 OC 对象。
block 的类型
block 有3种类型,可以通过调用 class 方法或者 isa 指针查看具体类型,最终都是继承自 NSBlock 类型。
- __NSGlobalBlock__ ( _NSConcreteGlobalBlock )
- __NSStackBlock__ ( _NSConcreteStackBlock )
- __NSMallocBlock__ ( _NSConcreteMallocBlock )
查看 block 的类型
(ARC 环境下)定义三种类型的 block:
打印结果:
终端通过 clang 生成 C++ 代码(只贴 block 结构体):
从上面👆 C++ 代码可以看到,三个 block 的 isa 都是指向 &_NSConcreteStackBlock,即三个 block 都是 __NSStackBlock__
类型的?!通过终端命令生成的编译文件,跟运行时打印的结果不一样?!
原因:
- 因为运行时可能会在系统运行过程中修改一些内容,所以这里还是以运行时打印的结果为准。
- 通过 clang 生成的 C++ 代码,有时不一定是编译生成的代码,大致一样,细节上有区别。
三种 block 类型的内存分配
应用程序的内存分配:
编译时:
程序区域:用于存放编写的代码。
数据区域:用于存放全局变量。运行时:
堆区域:用于存放动态分配的内存,如通过 [NSObject alloc] 或者 malloc() 等方式主动申请出的内存。同时也要管理这块内存的释放工作,如 release 或 free() 等。
栈区域:用于存放局部变量,系统会负责管理这部分内存的创建和释放工作。
如图,GlobalBlock 存放在数据区域,MallocBlock 存放在堆区域,StackBlock 存放在栈区。
三种 block 类型的划分
为了保证打印结果的准确性,需要关闭 Xcode 的 ARC。build setting -> Automatic Reference Counting(NO)。
__NSGlobalBlock__
不访问变量:
|
|
打印结果:
访问 static 变量:
|
|
打印结果:
访问全局变量:
|
|
打印结果:
小结
block 在“没有访问变量”、“访问 static 变量”和“访问全局变量”的时候,都是 __NSGlobalBlock__
类型,放在数据区域。
__NSStackBlock__
访问 auto 变量:
|
|
打印结果:
上面👆的打印结果中可以看到,block 在访问 auto 变量的时候类型是 __NSStackBlock__
,放在栈区。
放在栈区的 block 会有内存销毁的问题:
打印结果:
可以看到打印出来的 age 出现异常。因为 block 是 __NSStackBlock__
类型的,放在栈区,它的作用域是 void test 方法的“{}”内部。在调用 test() 方法时,会在栈区开辟一块空间(调用栈)给 test() 函数使用,调用完成后该空间(调用栈)会被回收,这时 block 内部的数据就变成垃圾数据了。
小结
虽然 block 捕获了 auto 变量的值,但是 block 结构体的内存是在栈区的,在 test 函数调用完被销毁后,block 结构体在栈上的内存里的数据可能就变成了垃圾数据。
__NSMallocBlock__
可以通过 copy 方法将 __NSStackBlock__
类型的 block 变成 __NSMallocBlock__
类型。
__NSStackBlock__
类型的 block 在调用 copy 后,block 的类型就变成了 __NSMallocBlock__
类型。__NSMallocBlock__
类型的 block 的内存存放在堆区,由开发者手动管理内存的释放,保证了 block 内存的完整性。
block 的 copy
三种 block 类型的 copy
__NSGlobalBlock__ 的 copy
|
|
打印结果:
__NSGlobalBlock__
类型的 block 调用 copy 后还是 __NSGlobalBlock__
类型。
__NSStackBlock__ 的 copy
|
|
打印结果:
调用 copy 方法后,block 的类型从 __NSStackBlock__
类型变成了 __NSMallocBlock__
类型,block 的内存位置就从栈区拷贝到堆区,由开发者手动管理内存的释放。将 block 的内存 copy 到堆区保证了 block 内存的完整性。
__NSMallocBlock__ 的 copy
|
|
打印结果:
__NSMallocBlock__
类型的 block 调用 copy 后还是 __NSMallocBlock__
类型,引用计数+1。
小结
从内存管理的角度分析不同类型的 block 调用 copy 的不同现象:
- 数据区域的
__NSGlobalBlock__
,因为数据区域的内存在程序运行期间始终存在不会销毁,所以__NSGlobalBlock__
的内存也没必要拷贝到堆区通过引用计数的方式管理内存。 - 堆区的
__NSMallocBlock__
是通过引用计数策略被开发者管理内存的,所以在调用 copy 时要遵循引用计数管理逻辑+1。 - 栈区的
__NSStackBlock__
是系统管理内存的,离开作用域就会销毁。通过 copy 将__NSStackBlock__
类型的 block 的内存放到堆区,通过引用计数的方式管理内存,实现让开发者管理内存。
ps:类对象内存的存放位置
打印结果:
class 的内存地址跟 age 很接近,推测类对象的内存地址存放在数据段。
ARC 环境下 block 的 copy
在 ARC 环境下,编译器会根据情况自动将栈上的 block 复制到堆上,比如以下情况:
block 作为函数返回值
MRC 环境下 block 作为返回值的报错:Returning block that lives on the local stack
因为 myBlock() 方法里定义的 block 访问了 auto 变量,所以该 block 是 __NSStackBlock__
类型的,内存在栈区,作用域是在 myBlock() 方法的“{}”内。在 MRC 环境下,当 myBlock() 方法调用完成后,该 block 的内存就会被销毁。
ARC 环境下 block 作为返回值会调用 copy:
ARC 环境下打印结果:
在 ARC 环境下,^{} 在返回时返回的是 {} copy,将 block 的内存从栈区拷贝到了堆区,所以打印 block 类型的结果是 __NSMallocBlock__
。
将 block 赋值给 __strong 指针
|
|
ARC 环境下打印结果:
MRC 环境下打印结果:
因为 block 访问了 auto 变量,所以该 block 是 __NSStackBlock__
类型的,内存在栈区,作用域是在当前“{}”内。在 ARC 环境下,^{} 在赋值给 __strong
指针时,调用了 copy({} copy),将 block 的内存从栈区拷贝到了堆区,所以打印 block 类型的结果是 __NSMallocBlock__
。
反证:
ARC 环境下打印结果:
从打印结果可以看到,block 在没有被 __strong
指针指向时,其类型还是 __NSStackBlock__
类型,内存依然在栈区,说明 block 在没有 __strong 指针指向的时候不会调用 copy。
block 作为 Cocoa API 中方法名含有 usingBlock 的方法参数
|
|
block 作为 GCD API 的方法参数
GCD API 里的 block 都是在堆上的:
小结
MRC 下 block 属性的建议写法
ARC 下 block 属性的建议写法
对象类型的 auto 变量
ARC 下的“对象类型的 auto 变量”
|
|
运行到断点处的打印结果:
auto 变量 person 的作用域在当前“{}”内,在没有其它引用的情况下,离开作用域就会被销毁。
block 捕获 person 对象:
运行到断点1处没有打印结果。
运行到断点2处的打印结果:
简化代码,查看 block 与 person 的关系:
查看 block 的 C++ 代码:
可以看到 block 捕获了 person 对象。因为 person 对象是 auto 变量,所以 block 在捕获 person 对象时生成的也是 Person 类型的变量,即:
因为在 ARC 下 block 有 copy 操作,所以 block 在堆空间。堆空间的 block 在捕获 person 对象时生成的变量 Person *person 在 ARC 下是强指针,即 block 持有了 person 对象,所以在 block 销毁前,block 不会释放 person。
MRC 下的“对象类型的 auto 变量”
|
|
运行到断点处的打印结果:
因为在 MRC 下 block 没有 copy 操作,所以 block 在栈空间。在断点处 person 对象被销毁了,说明栈空间的 block 对外部变量 person 对象时弱引用。
对 block 进行 copy 操作
运行到断点处没有打印结果。
因为在 MRC 下对 block 进行 copy 操作后,block 的内存就从栈空间拷贝到了堆空间,堆空间的 block 会对 person 对象进行 retain 操作 [person retain],即 block 持有了 person 对象,所以在 block 销毁前,block 不会释放 person。
堆空间的 block 在销毁时会对 person 对象进行一次 release 操作 [person release]。
__weak
在使用 clang 转换 OC 为 C++ 代码时,如果使用了 __weak
可能会遇到以下问题:cannot create __weak reference in file using manual reference
解决方案:支持 ARC、指定运行时系统版本
即
__strong 修饰的“对象类型的 auto 变量”
|
|
断点处没有打印结果。
在 ARC 下,block 在赋值给 strong 指针时会调用 copy,block 的内存从栈区被拷贝到堆区,同时会对不会的变量进行强引用(strong),所以断点处 person 没有销毁。
查看 c++ 代码:
__weak 修饰的“对象类型的 auto 变量”
|
|
断点处的打印结果:
__weak
修饰的 person 对象,不会被 block 强引用。
查看 c++ 代码:
结合“MRC 下对’对象类型的 auto 变量’的引用”可以看出,不管 block 的 c++ 结构体里引用外部变量的是 __weak
(弱引用) 还是 __strong
(强引用),栈上的 block 对外部变量的引用都不是强引用。
copy 函数和 dispose 函数
以上面使用 __weak 修改变量的 c++ 代码为例:
__main_block_desc_0
结构体多了两个函数指针 copy 和 dispose,分别对应着 __main_block_copy_0
方法和 __main_block_dispose_0
方法。
GCD 与“对象类型的 auto 变量”
GCD 与 __strong 修饰的“对象类型的 auto 变量”
创建一个 iOS 项目测试:
打印结果:
从打印结果可以看到,在触摸事件触发后3秒,GCD 的 block 代码块打印了 person 对象,几乎是同时,person 对象被销毁了,说明 GCD 的 block 与 person 对象之间是强引用关系(__strong
)。这是因为 GCD 的 block 在 ARC 下回自动调用 copy,将内存从栈区拷贝到堆区,堆区的 block 又会调用 block 内部的 copy 函数对 person 对象根据引用类型(__strong
)进行强引用(retain)。
GCD 与 __weak 修饰的“对象类型的 auto 变量”
|
|
打印结果:
从打印结果可以看到,在触摸事件触发时,person 对象就被销毁了,3秒后 block 内部打印的 person 对象等于(null),说明 GCD 的 block 与 person 对象之间是弱引用关系(__weak
)。这是因为 GCD 的 block 在 ARC 下会自动调用 copy,将内存从栈区拷贝到堆区,堆区的 block 又会调用 block 内部的 copy 函数对 person 对象根据引用类型(__weak
)进行弱引用。
拓展:
打印结果:
1秒的定时器对 person 对象时弱引用(__weak
),2秒的定时器对 person 对象是强引用(__strong
),所以 person 对象会在2秒的 GCD 定时器执行完成后,在 block 销毁前被释放。
小结
如果 block 是在栈上,将不会对 auto 变量产生强引用
不管是 ARC 下还是 MRC 下,栈空间的 block 是不会持有“对象类型的 auto 变量”的。堆空间的 block 在 ARC 下通过__strong
(强引用)持有“对象类型的 auto 变量”。在 MRC 下,当 block 手动调用 copy 从栈区拷贝到堆区,并通过 retain 持有“对象类型的 auto 变量”,通过 release 释放“对象类型的 auto 变量”。如果 block 被拷贝到堆上,会调用 block 内部的 copy 函数,copy 函数内部会调用 _Block_object_assign 函数,_Block_object_assign 函数会根据 auto 变量的修饰符(
__strong
、__weak
、__unsafe_unretained
)做出相应的操作,形成强引用(retain)或者弱引用。如果 block 从堆上移除,会调用 block 内部的 dispose 函数,dispose 函数内部会调用 _Block_object_dispose 函数,_Block_object_dispose 函数会自动释放引用的 auto 变量(release)。
__block
__block 的本质
block 内部无法修改 auto 变量的值:
block 内部可以修改全局变量、静态变量(static)。
全局变量:
打印结果:
静态变量(static):
打印结果:
__block
可以用于解决 block 内部无法修改 auto 变量值的问题:
打印结果:
编译器会将 __block
变量包装成一个对象,查看 c++ 代码:
被 __block
修饰过的 auto 变量被包装成一个 __Block_byref_age_0
结构体。因为 __Block_byref_age_0
结构体内有 isa 指针,所以 __Block_byref_age_0
结构体是一个对象。__Block_byref_age_0
结构体的 __forwarding
指针指向自身,在 block 的执行代码里调用 __Block_byref_age_0
结构体内部参数 age 时,就是通过 __forwarding
指针调用的(age->__forwarding->age)。
外部再想访问 age 时,也会通过 __block
结构体访问 &(age.__forwarding->age),如:NSLog(@”%p”, &age):
__block
修改“对象类型的 auto 变量”
使用 __bloclk
修改“对象类型的 auto 变量” 同样会生成对应的 __Block_byref_obj_0
对象。相对于普通的 auto 变量,增加了 copy 函数和 dispose 函数用于内存管理。
__bloclk
结构体内部用于保存 auto 变量 obj 的变量 NSObject *obj,同 auto 变量的类型保持一致。
__block
不能修饰全局变量、静态变量(static)
block 内部可以使用 array 指针,但是不可以修改 array 指针:
使用 array 指针:
block 内部可以使用 NSMutableArray 指针(如:[array addObject:@”123”]),不需要添加 __block
。
修改 array 指针:
block 内部不可以修改 NSMutableArray 的指针(如:array = nil),如果需要修改 NSMutableArray 指针的话,需要添加 __block
。
__block 的内存管理
当 block 在栈上时,并不会对 __block
变量产生强引用。
当 block 被 copy 到堆时,会调用 block 内部的 copy 函数,copy 函数内部会调用 _Block_object_assign 函数,_Block_object_assign 函数会对 __block
变量形成强引用(retain)。
当 block 从堆中移除时,会调用 block 内部的 dispose 函数,dispose 函数内部会调用 _Block_object_dispose 函数,_Block_object_dispose 函数会自动释放引用的 __block
变量(release)。
对象类型的 auto 变量、__block 变量
当 block 在栈上时,对对象类型的 auto 变量、__block
变量都不会产生强引用.
当 block 拷贝到堆上时,都会通过 copy 函数来处理对象类型的 auto 变量、__block
变量。
当 block 从堆上移除时,都会通过 dispose 函数来释放对象类型的 auto 变量、__block
变量。
|
|
查看 c++ 代码:
被 __block 修饰的对象类型
ARC 下:
__block Person *person 的内存结构:
__block __weak Person *weakPerson 的内存结构:
MRC 下:
查看支持 MRC、指定运行时系统版本的 c++ 代码:
栈区的 block:
打印结果:
堆区的 block:
打印结果:
栈区和堆区的 block 在执行代码时出现同样的错误:
MRC 下栈区和堆区的 block 都不会对指向的对象产生强引���(_Block_object_assign 没有 retain 操作),内存结构:
截图👆里 __Block_byref_person_0
结构体里的 Person *person 可能是省略了 __weak
,即 Person *__weak person;
对比没有 __block
的对象类型的 auto 变量的内存结构:
小结
当 __block
变量在栈上时,不会对指向的对象产生强引用。
当 __block
变量被 copy 到堆时,会调用 __block
变量内部的 copy 函数,copy 函数内部会调用 _Block_object_assign 函数,_Block_object_assign 函数会根据所指向对象的修饰符(__strong
、__weak
、__unsafe_unretained
)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于 ARC 时会 retain,MRC 时不会 retain)。
当 __block
变量从堆上移除时,会调用 __block
变量内部的 dispose 函数,dispose 函数内部会调用 _Block_object_dispose 函数,_Block_object_dispose 函数会自动释放指向的对象(release)。
__block 的 __forwarding 指针
age.__forwarding->age:__Block_byref_obj_0
结构体对应的 age 对象通过 __forwarding
指针找到被拷贝到堆里的 __block
结构体,再找到结构体里的 age 变量。
循环引用
常见循环引用问题:
引用关系:self -> block -> self
引用关系图解:
__block
变量的循环引用问题:
引用关系:person -> block -> __block person -> person
引用关系图解:
解决循环引用问题 - ARC
解决方案一:用 __weak
、__unsafe_unretained
解决,去掉 block 对 对象的强引用关系:
__weak
:不会产生强引用,指向的对象销毁时,会自动让指针置为 nil。__unsafe_unretained
:不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变。
图解:
解决方案二:用 __block
解决(必须要调用 block),通过将捕获的变量置为 nil,去掉 __block
变量和对象之间的强引用关系:
图解:
__weak 安全问题
|
|
被 __weak
修饰的变量随时可能被释放,block 内部有可能访问的 weakSelf 已经不存在了。通过 __strong
修饰后,可以保证在 block 执行完成前 strongSelf 一直在。
解决循环引用问题 - MRC
解决方案一:用 __unsafe_unretained
解决,声明后的 person 对象在 block 里不会被 retain:
解决方案二:用 __block
解决,MRC 下的 block 不会对 __block
变量进行 retain 操作:
总结
block 的原理是怎样的?本质是什么?
block 本质上是封装了函数调用以及函数调用环境的 OC 对象。__block
的作用是什么?有什么使用注意点?
作用:__block
可以用于解决 block 内部无法修改 auto 变量值的问题。
注意:在 MRC 下__block
变量不会对指向的对象产生强引用。block 的属性修饰词为什么是 copy?使用 block 有哪些使用注意?
block 创建时内存是在栈上的,进行 copy 操作后,block 的内存就从栈上拷贝到了堆上。
堆上的 block 对捕获到的变量有强引用,需要注意 block 与被捕获的变量之间是否存在循环引用的问题。block 在修改 NSMutableArray,需不需要添加
__block
?
block 内部可以使用 NSMutableArray 指针(如:[array addObject:@”123”]),不需要添加__block
。
block 内部不可以修改 NSMutableArray 的指针(如:array = nil),如果需要修改 NSMutableArray 指针的话,需要添加__block
。