要了解什么是block
, 我们先写一个block
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^myBlock)(void) = ^{
NSLog(@"this is a block");
};
myBlock();
}
return 0;
}
现在我写了一个简单的block
利用
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
命令行生成编译完的C++
代码, 发现block
被编译后的样子:
这是block
的声明:
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
这是block
的调用:
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
去掉一些无用的类型转换:
void(*block)(void) = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA
);
我们发现block实际上就是一个__main_block_impl_0
函数的返回值的地址, 将地址赋值给一个名叫block
的函数指针
那么__main_block_impl_0
是什么呢?
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__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;
}
};
我们发现__main_block_impl_0
是一个结构体,
要注意的是, 还有一个__main_block_impl_0
的同名函数, 这是C++
里定义的一个结构体的构造函数, 也就是说, 这个构造函数返回的是一个结构体struct __main_block_impl_0
, 我们在上面看到的这个就是利用这个构造函数产生的一个struct __main_block_impl_0
结构体
void(*block)(void) = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA
);
-
第一个参数
__main_block_func_0
,
通过这个NSLog
就可以看出, 这是block
这里面的代码实现. -
第二个参数
&__main_block_desc_0_DATA
这相当于是一个block
的描述, 也是一个block
,
第一个成员变量是保留字段, 现在传的是0
第二个成员变量是Block_size
, 就是block
的大小. 传的就是struct __main_block_impl_0
的大小(size of
)
参数传进去了之后就是给这个结构体赋值, 让我们再来看看这个结构体
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__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;
}
};
第一个成员变量impl
, 类型是struct __block_impl
是这样的:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
相当于block
的内存布局是这样的:
struct __main_block_impl_0 {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
struct __main_block_desc_0* Desc;
};
我们发现block
也是有isa
指针的, 所以从本质上说, block
也是OC
对象
__main_block_func_0
就是函数指针, 当做参数传进去构造函数, 然后在构造函数里传给结构体里的变量FuncPtr
那我们来看一下block
的调用:
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
去掉一些类型转换
myBlock->FuncPtr(myBlock)
实际上就是找到myBlock
中保存的FuncPtr
函数指针, 然后直接调用就好了
我们再来看一下复杂一点的block
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
void (^myBlock)(void) = ^{
NSLog(@"this is a block--%d", a);
};
a = 20;
myBlock();
}
return 0;
}
然后再编译成C++
文件, 看看发生了什么:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以很清楚的看到, 这个block
包含了一个新的成员变量int a
, 这个block
的内存布局就是:
struct __main_block_impl_0 {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
struct __main_block_desc_0* Desc;
int a;
};
这个结构体的最后一个成员变量就是int a
, 看这个结构体的构造函数:
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
这个构造函数多了一个参数, 就是int _a
而创建这个block
的时候, 传进去的参数就是我定义的int a = 10
, 传进去了之后, 就把_a
的值, 赋给了结构体内部的成员变量a
,
: a(_a)
这就是C++
的语法, 将_a
赋值给a
, 所以外面的a
和里面的a
, 不是同一个东西, 外面的a, 是我定义的变量a
, 里面的a
是block
在创建的时候, 由编译器生成的成员变量a
相当于, 结构体在创建的时候, 捕获了这个外部的变量a
这就造成, 即使在
block
调用之前, 修改变量的值为20
, 也不会改变block
调用时获取的变量值, 因为block
调用的值, 是block
的成员变量的那个值.
请记住一个关键的词- Capture捕获
那么什么情况下会捕获呢?
记住以下原则:
- 局部变量会捕获
- 全局变量不会捕获
请问, 什么是局部变量, 什么是全局变量呢?
简单来说, 声明在函数内部的变量是局部变量, 声明在函数外部的变量称之为全局变量
那block
真的不会捕获全局变量吗?
好, 记住了两条大的原则:
- 局部变量会捕获
- 全局变量不会捕获
还有, 局部变量又分为auto
变量和static
变量
像这种声明之后存在于栈上的变量称之为auto
变量,auto
这个关键字是可以省略的
那么, 被static
修饰了的变量和auto
变量有什么不同呢?
放在常量区的变量有一个特点是生命周期延长了, 他的生命周期跟程序的运行周期是一致的, 只要程序没有终止, 那么常量区的数据是一直存在的. 这和栈区的数据不同, 栈区存放的数据的特点是, 只要作用域结束, 那栈区的内存就会被回收. 那我们来看看是不是这样的:
如图2的所示, 当用static
关键字修饰变量时, 当作用域结束时, 变量是不会销毁的, 当时离开作用域, 是访问不到变量的, 意思是, 虽然变量存在数据段, 但离开作用域, 无法访问变量. 这相当于变量的作用域不变, 变量的生命周期延长了.
由于这个情况, 局部变量中, auto
变量和static
变量被block
时, 处理情况是不同的:
值传递(
auto
变量)
因为auto
变量在作用域结束之后, 变量就会回收, 它的生命周期是很短的, 所以block
在捕获时, 会把auto
变量的值赋值给block
内部的同名成员变量, 这个成员变量是一个新的内存空间存储这个值指针传递(
static
变量)
但static
变量就不一样了, 它存储在数据段(常量区), 它的生命周期是跟程序的生命周期一致的, 也就是说, 在block
需要访问这个变量的时候, 我访问的仍然是这个变量本身, 那么这时候, 我捕获的变量, 就是这个变量的指针(存储这个变量的地址),block
把变量的指针赋值给block
的同名成员变量
可以很清楚地看到, 是将变量a
的地址值传到了block
的构造函数中
最终, 赋值给了block
内部的指针变量a
, 所以block
的成员变量的类型是int *
.
因此, 在调用myBlock
之前, 修改了static
变量a
的值, 在调用myBlock
时, a
的值已经改了
上面是基本数据类型的情况, 那么如果是OC
对象, block
又将如何捕获呢?
通过编译, 我们发现, 在struct __main_block_desc_0
结构体中, 多出来两个成员变量
copy
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
栈 -> 堆
dispose
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
那么这个有什么用呢? 还得从ARC
的自动copy
说起
在ARC
模式下,
在
-
block
作为函数返回值时 - 将
block
赋值给__strong
指针时(ARC
环境下, 一般默认就是__strong
指针, 除非用__weak
或者__unsafe_unretained
修饰的指针), - 或者
GCD
, - 或者函数名中有
usingBlock
的,
都会对block
进行一次copy
操作. 那么这个copy
操作有什么用呢?
我们先来看看block
的种类:
从上图可以看出, 我们把编译环境改成MRC
(这是因为ARC
环境编译器帮我们做了很多事情, 不方便我们探究本质), 然后打印这三个block
的类型, 我们发现block
是分为三种的:
-
__NSGlobalBlock__
这个block
是存在数据段的, 暂时不探究 -
__NSStackBlock__
这个是存在栈区, 也叫栈block
-
__NSMallocBlock__
这个是在堆区, 所以叫堆block
说回来copy
, 当我们对一个block
执行copy
操作时
-
__NSGlobalBlock__
还是__NSGlobalBlock__
-
__NSStackBlock__
会升级为__NSMallocBlock__
(这一点要尤其注意) -
__NSMallocBlock__
还是__NSMallocBlock__
当栈block
升级为堆block
时, 这时候堆中的数据就依靠程序员来管理了, 而不是像栈block
一样, 栈空间自动回收之后, 保存在栈中的block
就没有了. 在MRC
环境下, 如果是栈block
的话, 如下图所示
栈block
内部的person
指针只是指向person
存储的那片内存空间, 并不会对person
对象引用计数+1
, 那么当person
对象被回收后[person release]
, 再去访问栈block
中的person
指针指向的那片内存空间, 就很危险了, 就会造成野指针访问.
但如果是堆block
呢? 这时候就会对person
对象进行引用计数+1
, 那这时候再去访问堆block
中的person
指针指向的那片内存空间, 是没有问题的
但如果使用__unsafe_unretained
关键字修饰person
对象时, 会发生什么呢?
可以看到的是, 坏内存访问. 这是因为, 因为我们用__unsafe_unretained
关键字修饰了person
对象, 所以, 即使block
被拷贝到堆区, block
内部也不会对person
对象引用计数+1
, 那么当我向person
对象发送release
消息后, person
对象引用计数-1
, 这时候是会被销毁的, 此时再去访问block
内部的person
指针指向的那片内存空间, 就会造成野指针访问
在ARC
模式下,
在
-
block
作为函数返回值时 - 将
block
赋值给__strong
指针时(ARC
环境下, 一般默认就是__strong
指针, 除非用__weak
或者__unsafe_unretained
修饰的指针), - 或者
GCD
, - 或者函数名中有
usingBlock
的,
ARC
在以上四种情况下, 会自动对block
进行copy
操作, 也就是说, 这个栈block
会升级成堆block
, 升级成堆block
后, 堆block
中的person
指针会对person
对象强引用, 那么这样一来, 即使block
外面的person
指针被回收了,person
对象依然不会销毁, 它会随着block
的生命周期结束而销毁.
上面提到的例子中, 如果是局部的auto
变量, 我们其实是无法修改变量的值. 因为auto
变量的地址没有变, 假设我们要修改定义的局部变量的值, 我们需要做一件事, 就是加上__block
关键字, 那么__block
的作用是什么呢?
还是先看看编译情况:
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 18};;
简化下来就是:
__Block_byref_a_0 a = {
0,
&a,
0,
sizeof(__Block_byref_a_0),
18
};
那__Block_byref_a_0
这个又是什么东西呢?
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
可以看到的是, __block
修饰的变量, 在编译时期, 自动包装成了一个结构体(这个结构体的名字跟修饰的变量同名), 这个结构体的内存布局如上图所示.
-
0
赋值给了_isa
指针 -
&a
(结构体a
的地址)赋值给了__forwarding
指针 -
0
赋值给__flags
-
sizeof(__Block_byref_a_0)
赋值给了__size
- 最后
18
这个变量赋值给了结构体的最后一个变量a
所以原本的int a
, 包装成了一个结构体, 然后这个结构体的地址作为参数传给了block
的构造函数. block
内部有一个成员变量__Block_byref_a_0 *
相当于捕获了这个变量
但是很明显, 在MRC
模式下, 包装过后的这个结构体也存在于栈区
block
此时也是一个栈block
, 那栈的内存空间是随着作用域的结束而回收的!事实上, 我理解的是, 例如下段代码:
在31行代码的时候, age
作为一个存在于栈的变量, 它已经被系统回收了, 所以31行block
去访问age
这片内存空间的时候, 其实是很危险的. 虽然这里成功打印了age
的值, 但这么做是不合理的.
这幅图应该这么画. 也就是说, 在
MRC
情况下, 实际上并没有强弱引用的概念. 指针只是指向这这片存储空间, 并没有强指针, 弱指针的概念. 当作用域结束, 栈空间被系统回收, 指针再指向被回收的栈空间, 是一件很危险的事, 可能取到的值不正确.
即使是指针指向的是堆空间的对象, 也没有强弱指针的概念, 这就是为什么在MRC
环境下, 需要手动给引用计数+1.
那么在MRC
环境下, 需要手动将block
拷贝到堆区. 或者, 在属性修饰时, 使用copy
修饰
没有用copy
修饰
打印出来就是栈
block
使用
copy
修饰打印出来就是堆
block
当使用了copy
关键字时, block
已经升级成了堆block
, 同时, __block
修饰的变量也在堆区, 何以见得? 请看下图:
很明显,
age
这个变量已经到了堆区. 那么问题来了, block内部会对这个__block
修饰的变量有一个retain
操作吗? 我觉得应该有一个类似retain
的操作, 理由如下:__block
修饰的变量在堆区, 堆区的变量回收是程序员来决定的, 而__block
修饰的变量的生命周期是和block
一致的, 其实就相当于block
持有了__block
修饰的变量
我猜测内部的原理是这样的:
堆区的block
会调用__main_block_copy_0
方法
__main_block_copy_0
方法内部又调用了_Block_object_assign
你也可以理解为将__block
修饰的变量(此时被包装成了一个对象), 然后堆block
会持有这个对象(也就是引用计数+1).
在这一点上ARC
和MRC
是一致的, 那么不同点是什么呢?
MRC
环境下, __block
修饰的变量(包装后的对象), 这个对象内部的指针并不会持有外面的对象, 举个例子:
这里出现了坏内存访问的错误, 尽管
block
持有了 __block
修饰的变量(包装后的对象), 这个对象内部的指针并不会持有外面的对象, 也就是图中的person
对象并没有被__block
修饰的变量(包装后的对象)持有, 在MRC
环境下:
在MRC
环境下, 给person
对象发送release
消息, 引用计数-1, 对象直接销毁, 坏内存访问, 说明person1
并没有持有person
对象, 画图表示就是
但是, 在ARC
环境下, __block
修饰的变量的person
指针是会通过__Block_byref_id_object_copy
方法, 对person
对象强引用的(引用计数+1)
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
简化下来就是:
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign(dst + 40, src + 40, 131);
}
这句代码实际上就是调用_Block_object_assign
, 内部对person
对象的引用计数+1, 这样就形成了强引用.画图表示就是:
通过这幅图, 可以看出来的是, block
内部的内存管理就是:
在MRC
环境下, 执行copy
操作. 在ARC
环境下, 系统默认执行copy
操作, 此时, block
内部的person
指针会指向__block
修饰的结构体, 通过
_Block_object_assign
方法, 对__block
修饰的结构体进行一次copy
操作, 也就是引用计数+1
, 在block
执行完毕, 需要被销毁时, 执行_Block_object_dispose
方法, 对__block
修饰的结构体进行一次release
操作, 也就是引用计数-1
.此时, __block
修饰的结构体和block
一起被销毁.
不同的是, 在MRC
环境下, __block
修饰的结构体内部的_Block_object_assign
不会再对person
对象引用计数+1, _Block_object_dispose
方法也不会对person
对象引用计数-1,
, 在ARC
环境下, __block
修饰的结构体内部的_Block_object_assign
会对person
对象引用计数+1, _Block_object_dispose
方法也会对person
对象引用计数-1.
以上就是block内部的内存管理.
还有一点需要提到的是, __block
修饰的变量转化成的结构体中, __forwarding
指针是干嘛用的? 在我们之前的代码中, 看到是将结构体自己的地址传给了__forwarding
指针. 那么这个指针的值就是自己这个结构体的地址, 也就是说__forwarding
指针指向了自己, 那么为什么不直接从结构体中取值, 而是要通过一个__forwarding
指针呢?
当__block修饰的这个变量包装成的结构体还存在于堆区的时候, 现在这个地址是指向栈区的地址的, 但这本身并没有什么意义.
但当这个包装结构体被拷贝到了堆区, 此时再去访问这个变量的时候, 就会指向堆区的那个包装结构体. 也就是说, a->__forwarding->a的这个过程就是访问堆区数据的过程.
那么假设, 我想和MRC
一样, 对person
对象不进行强引用呢?
这时候就需要用到__weak
和__unsafe_unretain
关键字了. 事实上, 默认状态下, 都是相当于使用了__strong
关键字, 相当于__block
修饰的变量结构体里的那根person
指针默认就是强指针. 当使用__weak
修饰时, 被__block
修饰的变量就变成了:
编译器还爆出了警告, 让我不要这么做:
将持有的对象赋值给一个弱指针, 对象将在赋值完成后立即释放
使用__unsafe_unretained
也是差不多的效果:
__weak
和__unsafe_unretained
这两个关键字是用来解决循环引用的时候用到的. 那么什么是循环引用呢? 如下图所以:
在ARC
环境下, 被强指针指向的对象引用计数+1, 此时person
对象创建时引用计数+1, 被强指针指向时, 引用计数+1, 此时引用计数是2, block
对象创建时引用计数+1, 被person对象内部的强指针指向时, 引用计数+1, 此时引用计数是2. 那么当他们引用计数都是2时. 它们两个就都无法销毁. 此时, 必须打破这个循环
一般打破循环的方式, 就是让其中一根指针变成弱指针, 一般就是将block
内部指向对象的指针变成弱指针
一旦这跟指针变成弱指针后,
person
对象销毁后, person
对象内部的那根指针被回收, 回收后, block
对象释放.
其实被__block
修饰的变量也是同理, 它是这样形成:
而我们用__weak
修饰变量后, 形成的闭环其实是将__block
修饰的结构体里的person
变成弱指针:
那么__weak
和__unsafe_unretained
有什么区别呢?
__weak
修饰的变量, 一旦内存空间被回收, __weak
修饰的指针变量就会置为nil, 后面再访问就会直接return
, 因此它是安全的
__unsafe_unretained
修饰的变量, 一旦内存空间被回收,__unsafe_unretained
修饰的指针变量不会置为nil, 后面再访问, 是非常危险的. 有可能会造成野指针访问.
最后再探讨一个问题:
iOS block内部为什么要加__strong
?
这是为了防止当程序执行block
时, block
内部的指针指向的那块地址突然为空. 举个例子:
假设上图中, 在程序执行到第22行时, self突然为空, 如果不写
__strong typeof(self)strongSelf = weakSelf;
这行代码时, 那后面访问weakSelf
指向的地址空间时, 就可能为空. 但是当我写了__strong typeof(self)strongSelf = weakSelf;
时, 此时, 我用一个栈区的局部变量强引用了self
对象. 那self
此时的引用计数+1, 它不会被置为空. 我后面的代码就可以继续访问. 等到作用域结束, 局部变量栈空间回收, self对象的引用计数-1. 这样是不会形成循环引用的, 如下图所示iOS开发中在block中为什么要__weak和__strong配合使用
上文中举出了一个__weak
和__strong
配合使用的例子, 仅供参考