要了解什么是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
,
image.png
通过这个NSLog
就可以看出, 这是block
这里面的代码实现. -
第二个参数
&__main_block_desc_0_DATA
这相当于是一个block
的描述, 也是一个block
,
第一个成员变量是保留字段, 现在传的是0
第二个成员变量是Block_size
, 就是block
的大小. 传的就是struct __main_block_impl_0
的大小(size of
)
image.png
参数传进去了之后就是给这个结构体赋值, 让我们再来看看这个结构体
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
这个关键字是可以省略的
image.png
那么, 被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
配合使用的例子, 仅供参考
网友评论