mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-24 20:00:37 +00:00
2247 lines
81 KiB
Markdown
2247 lines
81 KiB
Markdown
# block 底层原理
|
||
|
||
> 大家写 OC 肯定用过不少 block。有这样4个问题:
|
||
>
|
||
> - block 原理是什么,系统是如何实现的?
|
||
> - __block 的作用是什么?
|
||
> - block 作为属性时,为什么用 copy 修饰?
|
||
> - block 在修改 NSMutableArray 的时候,需要加 __block 吗?
|
||
>
|
||
> 带着问题探究本文。
|
||
|
||
|
||
|
||
## 一、block 本质探索
|
||
|
||
### 1. 实验探索
|
||
|
||
Demo
|
||
|
||
```objective-c
|
||
NSInteger age = 27;
|
||
void(^block)(NSInteger, NSInteger) = ^(NSInteger a, NSInteger b) {
|
||
NSLog(@"age is %zd", age);
|
||
NSLog(@"a is %zd, b is %zd", a, b);
|
||
};
|
||
block(1, 2);
|
||
```
|
||
|
||
用指令`xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转为 c++
|
||
|
||
<img src="./../assets/BlockViaClangC.png" style="zoom:25%">
|
||
|
||
`ViewController.m` 的 `viewDidLoad` 函数为 `_I_ViewController_viewDidLoad`
|
||
|
||
```c
|
||
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
|
||
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
|
||
NSInteger age = 27;
|
||
void(*block)(NSInteger, NSInteger) = ((void (*)(NSInteger, NSInteger))&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, age));
|
||
((void (*)(__block_impl *, NSInteger, NSInteger))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1, 2);
|
||
}
|
||
```
|
||
|
||
block 被定义为一个叫做 `__ViewController__viewDidLoad_block_impl_0` 的结构体
|
||
|
||
```c++
|
||
struct __ViewController__viewDidLoad_block_impl_0 {
|
||
struct __block_impl impl;
|
||
struct __ViewController__viewDidLoad_block_desc_0* Desc;
|
||
NSInteger age;
|
||
__ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, NSInteger _age, int flags=0) : age(_age) {
|
||
impl.isa = &_NSConcreteStackBlock;
|
||
impl.Flags = flags;
|
||
impl.FuncPtr = fp;
|
||
Desc = desc;
|
||
}
|
||
};
|
||
|
||
struct __block_impl {
|
||
void *isa;
|
||
int Flags;
|
||
int Reserved;
|
||
void *FuncPtr;
|
||
};
|
||
```
|
||
|
||
因为 `__block_impl` 结构体位于 `__ViewController__viewDidLoad_block_impl_0` 结构体的第一个成员,所以上述代码等价于
|
||
|
||
```c++
|
||
struct __ViewController__viewDidLoad_block_impl_0 {
|
||
void *isa;
|
||
int Flags;
|
||
int Reserved;
|
||
void *FuncPtr;
|
||
struct __ViewController__viewDidLoad_block_desc_0* Desc;
|
||
NSInteger age;
|
||
__ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, NSInteger _age, int flags=0) : age(_age) {
|
||
impl.isa = &_NSConcreteStackBlock;
|
||
impl.Flags = flags;
|
||
impl.FuncPtr = fp;
|
||
Desc = desc;
|
||
}
|
||
};
|
||
```
|
||
|
||
block 内部的 NSLog 语句,被封装为 `__ViewController__viewDidLoad_block_func_0` 结构体
|
||
|
||
```c++
|
||
static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself, NSInteger a, NSInteger b) {
|
||
NSInteger age = __cself->age; // bound by copy
|
||
|
||
NSLog((NSString *)&__NSConstantStringImpl__var_folders_7x_g921y52j5yb_w5hsn3fb3m8r0000gn_T_ViewController_95a9e6_mi_0, age);
|
||
NSLog((NSString *)&__NSConstantStringImpl__var_folders_7x_g921y52j5yb_w5hsn3fb3m8r0000gn_T_ViewController_95a9e6_mi_1, a, b);
|
||
}
|
||
|
||
__ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, NSInteger _age, int flags=0) : age(_age) {
|
||
impl.isa = &_NSConcreteStackBlock;
|
||
impl.Flags = flags;
|
||
impl.FuncPtr = fp;
|
||
Desc = desc;
|
||
}
|
||
```
|
||
|
||
可以看到构造函数 `__ViewController__viewDidLoad_block_impl_0` 有4个参数。
|
||
|
||
第一个参数:
|
||
|
||
在 `viewDidLoad` 中,`__ViewController__viewDidLoad_block_func_0` 当作构造函数 `__ViewController__viewDidLoad_block_impl_0` 的参数,传递给了参数 fp,构造函数内部将 fp 赋值给了 impl 的 FuncPtr。在
|
||
|
||
```c++
|
||
((void (*)(__block_impl *, NSInteger, NSInteger))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1, 2);
|
||
```
|
||
|
||
简化为
|
||
|
||
```c++
|
||
block->FuncPtr(block, 1, 2);
|
||
```
|
||
|
||
最后在 viewDidLoad 函数中通过结构体 impl 的成员 FuncPtr,调用了函数。
|
||
|
||
第二个参数:
|
||
|
||
`__ViewController__viewDidLoad_block_desc_0_DATA` 可以看成是一个 block 信息的描述,占用了 `sizeof(struct __ViewController__viewDidLoad_block_impl_0)` 大小的空间。
|
||
|
||
```c++
|
||
static struct __ViewController__viewDidLoad_block_desc_0 {
|
||
size_t reserved;
|
||
size_t Block_size;
|
||
} __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0)
|
||
```
|
||
|
||
|
||
|
||
QA:
|
||
|
||
```c++
|
||
((void (*)(__block_impl *, NSInteger, NSInteger))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1, 2);
|
||
```
|
||
|
||
为什么 `block->FuncPtr` 可以直接访问,而不是通过 block 先访问 impl,再访问 FuncPtr?因为 `__block_impl` 就是 `__main_block_impl_0` 这个结构体的第一个变量地址(结构体特性),然后访问第一个结构体变量内的成员变量,等价于,直接访问结构体第一个成员变量的结构体变量
|
||
|
||
举个例子
|
||
|
||
```c++
|
||
struct B { int x; };
|
||
struct A { struct B b; int y; };
|
||
|
||
struct A a;
|
||
// 以下两个指针指向同一地址:
|
||
struct B *b_ptr1 = &(a.b);
|
||
struct B *b_ptr2 = (struct B *)&a; // 强制类型转换
|
||
```
|
||
|
||
`b_ptr1` 和 `b_ptr2` 是等价的,因为 `a` 的起始地址就是 `a.b` 的起始地址。
|
||
|
||
因为 `__block_impl` 是 `__ViewController__viewDidLoad_block_impl_0` 结构起的第一个成员变量,所以 `block->FuncPtr` 会被转换为 `block->imp.FuncPtr`
|
||
|
||
结构体访问成员变量的原理就是:**基地址 + 偏移量**(baseAddress + Offset)
|
||
|
||
- `__block_impl` 的 `FuncPtr` 成员在其内部的偏移量是固定的(例如,假设 `isa` 占 8 字节,`Flags` 和 `Reserved` 各占 4 字节,则 `FuncPtr` 的偏移量为 `8 + 4 + 4 = 16` 字节)。
|
||
- 无论通过 `__main_block_impl_0` 还是 `__block_impl` 的指针访问 `FuncPtr`,最终都是基于同一基地址(`block` 的起始地址)加上相同的偏移量(16 字节)来获取值。
|
||
|
||
```c++
|
||
struct block { // baseAddress
|
||
struct first {
|
||
isa;
|
||
Flags;
|
||
FuncPtr; // offset
|
||
Desc;
|
||
}
|
||
struct second;
|
||
// ...
|
||
}
|
||
```
|
||
|
||
上述的代码,等价于 `block->impl.FuncPtr`
|
||
|
||
```c
|
||
struct __block_impl {
|
||
void *isa;
|
||
int Flags;
|
||
int Reserved;
|
||
void *FuncPtr;
|
||
};
|
||
```
|
||
|
||
类似于下面代码
|
||
|
||
```c++
|
||
struct __main_block_impl_0 {
|
||
void *isa;
|
||
int Flags;
|
||
int Reserved;
|
||
void *FuncPtr;
|
||
struct __main_block_desc_0* Desc;
|
||
int age;
|
||
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
|
||
impl.isa = &_NSConcreteStackBlock;
|
||
impl.Flags = flags;
|
||
impl.FuncPtr = fp;
|
||
Desc = desc;
|
||
}
|
||
};
|
||
```
|
||
|
||
通过 clang 转为 c++ 分析后,知道了 block 的本质,然后自定义结构体,mock 对象去承载 block 信息,然后查看
|
||
|
||
```objective-c
|
||
struct __block_impl {
|
||
void *isa;
|
||
int Flags;
|
||
int Reserved;
|
||
void *FuncPtr;
|
||
};
|
||
|
||
struct __ViewController__viewDidLoad_block_impl_0 {
|
||
struct __block_impl impl;
|
||
struct __ViewController__viewDidLoad_block_desc_0* Desc;
|
||
NSInteger age;
|
||
};
|
||
|
||
struct __ViewController__viewDidLoad_block_desc_0 {
|
||
size_t reserved;
|
||
size_t Block_size;
|
||
};
|
||
|
||
- (void)viewDidLoad {
|
||
[super viewDidLoad];
|
||
|
||
NSInteger age = 27;
|
||
void(^block)(NSInteger, NSInteger) = ^(NSInteger a, NSInteger b) {
|
||
NSLog(@"age is %zd", age);
|
||
NSLog(@"a is %zd, b is %zd", a, b);
|
||
};
|
||
block(1, 2);
|
||
|
||
struct __ViewController__viewDidLoad_block_impl_0 *mockBlock = (__bridge struct __ViewController__viewDidLoad_block_impl_0 *)block;
|
||
NSLog(@"");
|
||
}
|
||
```
|
||
|
||
<img src="./../assets/MockBlockDemo.png" style="zoom:25%">
|
||
|
||
|
||
|
||
### 2. 结论
|
||
|
||
通过探索发现:
|
||
|
||
- block 本质上就是一个 oc 对象,也有 isa 指针
|
||
|
||
- block 是封装了函数调用和函数调用环境的 OC 对象
|
||
|
||
|
||
|
||
<img src="./../assets/block-structure.png" style="zoom:40%">
|
||
|
||
|
||
|
||
|
||
|
||
## 二、block 变量捕获
|
||
|
||
### 1. auto 变量捕获
|
||
|
||
> 在 C 和 Objective-C 编程语言中,`auto` 关键字用于声明自动存储期的变量。自动存储期的变量会在定义它们的块(block)或作用域(scope)中自动创建,并在退出该作用域时自动销毁。这是变量存储期的默认行为,因此 `auto` 关键字实际上是可选的,但有时候为了清晰起见,开发者可能会显式使用它。
|
||
|
||
Demo1:
|
||
|
||
一个最简单的 block,参数和返回值都是 void,内部仅一条打印语句。
|
||
|
||
```objective-c
|
||
void(^printBlock)(void) = ^ {
|
||
NSLog(@"Hello block");
|
||
};
|
||
printBlock();
|
||
```
|
||
|
||
用 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++
|
||
|
||
<img src="./../assets/SimpleBlockExploreDemo.png" style="zoom:40%">
|
||
|
||
概括如下:
|
||
|
||
<img src="./../assets/SimpleSimpleBlockCaptureStructure.png" style="zoom:40%">
|
||
|
||
|
||
|
||
Demo2: 捕获外部变量
|
||
|
||
```objective-c
|
||
age = 27;
|
||
void(^printAgeBlock)(void) = ^ {
|
||
NSLog(@"age is %zd", age);
|
||
};
|
||
age = 28;
|
||
printAgeBlock();
|
||
// 27
|
||
```
|
||
|
||
用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++ 代码
|
||
|
||
<img src="./../assets/AutoVariableCaptureByBlock.png" style="zoom:25%">
|
||
|
||
代码分析:
|
||
|
||
- 可以看到我们编写的 block 被声明为一个 `__ViewController__viewDidLoad_block_impl_0` 类型的结构体
|
||
- 结构体内有个构造函数,见50773行代码。
|
||
- c++ 中,构造方法中 `age(_age) `的写法,表明传入的 `_age` 会被赋值给结构体内的 age
|
||
- 50794行代码,调用结构体的构造方法,传入参数。结构体构造方法内部将 参数 age 的值保存到结构体内部的 age 中。
|
||
- 因为是值传递。所以即使在 50795 行代码对 age 进行了修改,结构体内部的 age 值不变
|
||
- 所以执行 block,输出 age 依旧为27
|
||
|
||
|
||
|
||
block 内部多了一个变量来存储外部变量,这个现象叫做 block 捕获了外部变量。
|
||
|
||
c++ 中,在函数内部定义的变量,默认用 **auto** 修饰,叫做自动变量,离开作用域后自动销毁。上述 age 等价于 `auto NSInterge age = 27;`
|
||
|
||
所以上述的情况,叫做 block 的 auto 变量捕获。
|
||
|
||
|
||
|
||
### 2. static 变量捕获
|
||
|
||
```objective-c
|
||
auto NSInteger age = 27;
|
||
static NSInteger height = 175;
|
||
void(^printInfoBlock)(void) = ^ {
|
||
NSLog(@"age is %zd, height is %zd", age, height);
|
||
};
|
||
age = 28;
|
||
height = 176;
|
||
printInfoBlock();
|
||
// age is 27, height is 176
|
||
```
|
||
|
||
用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++ 代码
|
||
|
||
<img src="./../assets/StaticVariableCaptureByBlock.png" style="zoom:25%">
|
||
|
||
|
||
|
||
对代码进行分析:
|
||
|
||
- 可以看到我们编写的 block 被声明为一个 `__ViewController__viewDidLoad_block_impl_0` 类型的结构体
|
||
- 结构体内有个构造函数,见50774行代码。
|
||
- c++ 中,构造方法中 `age(_age) `的写法,表明传入的 `_age` 会被赋值给结构体内的 age,`NSInteger _age` 则 age 为值传递;`height(_height)` 写法,表明传入的 `_height` 会被复制给结构体内的 height,`NSInteger *_height` 则 height 为引用传递
|
||
- 50797行代码,调用结构体的构造方法,age 以值传递的方式传入参数,结构体构造方法内部将 参数 age 的值保存到结构体内部的 age 中。height 以引用传递的方式传入参数,结构体构造方法内,将参数 height 的引用保存起来
|
||
- 因为 age 是值传递。所以即使在 50798 行代码对 age 进行了修改,结构体内部的 age 值不变
|
||
- 因为 height 是引用传递。所以在 50799 行代码对 height 进行了修改,结构体内部的 height 值跟着改变
|
||
- 所以执行 block,输出 age 依旧为27,输出 height 的时候,根据保存地址,找到 height,也就是最新的 height 会被输出
|
||
|
||
|
||
|
||
### 3. 全局变量捕获
|
||
|
||
```objective-c
|
||
NSInteger age = 27;
|
||
static NSInteger height = 175;
|
||
|
||
@implementation ViewController
|
||
|
||
- (void)viewDidLoad {
|
||
[super viewDidLoad];
|
||
|
||
void(^printInfoBlock)(void) = ^ {
|
||
NSLog(@"age is %zd, height is %zd", age, height);
|
||
};
|
||
age = 28;
|
||
height = 176;
|
||
printInfoBlock();
|
||
}
|
||
@end
|
||
// console
|
||
age is 28, height is 176
|
||
```
|
||
|
||
用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++ 代码
|
||
|
||
<img src="./../assets/GlobalVariableWillNotCaptureByBlock.png" style="zoom:25%">
|
||
|
||
|
||
|
||
代码分析:
|
||
|
||
- 可以看到我们编写的 block 被声明为一个 `__ViewController__viewDidLoad_block_impl_0` 类型的结构体
|
||
- 结构体内有个构造函数,见50774行代码。可见针对全局变量,结构体内部不会捕获全局变量
|
||
- block 内部的指令,被封装为一个叫做 `__ViewController__viewDidLoad_block_func_0` 的结构体,打印的时候直接访问全局变量
|
||
- 针对全局变量的修改会实时生效
|
||
- 所以执行 block,输出 age 和 height 的时候,直接输出全局变量的值
|
||
|
||
|
||
|
||
QA:为什么局部变量存在捕获,全局变量不需要捕获?
|
||
|
||
全局变量到哪都可以访问,所以没必要捕获。局部变量因为作用域的问题,所以需要捕获到哪步,以便后续使用。
|
||
|
||
理解 block 的本质和意义:
|
||
|
||
- block 本质上就是一个 oc 对象,也有 isa 指针
|
||
|
||
- block 是封装了函数调用和函数调用环境的 OC 对象
|
||
|
||
|
||
|
||
### 4. 变量捕获总结
|
||
|
||
block 截获变量可以分为:
|
||
|
||
- 局部变量
|
||
- 基本数据类型:对于基本数据类型的局部变量,截获其值
|
||
- 对象类型:对于对象类型的局部变量,连同所有权修饰符一起截获
|
||
- 静态局部变量:以指针形式进行截获的
|
||
- 全局变量:不截获
|
||
- 静态全局变量:不截获
|
||
|
||
变量分为:static、auto、register。
|
||
|
||
- static:表示作为静态变量存储在数据区。
|
||
|
||
- auto:一般的变量不加修饰词则默认为 auto,auto 表示作为自动变量存储在栈上。意味着离开作用域变量会自动销毁。
|
||
|
||
- register:这个关键字告诉编译器尽可能的将变量存在CPU内部寄存器中,而不是通过内存寻址访问,以提高效率。是尽可能,不是绝对。如果定义了很多 register 变量,可能会超过CPU 的寄存器个数,超过容量。所以只是可能。
|
||
|
||
| 作用域 | 捕获到 block 内部 | 访问方式 |
|
||
| ---------- | ------------ | ---- |
|
||
| 局部变量 auto | YES | 值传递 |
|
||
| 局部变量static | YES | 指针传递 |
|
||
| 全局变量 | NO | 直接访问 |
|
||
|
||
|
||
|
||
来一个开发中常见的 case:
|
||
|
||
下面的例子中 的 block,self 会被捕获吗?
|
||
|
||
```objective-c
|
||
#import "Person.h"
|
||
|
||
@implementation Person
|
||
|
||
- (instancetype)initWithName:(NSString *)name {
|
||
if (self = [super init]) {
|
||
_name = name;
|
||
}
|
||
return self;
|
||
}
|
||
|
||
- (void)play {
|
||
void(^playBlock)(void) = ^{
|
||
NSLog(@"%@ is playing", self);
|
||
};
|
||
playBlock();
|
||
}
|
||
|
||
@end
|
||
```
|
||
|
||
用指令 `xcrun --sdk iphoneos clang -arch arm64 Person.m -rewrite-objc -o Person-arm64.cpp` 转换为 c++ 代码
|
||
|
||
<img src="./../assets/BlockWillCaptureMethodSelfVariable.png" style="zoom:25%">
|
||
|
||
|
||
|
||
代码分析:
|
||
|
||
- 可以看到我们编写的 block 被声明为一个 `__Person__play_block_impl_0` 类型的结构体
|
||
|
||
- 结构体内有个构造函数,见22893行代码。
|
||
|
||
- 因为 objective-c 的方法,默认会携带2个参数,`self` 和 ` _cmd`,等价于 `void play(Person *self, SEL _cmd)`,所以22917行代码 调用构造函数的时候,self 会被传递进去。查看 c++ 代码,可以看到 OC 的 play 方法被转换为
|
||
|
||
```c++
|
||
static void _I_Person_play(Person * self, SEL _cmd) {
|
||
void(*playBlock)(void) = ((void (*)())&__Person__play_block_impl_0((void *)__Person__play_block_func_0, &__Person__play_block_desc_0_DATA, self, 570425344));
|
||
((void (*)(__block_impl *))((__block_impl *)playBlock)->FuncPtr)((__block_impl *)playBlock);
|
||
}
|
||
```
|
||
|
||
- `play` 方法等价于
|
||
|
||
```c++
|
||
- (void)play:(id)self selector:(SEL)_cmd {
|
||
//...
|
||
}
|
||
```
|
||
|
||
所以 self 是局部变量,block 为了访问局部变量,会发生变量捕获。
|
||
|
||
- block 为了局部变量 self 的将来访问,结构体内部也增加了一个 Person 类型的 self,所以存在 self 的变量捕获。
|
||
|
||
所以,答案是会,因为 self 就是局部变量。 一个 oc 方法转换为 `void test(Person *self, SEL _cmd)` 形式,所以 self 也是局部变量,会被捕获。
|
||
|
||
Tips:判断会不会捕获的本质就是判断某个对象、变量是不是局部变量,是局部变量则会捕获,否则不会捕获。
|
||
|
||
举个例子:
|
||
|
||
```objective-c
|
||
// Person.h
|
||
@interface Person : NSObject {
|
||
@public
|
||
NSString *_hobby;
|
||
}
|
||
- (void)testBlockCapture;
|
||
@end
|
||
|
||
@implementation Person
|
||
- (void)testBlockCapture {
|
||
void (^test)(void) = ^(void) {
|
||
// NSLog(@"%@", _hobby);
|
||
};
|
||
test();
|
||
}
|
||
@end
|
||
```
|
||
|
||
利用 clang 转为 c++ `xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person.m -o Person-arm64.cpp`
|
||
|
||
```c++
|
||
struct __Person__testBlockCapture_block_impl_0 {
|
||
struct __block_impl impl;
|
||
struct __Person__testBlockCapture_block_desc_0* Desc;
|
||
__Person__testBlockCapture_block_impl_0(void *fp, struct __Person__testBlockCapture_block_desc_0 *desc, int flags=0) {
|
||
impl.isa = &_NSConcreteStackBlock;
|
||
impl.Flags = flags;
|
||
impl.FuncPtr = fp;
|
||
Desc = desc;
|
||
}
|
||
};
|
||
|
||
|
||
static void __Person__testBlockCapture_block_func_0(struct __Person__testBlockCapture_block_impl_0 *__cself) {
|
||
|
||
}
|
||
```
|
||
|
||
可以看到没有捕获任何变量。但对上述代码进行调整
|
||
|
||
```objective-c
|
||
// Person.m
|
||
- (void)testBlockCapture {
|
||
void (^test)(void) = ^(void) {
|
||
NSLog(@"%@", _hobby);
|
||
};
|
||
test();
|
||
}
|
||
|
||
```
|
||
|
||
继续调为 C++
|
||
|
||
```c++
|
||
struct __Person__testBlockCapture_block_impl_0 {
|
||
struct __block_impl impl;
|
||
struct __Person__testBlockCapture_block_desc_0* Desc;
|
||
Person *self;
|
||
__Person__testBlockCapture_block_impl_0(void *fp, struct __Person__testBlockCapture_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) {
|
||
impl.isa = &_NSConcreteStackBlock;
|
||
impl.Flags = flags;
|
||
impl.FuncPtr = fp;
|
||
Desc = desc;
|
||
}
|
||
};
|
||
|
||
|
||
static void __Person__testBlockCapture_block_func_0(struct __Person__testBlockCapture_block_impl_0 *__cself) {
|
||
Person *self = __cself->self; // bound by copy
|
||
|
||
NSLog((NSString *)&__NSConstantStringImpl__var_folders_vf_05htlrfx42qck4yh6bsnh3yr0000gn_T_Person_28fd90_mi_4, (*(NSString **)((char *)self + OBJC_IVAR_$_Person$_hobby)));
|
||
}
|
||
```
|
||
|
||
观察可以看到 Person 的 self 被捕获进去了,本质上是因为 `testBlockCapture` 方法内的 block 访问了成员变量 `_hobby`,本质上还是访问了 `self->hobby` ,也就是对局部变量 self 进行了访问,所以存在变量捕获。
|
||
|
||
|
||
|
||
## 三、block 类型
|
||
|
||
### 1. 类型划分
|
||
|
||
我们知道 block 可以看成是一个 oc 对象,所以它有类型,写个 Demo1 验证下
|
||
|
||
<img src="./../assets/BlockClassType.png" style="zoom:40%">
|
||
|
||
也就是说: `__NSGlobalBlock__` -> `NSBlock` -> `NSObject`。
|
||
|
||
继续验证,Demo2
|
||
|
||
<img src="./../assets/OCBlockType.png" style="zoom:40%">
|
||
|
||
同时利用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++
|
||
|
||
梳理分析下:
|
||
|
||
- 通过打印发现,block1 为 `__NSGlobalBlock__` 类型,但是 clang 转为 c++ 后为 `_NSConcreteStackBlock` 类型
|
||
- block1 为 `__NSMallocBlock__` 类型,但是 clang 转为 c++ 后为 `_NSConcreteStackBlock` 类型
|
||
- block2 为 `__NSStackBlock__` 类型,但是 clang 转为 c++ 后为 `_NSConcreteStackBlock` 类型
|
||
- 虽然可以用 clang 将 OC 转换为 c++ 来分析问题,但是 OC 最强大的是运行时,所以编译期转换为 c++ 看到的信息不一定是准确的,还是以运行时的信息为准
|
||
|
||
|
||
|
||
简单结论:
|
||
|
||
block 的类型可以通过 isa 或者 class 方法查看,最终都是继承自 NSBlock 类型,共存在3种类型的 block:
|
||
|
||
- `__NSGlobalBlock__` (`_NSConcreteGlobalBlock`):程序的数据区域(.data 区)
|
||
|
||
- `__NSStackBlock__` (`_NSConcreteStackBlock`),会自动销毁
|
||
|
||
- `__NSMallocBlock__`(`_NSConcreteMallockBlock`),需要程序员自己管理内存
|
||
|
||
这3种 block 在内存中的排布如下图:
|
||
|
||
<img src="./../assets/block-memorylayout.png" style="zoom:40%">
|
||
|
||
|
||
|
||
|
||
|
||
### 2. 如何判断 block 属于什么类型
|
||
|
||
Demo:
|
||
|
||
由于 ARC 默认会做一些优化,为了彻底的研究 block,我们将工程的 ARC 关掉(Build Setting 里 Automatic Reference Counting 设置为 No)
|
||
|
||
<img src="./../assets/OCStackBlockCrash.png" style="zoom:30%">
|
||
|
||
分析:
|
||
|
||
- block1 是 `__NSGlobalBlock__` ,此**类型的 block 用结构体实例的内容不依赖于执行时的状态,所以整个程序只需要1个实例。因此存放于全局变量相同的数据区域即可。**
|
||
|
||
- block2 是 `__NSStackBlock__`.
|
||
|
||
- 但为什么执行 block2 的时候发生了 crash?
|
||
|
||
猜测由于在 test 方法内给 block2 赋值,也就是在栈上定义和捕获了栈上的变量 age。等 test 方法结束,可能栈上的数据消失或者内存指向某个不可预知的地方,所以这个情况下调用 block2 会 crash。
|
||
|
||
如何解决该问题?
|
||
|
||
这类问题概括就是:`__NSStackBlock__` 栈上的 block 及其捕获的数据出了栈后,内存不稳定,将来某个时间调用 block,可能会发生 crash。需要把数据和内存稳定下来,不要交由栈自动处理。想办法把栈上的数据移动到堆上,由程序员自己管理内存。
|
||
|
||
当 `__NSStackBlock__` 调用 `copy` 方法后会变为 `__NSMallocBlock__`。如下图:
|
||
|
||
<img src="./../assets/FixStackBlockIssueWithCopy.png" style="zoom:30%">
|
||
|
||
|
||
|
||
Demo 也同时发现,当对 `__NSGlobalBlock__` 调用 copy ,不会变为 `__NSMallocBlock__` 。
|
||
|
||
|
||
|
||
### 3. 总结
|
||
|
||
| block 类型 | 环境 |
|
||
| ------------------- | ------------------------------ |
|
||
| `__NSGlobalBlock__` | 没有访问 auto 变量 |
|
||
| `__NSStackBlock__` | 访问了 auto 变量 |
|
||
| `__NSMallocBlock__` | `__NSStackBlock__` 调用了 copy 方法 |
|
||
|
||
调用 `copy` 方法
|
||
|
||
| Block 类 | 原本位置 | 复制效果 |
|
||
| --------------------------- | ------------ | ---------- |
|
||
| `__NSConcreteStackBlock__` | 栈 | 栈复制到堆 |
|
||
| `__NSConcreteGlobalBlock__` | 程序的数据段 | 什么也不做 |
|
||
| `__NSConcreteMallocBlock__` | 堆 | 引用计数+1 |
|
||
|
||
Demo
|
||
|
||
```objective-c
|
||
int age = 30;
|
||
int main(int argc, const char * argv[]) {
|
||
@autoreleasepool {
|
||
int height = 175;
|
||
NSLog(@"数据段:%p", &age); // 数据段:0x100008938
|
||
NSLog(@"栈区:%p", &height); // 栈区:0x7ff7bfeff400
|
||
NSLog(@"堆区:%p", [[NSObject alloc] init]); // 0x600000014000
|
||
NSLog(@"数据段:%p", [Person class]); // 0x1000088c8
|
||
}
|
||
}
|
||
```
|
||
|
||
可以看到:
|
||
|
||
- age 是全局变量,在数据段区
|
||
- height 是局部变量,在栈区
|
||
- `[[NSObject alloc] init]` 是 alloc 的对象,在堆区
|
||
- `[Person class]` 看到地址接近数据段,所以也在数据段区
|
||
|
||
|
||
|
||
## 四、内存管理
|
||
|
||
### 1. ARC 针对 block 的优化
|
||
|
||
#### 1. block 作为函数返回值,并且捕获了 auto 变量
|
||
|
||
MRC 下 block 作为函数的返回值是比较危险的。在方法内部,也就是栈上定义的 block,函数调用结束后可能一些相关数据就释放了,存在潜在风险。
|
||
|
||
<img src="./../assets/BlockAsFunctionReturnValueIsDangerous.png" style="zoom:30%">
|
||
|
||
|
||
|
||
|
||
|
||
MRC 下如果函数返回值是 block,且 block 内做了 auto 变量捕获的逻辑,编译器会报错:`Returning block that lives on the local stack`。此时 block 应该为 `__NSStackBlock__`
|
||
|
||
<img src="./../assets/MRCCompileFailedWhenBlockCaptureAutoVarsiableAndAsReturnValue.png" style="zoom:30%">
|
||
|
||
即使编译器不报错,也存在很大风险,因为 block 创建在栈上,函数返回后栈内存可能被回收,导致后续访问野指针。
|
||
|
||
需要:
|
||
|
||
- 复制到堆:使用 `copy` 方法将栈 block 复制到堆,延长生命周期
|
||
- 自动释放:返回堆 block 时,通常用 `autorelease` 标记,遵循 MRC 的命名规则(非 `alloc/new/copy` 方法返回自动释放对象)
|
||
|
||
改正:
|
||
|
||
```objective-c
|
||
HelloBlock generateBlock(void) {
|
||
int age = 28;
|
||
HelloBlock block = ^(void) {
|
||
NSLog(@"Hello block, age is %d", age);
|
||
};
|
||
return [[block copy] autorelease];
|
||
}
|
||
```
|
||
|
||
内存管理总结:
|
||
|
||
- 返回 block 前:务必使用 `[[block copy] autorelease]`
|
||
- 调用者:若需长期持有返回的 block,应手动 `retain` 并在不再使用时 `release`
|
||
- 命名规范:若方法名包含 `alloc/new/copy`,返回对象由调用者释放;否则返回 `autorelease` 对象。
|
||
|
||
|
||
|
||
另外的改法是,在 Build Setting 中改为 ARC,看看
|
||
|
||
<img src="./../assets/CompileWillCallCopyWhenBlockCaptureAutoVarsiableAndAsReturnValueInARC.png" style="zoom:30%">
|
||
|
||
也就是说,ARC 模式下,当 block 捕获了 auto 变量,并且作为函数返回值的时候,ARC 会自动调用 copy 方法,将 `__NSStackBlock__` 变为 `__NSMallocBlock__`
|
||
|
||
|
||
|
||
|
||
|
||
Demo1:
|
||
|
||
```objective-c
|
||
MyBlock block;
|
||
{
|
||
Person *person = [[Person alloc] init];
|
||
block = ^{
|
||
NSLog(@"block called");
|
||
};
|
||
NSLog(@"%@", [block class]);
|
||
};
|
||
```
|
||
|
||
MRC 环境: 如果 block 不访问外部局部变量,则`__NSGlobalBlock__`
|
||
|
||
ARC 环境:如果 block 不访问外部局部变量,则`__NSGlobalBlock__`
|
||
|
||
Demo2:
|
||
|
||
```objectivec
|
||
typedef void(^MyBlock)(void);
|
||
int main(int argc, const char * argv[]) {
|
||
@autoreleasepool {
|
||
MyBlock block;
|
||
{
|
||
auto Person *person = [[Person alloc] init];
|
||
person.age = 10;
|
||
block = ^{
|
||
NSLog(@"age:%zd", person.age);
|
||
};
|
||
NSLog(@"%@", [block class]); // __NSStackBlock__
|
||
};
|
||
}
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
MRC 环境下:如果访问了 auto 变量,则为 `__NSStackBlock__`
|
||
|
||
ARC 环境下:**ARC 下面比较特殊,默认局部变量对象都是强指针,存放在堆里面。所以 block 为 `__NSMallocStack__`**
|
||
|
||
Demo3:
|
||
|
||
```objectivec
|
||
typedef void(^MyBlock)(void);
|
||
int main(int argc, const char * argv[]) {
|
||
@autoreleasepool {
|
||
MyBlock block;
|
||
{
|
||
auto Person *person = [[Person alloc] init];
|
||
person.age = 10;
|
||
block = [^{
|
||
NSLog(@"age:%zd", person.age);
|
||
} copy];
|
||
NSLog(@"%@", [block class]); // __NSMallocBlock__
|
||
};
|
||
}
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
MRC 下:如果 block 调用 copy 方法,则 block 为 `__NSMallocStck__`
|
||
|
||
ARC 下:如果 block 调用 copy 方法,则 block 仍旧为 `__NSMallocBlock__`。`__NSMallocBlock__` 调用 copy 仍旧为 `__NSMallocBlock__`
|
||
|
||
在 ARC 下,如果有一个强指针引用 block,则 block 会被拷贝到堆上,成为 `__NSMallocStck`
|
||
|
||
|
||
|
||
#### 2. 将 block 赋值给强指针的时候会调用 copy(ARC 针对强指针指向的 block 会调用 copy)
|
||
|
||
MRC 下,栈内捕获了 auto 变量的 block 为 `__NSStackBlock__`
|
||
|
||
<img src="./../assets/LocalBlockInMRC.png" style="zoom:30%">
|
||
|
||
改为 ARC
|
||
|
||
<img src="./../assets/LocalBlockInARCWillCallCopy.png" style="zoom:30%">
|
||
|
||
|
||
|
||
说明:ARC 模式下,如果 block 被强指针指向,则会自动调用 copy 方法。
|
||
|
||
- 捕获了 auto 变量的 `__NSStackBlock__`,ARC 下调用 copy 会变为 `__NSMallocBlock__`
|
||
- 没有捕获变量的 `__NSGlobalBlock__`,ARC 下调用 copy 依旧为 `__NSGlobalBlock__`
|
||
|
||
#### 3. block 作为 Cocoa API 中的方法名含有 usingBlock 的方法参数时
|
||
|
||
```objective-c
|
||
NSArray *array = @[];
|
||
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
|
||
|
||
}];
|
||
```
|
||
|
||
#### 4. block 作为 GCD API 的方法参数时
|
||
|
||
```objective-c
|
||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||
|
||
});
|
||
```
|
||
|
||
|
||
|
||
#### 总结
|
||
|
||
在 ARC 下,编译器会根据情况,自动将栈上的 block 复制到堆上,比如:
|
||
|
||
- block 作为函数返回值时
|
||
- 将 block 赋值给 `__strong` 指针时
|
||
- block 传递给 Cocoa API 中名字含有 usingBlock 的方法参数时
|
||
- block 传递给 GCD 的方法参数时
|
||
|
||
ARC 下:block 对象捕获了 auto 外部变量,是一种 `__NSMallocBlock__`,捕获的对象将会在 block 销毁后销毁
|
||
|
||
<img src="./../assets/ARCObjectWillReleasedWhenLeaveScope.png" style="zoom:30%">
|
||
|
||
|
||
|
||
MRC 下:block 对象捕获了 auto 外部变量,是一种 `__NSMallocBlock__`,因为是 MRC,所以需要手动管理内存。会发现对象将在离开作用域后立马销毁,不会被 block 所捕获。
|
||
|
||
<img src="./../assets/MRCObjectCannotCaptureByMallocBlock.png" style="zoom:30%">
|
||
|
||
|
||
|
||
MRC,对 block 加 copy,变为 `__NSMallocBlock__` 呢?
|
||
|
||
<img src="./../assets/MRCObjectWillReleaseWhenBlockIsReleased.png" style="zoom:30%">
|
||
|
||
ARC 下对 block 引用的对象加 `__weak` 修饰呢?
|
||
|
||
<img src="./../assets/ARCWeakObjectWillReleaseWhenLeaveScope.png" style="zoom:30%">
|
||
|
||
用指令 `xcrun --sdk iphoneos clang -arch arm64 main.m -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 -o main-arm64.cpp` 转换为 c++ 进行分析看看。注意,因为 weak 涉及运行时,需要在 clang 后添加 runtime 参数
|
||
|
||
<img src="./../assets/WeakObjectCapturedByBlock.png" style="zoom:30%">
|
||
|
||
如果对 Person 不加 `__weak` 修饰,block 结构体内部将会是`__strong`。
|
||
|
||
<img src="./../assets/StrongObjectCapturedByBlock.png" style="zoom:30%">
|
||
|
||
|
||
|
||
思考:发现生成的 c++ 代码中,block_desc 里面多了2个成员变量。
|
||
|
||
````c++
|
||
static struct __main_block_desc_0 {
|
||
size_t reserved;
|
||
size_t Block_size;
|
||
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
|
||
void (*dispose)(struct __main_block_impl_0*);
|
||
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
|
||
````
|
||
|
||
仔细想想,查看 block 编译成 c++ 代码的源码可以发现 `__main_block_desc_0` 结构体内部是变化的。什么意思呢?reserved、Block_size 是一直有的,`void (*copy)`、`void (*dispose)` 只有在修饰对象的时候才有。为什么这么设计?
|
||
|
||
|
||
|
||
### 2. block 的 copy、dispose
|
||
|
||
```c++
|
||
static struct __main_block_desc_0 {
|
||
size_t reserved;
|
||
size_t Block_size;
|
||
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
|
||
void (*dispose)(struct __main_block_impl_0*);
|
||
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
|
||
```
|
||
|
||
因为 block 会对变量进行内存管理。`void *copy`、`void *dispose` 都是内存管理的方法。
|
||
|
||
如果 block 访问的不是对象,则结构体没有 `void *copy`、`void *dispose`
|
||
|
||
```c++
|
||
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);}
|
||
|
||
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);}
|
||
```
|
||
|
||
其中 `_Block_object_assign` 会根据要不要拥有对象,内部决定要不要给对象调用 retain 方法。
|
||
|
||
Demo1:
|
||
|
||
<img src="./../assets/ObjectWillReleaseWhenGCDTimeout.png" style="zoom:30%">
|
||
|
||
说明:ARC 环境下,GCD 的 block 会自动被拷贝到堆上 `__NSMallocBlock__`,堆上的 block 会对使用的对象进行 copy,所以 person 引用计数+1,则在 GCD block 执行完毕后才 release
|
||
|
||
Demo2
|
||
|
||
<img src="./../assets/WeakObjectWillReleaseWhenLeaveScope.png" style="zoom:30%">
|
||
|
||
- 栈空间上的 block 是不会对对象进行保命的(不管是 ARC 还是 MRC,都不调用 retain、copy 方法)。
|
||
- ARC 下, block 如果访问了 auto, static 变量,则属于 `__NSStackBlock__`,ARC 下用强指针指向,则会变为 `__NSMallocBlock__ 堆上的 block, 是会对对象进行保命的。GCD 的 block 会自动拷贝到堆上,属于 `\__NSMallocBlock__`,也会对对象进 行 copy 保命。
|
||
- 栈空间的 block 调用 copy 方法会变为堆空间的 block,会对 block 内使用的对象调用 retain、copy 方法进行保命。
|
||
|
||
马上执行了 Person 的 dealloc 方法。因为 `__weak` 修饰,block 内部的 `_Block_object_assign` 会根据 `__strong` 为对象引用计数 +1,`__weak` 则引用计数不变。所以是 `__weak` 修饰,出离作用域则立马会释放 Person 对象。
|
||
|
||
`_Block_object_assign` 会根据内存修饰符来对内存进行操作。
|
||
|
||
- `_block_object_assign` 函数会根据 auto 变量的修饰符(`__strong`、`__weak`、`__unsafe_unretained`)做出相应的操作,类似 retain(形成强、弱引用)
|
||
|
||
Demo3
|
||
|
||
<img src="./../assets/DoubleGCDMainQueueTaskObjectWillReleaseAfterTotalTime.png" style="zoom:30%">
|
||
|
||
因为 GCD 是给 MainQueue 添加任务的,所以是串行,2个任务前后按照3s、1s 后打印。由于最晚的一个任务是访问强指针对象,所以不会释放。等到 GCD 全部执行完后,Person 才释放。
|
||
|
||
Demo4
|
||
|
||
<img src="./../assets/DoubleGCDMainQueueTaskObjectWillReleaseAfterStrongReference.png" style="zoom:30%">
|
||
|
||
|
||
|
||
在给 MainQueue 提交同步任务的时候,第一个任务是一个 block,访问了强指针指向的 Person(内部会调用 `_Block_object_assign`,发现是强引用,对 p 的引用计数 +1,当 block 执行完后,调用 `_Block_object_dispose` 对 p 的引用计数 -1),第二个任务是弱指针指向的 Person,引用计数不做操作。当1s 后第一个任务执行后,Person 被释放。第二个任务执行的时候,访问 name 属性就是给 nil 发消息,不会 crash,但是为 null。
|
||
|
||
|
||
|
||
|
||
block 嵌套。多个 block 存在先后关系时
|
||
- 看看最晚的一个 block 是什么修饰的。如果是 strong,早期的是 weak,则也不会释放。
|
||
- 看看最晚的一个 block 是什么修饰的。如果是 weak,早起是 strong,则第一个 block 内部的可以正常访问,之后调用对象的 dealloc 方法,最后的 block 访问因为对象释放了,所以访问为 null
|
||
|
||
|
||
```objective-c
|
||
Person *p = [[Person alloc] init];
|
||
p.name = @"杭城小刘";
|
||
__weak Person *weakPerson = p;
|
||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||
NSLog(@"%@", weakPerson.name);
|
||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||
NSLog(@"%@", p.name);
|
||
});
|
||
});
|
||
NSLog(@"-touchesBegan:withEvent:");
|
||
|
||
2024-08-13 20:58:46.500553+0800 BlockExplore[29848:967516] -touchesBegan:withEvent:
|
||
2024-08-13 20:58:47.549486+0800 BlockExplore[29848:967516] 杭城小刘
|
||
2024-08-13 20:58:49.550015+0800 BlockExplore[29848:967516] 杭城小刘
|
||
2024-08-13 20:58:49.550315+0800 BlockExplore[29848:967516] -[Person dealloc]
|
||
|
||
Person *p = [[Person alloc] init];
|
||
p.name = @"杭城小刘";
|
||
__weak Person *weakPerson = p;
|
||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||
NSLog(@"%@", p.name);
|
||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||
NSLog(@"%@", weakPerson.name);
|
||
});
|
||
});
|
||
NSLog(@"-touchesBegan:withEvent:");
|
||
|
||
2024-08-13 20:59:51.265796+0800 BlockExplore[29889:968688] -touchesBegan:withEvent:
|
||
2024-08-13 20:59:52.313063+0800 BlockExplore[29889:968688] 杭城小刘
|
||
2024-08-13 20:59:52.313265+0800 BlockExplore[29889:968688] -[Person dealloc]
|
||
2024-08-13 20:59:54.313367+0800 BlockExplore[29889:968688] (null)
|
||
```
|
||
|
||
### 3. _Block_release 和 _Block_copy
|
||
|
||
Demo1
|
||
|
||
```objective-c
|
||
void test(void) {
|
||
|
||
int a = 0;
|
||
void(^__weak weakBlock)(void) = nil;
|
||
{
|
||
int b = 2;
|
||
void(^ __weak weakInnerBlock)(void) = ^{
|
||
NSLog(@"%d", b);
|
||
};
|
||
a = b;
|
||
weakBlock = weakInnerBlock;
|
||
}
|
||
weakBlock();
|
||
}
|
||
```
|
||
|
||
- 定义了一个局部变量 weakBlock
|
||
|
||
- 在 `{}` 内定义了一个栈上的 block **__NSStackBlock__** ,block 内访问了定义在 block 外部的 b
|
||
|
||
- 将 weakInnerBlock 赋值给 weakBlock
|
||
|
||
- 出了 `{}`,也意味着栈上的 block 作用域结束了。block 会调用 block_release 方法
|
||
|
||
- 由于出了局部作用域,栈上的 block 被释放了。所以在 `{}` 外调用 weakBlock 则存在野指针风险。编译器也会报警告:`Assigning block literal to a weak variable; object will be released after assignment` 。但可能不崩溃:栈内存虽回收但未被新数据覆盖
|
||
|
||
|
||
|
||
我们来看看 **block_release** 源码
|
||
|
||
```c++
|
||
void
|
||
_Block_release(const void *src)
|
||
{
|
||
struct StackBlockClass *self = (struct StackBlockClass *)src;
|
||
extern const void _NSConcreteStackBlock;
|
||
|
||
if (self->isa == &_NSConcreteStackBlock // 必须是栈块类型
|
||
// A Global block doesn't need to be released
|
||
&& self->flags & BLOCK_HAS_DESCRIPTOR // 必须有描述符结构
|
||
// Should always be true...
|
||
&& self->reserved > 0) // 引用计数大于0
|
||
// If false, then it's not allocated on the heap, we won't release auto memory !
|
||
{
|
||
self->reserved--;
|
||
if (self->reserved == 0)
|
||
{
|
||
if (self->flags & BLOCK_HAS_COPY_DISPOSE)
|
||
self->descriptor->dispose_helper(self);
|
||
free(self);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
说明:
|
||
|
||
- `StackBlockClass` 表示栈 block 结构,包含:
|
||
- isa:指向块类型的指针(栈块为 `__NSConcreteStackBlock`)
|
||
- flag: 标志位( BLOCK_HAS_DESCRIPTOR 表示有描述符)
|
||
- reserved:引用计数器
|
||
- descriptor:包含析构函数 dispose_helper。当 block 捕获了 OC、C++ 对象后,编译器会自动设置此标志
|
||
- 释放条件:
|
||
- 排除全局 block 和堆 block:全局block `__NSConcreteGlobalBlock` 和堆上的 block 无需释放。
|
||
- **栈块**:
|
||
存储在栈内存中,生命周期与函数作用域绑定。
|
||
**需要手动管理**:通过 `_Block_copy()` 复制到堆时,需用 `_Block_release()` 平衡引用计数。
|
||
- **堆块**:
|
||
由 `_Block_copy()` 动态分配在堆内存,受 ARC 管理。
|
||
**自动管理**:ARC 会自动插入 `objc_release()` 调用,使用标准 Objective-C 对象释放机制。
|
||
- 有效性检验:确保块结构完整且引用计数有效
|
||
- 引用计数管理:
|
||
- `self->reserved--` 引用计数减 1
|
||
- 引用计数归0的时候,调用 dispose_helper 方法。释放 block 捕获的对象、内存
|
||
- 释放 block 结构体内存
|
||
- 一般来说 block 由栈管理,但是被 copy、strong 等强引用作为属性或者参数,则会调用 **_Block_copy** 拷贝到堆上。本函数管理堆化后栈 block 的引用计数
|
||
|
||
|
||
|
||
Demo2
|
||
|
||
```c++
|
||
void test2(void) {
|
||
|
||
int a = 0;
|
||
void(^__weak weakBlock)(void) = nil;
|
||
{
|
||
int b = 2;
|
||
// 栈 block 赋值给一个 strong或copy 修饰的强引用变量,则会调用 _Block_copy 方法拷贝到堆上,变成堆 block
|
||
void(^ __strong strongInnerBlock)(void) = ^{
|
||
NSLog(@"%d", b);
|
||
};
|
||
a = b;
|
||
// 结构体赋值,也就是此时 a 是一个堆上的 block
|
||
weakBlock = strongInnerBlock;
|
||
} // 离开作用域,堆上的 block 会自动调用 _Block_release 方法,内部会 free 掉,此时再去调用释放的内存,系统机制会为已经释放的内存填充 0xDEADBEEF 标记,MMU 也会触发缺页异常,发送 crash
|
||
weakBlock();
|
||
}
|
||
```
|
||
|
||
现象:上面的代码会稳定 crash。报错:`Thread 1: EXC_BAD_ACCESS (code=1, address=0x10)`
|
||
|
||
分析:
|
||
|
||
- 在 `{}` 内定义初始化了一个栈 block
|
||
- 赋值给一个由强指针指向的 strongInnerBlock 变量时,自动触发 `_Block_copy` 方法,复制到堆上,变为堆 block **__NSConcreteMallocBlock**
|
||
- 然后通过结构体赋值的方式,赋值给 `weakBlock`
|
||
- 要出 `{}` 作用域时,则会调用 `_block_release` 方法。堆块的引用计数清零。释放后,堆内存表记为不可访问
|
||
- 此时调用 `weakBlock()` 则会 crash。访问已经释放的堆内存。系统在释放的堆内存上添加保护(如 `EXC_BAD_ACCESS`)
|
||
|
||
test 和 test2 的本质区别
|
||
|
||
| 函数 | 内存类型 | 内存回收机制 | 崩溃原因 |
|
||
| :------ | :------- | :---------------------- | :--------------- |
|
||
| `test` | 栈内存 | 系统自动回收(无保护) | 内存可能未被覆盖 |
|
||
| `test2` | 堆内存 | `free()` + 内存保护标记 | 强制触发访问异常 |
|
||
|
||
|
||
|
||
Demo3
|
||
|
||
<img src="./../assets/VisitReleasedStackBlockWillCrash.png" style="zoom:30%" />
|
||
|
||
为什么上面的代码会 crash?
|
||
|
||
- weakBlock 是一个栈 block(__NSConcreteStackBlock)
|
||
- GCD 代码以 block 的形式将延迟任务添加到主队列中
|
||
- `dispatch_block_t` 内部捕获了 block,但捕获的是原始指针,未复制 block
|
||
- GCD 不会阻塞,函数会结束,同时栈 block 会被回收,栈帧销毁。此时 `dispatch_block_t` 内持有其悬挂指针。
|
||
- 3s 后开始执行 dispatch_block_t 所指向的 weakBlock
|
||
- 发现 weakBlock 所指向的栈内存被回收,调用无效指针导致 **EXC_BAD_ACCESS** 崩溃(访问野指针)。
|
||
|
||
|
||
|
||
解决方案:
|
||
|
||
1. 将栈 block 改为堆 block。block 修饰改为 strong
|
||
|
||
````objective-c
|
||
void test3(void) {
|
||
int a = 10;
|
||
void(^__strong block)(void) = ^ {
|
||
NSLog(@"%d", a);
|
||
};
|
||
|
||
dispatch_block_t dispatch_block = ^{
|
||
block();
|
||
};
|
||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), dispatch_block);
|
||
}
|
||
````
|
||
|
||
2. 复制 block 到堆
|
||
|
||
```objective-c
|
||
void test3(void) {
|
||
int a = 10;
|
||
void(^__weak weakBlock)(void) = ^ {
|
||
NSLog(@"%d", a);
|
||
};
|
||
|
||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), [weakBlock copy]);
|
||
}
|
||
```
|
||
|
||
3. 栈帧不要回收。开启一个 RunLoop 晚退出3秒
|
||
|
||
```objective-c
|
||
void test3(void) {
|
||
int a = 10;
|
||
void(^__weak weakBlock)(void) = ^ {
|
||
NSLog(@"%d", a);
|
||
};
|
||
|
||
dispatch_block_t dispatch_block = ^{
|
||
weakBlock();
|
||
};
|
||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), dispatch_block);
|
||
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
|
||
}
|
||
```
|
||
|
||
|
||
|
||
Demo4
|
||
|
||
<img src="./../assets/BlockAssignIsValueAssign.png" style="zoom:30%" />
|
||
|
||
分析:
|
||
|
||
- block 本质就是结构体
|
||
|
||
- `void (^__strong strongBlock)(void) = weakBlock;` 看上去左侧是一个 `__strong` 修饰,其实就是结构体,所以不会像对象一样,底层调用 setter 方法。所以 block 的赋值本就是按照结构体的成员变量一个个字段简单赋值而已。
|
||
|
||
```c++
|
||
struct __block_impl {
|
||
void *isa; // 指向 Block 类 (栈上)
|
||
int flags; // 标志位 (栈上)
|
||
int reserved; // 保留字段 (栈上)
|
||
void (*invoke)(void); // 函数指针 (代码段)
|
||
struct __block_descriptor *descriptor; // 描述符指针 (栈上)
|
||
};
|
||
|
||
struct __block_descriptor {
|
||
unsigned long reserved; // 保留字段 (栈上)
|
||
unsigned long size; // Block 大小 (栈上)
|
||
void (*copy)(void); // copy 辅助函数 (代码段)
|
||
void (*dispose)(void); // dispose 辅助函数 (代码段)
|
||
};
|
||
```
|
||
|
||
- 当把结构体 block 的 invoke 设为 nil,由于是简单赋值,所以原来的 weakblock 的 invoke 也为 nil
|
||
|
||
- 所以 strongblock 的 invoke 也为 nil,所以执行 block 底层就是先判断 block 的 isa、然后根据 invoke 指针,找到代码段的函数去执行。此时已经为 nil,再去执行就会 crash
|
||
|
||
解决方案:将 weakblock copy 一下,即 `void (^__strong strongBlock)(void) = [weakBlock copy];`
|
||
|
||
|
||
|
||
### 4. 栈内存、堆内存保护机制
|
||
|
||
通过上面 test 和 test2 知道,栈内存、堆内存在释放后继续访问存在不同的表现
|
||
|
||
#### 1. 栈内存
|
||
|
||
##### 1. 栈内存的本质上是:
|
||
|
||
- **线性结构**:栈是连续的内存区域,通过栈指针(SP)管理
|
||
- **自动回收**:函数退出时,只需移动栈指针即可"释放"内存
|
||
- **物理内存不变**:移动栈指针不会立即清除数据,原内存内容保持不变
|
||
|
||
##### 2. 访问已释放的内存为何可能不崩溃?
|
||
|
||
- 无硬件保护:CPU 和 MMU(内存管理单元)不跟踪栈帧生命周期
|
||
- 内存数据保留:除非被新的栈帧覆盖,否则数据仍可被读取
|
||
|
||
##### 3. 不崩溃的深层原因:
|
||
|
||
- 无页表标记:栈内存页始终标记为:可读写 (PROT_READ|PORT_WRITE)
|
||
- 无隔离机制:不同函数的栈帧共享同一内存页
|
||
- 延迟覆盖:新栈帧写入前,元数据保持有效
|
||
|
||
|
||
|
||
|
||
|
||
#### 2. 堆内存
|
||
|
||
##### 1. 堆内存管理的核心机制:
|
||
|
||
- malloc -> 向内核申请内存页 -> 设置页表属性 -> 分配内存块
|
||
- free -> 标记内存页属性 -> 加入空闲链表 -> 触发内存页回收
|
||
|
||
##### 2. free 后的关键操作
|
||
|
||
内存标记
|
||
|
||
````objective-c
|
||
// 典型内存分配器实现
|
||
void free(void *ptr) {
|
||
// 1. 在内存块头部写入魔数(如0xDEADBEEF)
|
||
*(uint32_t*)(ptr - 4) = 0xDEADBEEF;
|
||
|
||
// 2. 通过 mprotect 设置内存页不可访问
|
||
mprotect(ALIGN_PAGE(ptr), PAGE_SIZE, PROT_NONE);
|
||
}
|
||
````
|
||
|
||
页表更新
|
||
|
||
| 状态 | 页表项标志位 | 访问后果 |
|
||
| :--- | :-------------- | :----------- |
|
||
| 正常 | PRESENT+RW+USER | 允许访问 |
|
||
| 释放 | ~PRESENT | 触发缺页异常 |
|
||
|
||
稳定崩溃的硬件基础:
|
||
|
||
MMU 介入:当访问 `PROT_NONE` 内存页时:
|
||
|
||
1. 触发缺页一场(Page Fault)
|
||
2. 内核检查异常地址
|
||
3. 发送 `SIGSEGV` 信号
|
||
4. 进程终止(崩溃)
|
||
|
||
#### 3. iOS/Macos 优化
|
||
|
||
##### 1. 堆内存保护优化
|
||
|
||
- Malloc Scribble:释放后填充`0x55`(调试模式默认启用)
|
||
|
||
- Zone-based Protection:
|
||
|
||
```objective-c
|
||
malloc_zone_t *zone = malloc_create_zone(0, 0);
|
||
malloc_set_zone_name(zone, "Protected Zone");
|
||
malloc_zone_protect(zone, 1); // 启用保护
|
||
```
|
||
|
||
##### 2. 栈内存的刻意放松
|
||
|
||
- **性能优先**:避免栈操作时的权限检查
|
||
- **安全边界**:仅防止栈溢出,不保护栈帧间访问
|
||
|
||
|
||
|
||
### 5. block 如何修改变量
|
||
|
||
#### 1. __block 修饰基本数据类型
|
||
|
||
```objectivec
|
||
typedef void(^MyBlock)(void);
|
||
int main(int argc, const char * argv[]) {
|
||
@autoreleasepool {
|
||
int age = 27;
|
||
MyBlock block = ^{
|
||
age = 28;
|
||
};
|
||
NSLog(@"%zd", age);
|
||
}
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
编译会报错 `// Variable is not assignable (missing __block type specifier)` 为什么不能修改?
|
||
|
||
把 block 内修改的那行代码注释了,转成 c++ 看看
|
||
|
||
```c
|
||
struct __ViewController__viewDidLoad_block_impl_0 {
|
||
struct __block_impl impl;
|
||
struct __ViewController__viewDidLoad_block_desc_0* Desc;
|
||
int age;
|
||
__ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
|
||
impl.isa = &_NSConcreteStackBlock;
|
||
impl.Flags = flags;
|
||
impl.FuncPtr = fp;
|
||
Desc = desc;
|
||
}
|
||
};
|
||
static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself) {
|
||
int age = __cself->age; // bound by copy
|
||
|
||
NSLog((NSString *)&__NSConstantStringImpl__var_folders_7x_g921y52j5yb_w5hsn3fb3m8r0000gn_T_ViewController_3ceae4_mi_0, age);
|
||
}
|
||
|
||
static struct __ViewController__viewDidLoad_block_desc_0 {
|
||
size_t reserved;
|
||
size_t Block_size;
|
||
} __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0)};
|
||
|
||
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
|
||
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
|
||
int age = 27;
|
||
void(*ChangeValueBlock)(void) = ((void (*)())&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, age));
|
||
}
|
||
```
|
||
|
||
可以看到, block 内部的逻辑被包装成一个新的 `__ViewController__viewDidLoad_block_func_0`方法,然而 age 定义在 `_I_ViewController_viewDidLoad` 方法中。没有传递引用,也没任何特殊处理,所以没办法修改。
|
||
|
||
|
||
|
||
思考:那如何实现修改一个变量?
|
||
|
||
全局变量、static 变量、`__block`修饰的变量在 block 内部可以修改。
|
||
|
||
- `__block` 用于解决 block 内部无法修改 auto 变量的问题。
|
||
|
||
- `__block` 不能修饰 static、全局变量
|
||
|
||
- 编译器会将 `__block` 修饰的变量包装为一个对象(后续修改则通过指针找到结构体对象,结构体对象再修改里面的值)
|
||
|
||
Demo
|
||
|
||
```objectivec
|
||
__block int age = 27;
|
||
MyBlock block = ^{
|
||
age = 28;
|
||
};
|
||
```
|
||
|
||
转为 C++
|
||
|
||
<img src="./../assets/BlockChangeVariableUse__Block.png" style="zoom:30%">
|
||
|
||
```c++
|
||
struct __Block_byref_age_0 {
|
||
void *__isa;
|
||
__Block_byref_age_0 *__forwarding;
|
||
int __flags;
|
||
int __size;
|
||
int age;
|
||
};
|
||
|
||
struct __ViewController__viewDidLoad_block_impl_0 {
|
||
struct __block_impl impl;
|
||
struct __ViewController__viewDidLoad_block_desc_0* Desc;
|
||
__Block_byref_age_0 *age; // by ref
|
||
__ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
|
||
impl.isa = &_NSConcreteStackBlock;
|
||
impl.Flags = flags;
|
||
impl.FuncPtr = fp;
|
||
Desc = desc;
|
||
}
|
||
};
|
||
static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself) {
|
||
__Block_byref_age_0 *age = __cself->age; // bound by ref
|
||
|
||
(age->__forwarding->age) = 28;
|
||
NSLog((NSString *)&__NSConstantStringImpl__var_folders_7x_g921y52j5yb_w5hsn3fb3m8r0000gn_T_ViewController_7fbccf_mi_0, (age->__forwarding->age));
|
||
}
|
||
|
||
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
|
||
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
|
||
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 27};
|
||
void(*ChangeValueBlock)(void) = ((void (*)())&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
|
||
}
|
||
```
|
||
|
||
可以看到 `__block int age = 27;` 变为了 `__Block_byref_age_0` 结构体,给结构体赋值的时候,第二个成员变量的值就是结构体自身地址。
|
||
|
||
`__ViewController__viewDidLoad_block_impl_0` 结构体构造函数 `age(_age->__forwarding)` 就是把外面传递进来结构体的指针,所指向的结构体的 `__forwarding` 成员变量赋值给 block 结构体内的 age 成员变量。
|
||
|
||
block 内部的函数在修改 age 的时候其实就是通过 `__main_block_impl_0` 结构体的 age 找到 `__Block_byref_age_0`,然后访问 `__Block_byref_age_0` 中的成员变量 `__forwarding` 访问成员变量 age,并修改值。
|
||
|
||
|
||
|
||
<img src="./../assets/block-forwarding.png" style="zoom:40%">
|
||
|
||
|
||
|
||
QA:为什么`__block` 变量的 `__Block_byref_age_0` 结构体并不在 block 结构体 `__main_block_impl_0` 中?
|
||
|
||
因为这样做可以在多个 block 中使用 `__block` 变量。
|
||
|
||
|
||
|
||
看个有趣的例子,验证下 __block 的效果
|
||
|
||
<img src="./../assets/Block__BLOCKAssignObject.png" style="zoom:30%" />
|
||
|
||
转换成 c++ 可以看到
|
||
|
||
```c++
|
||
struct __ViewController__viewDidLoad_block_impl_1 {
|
||
struct __block_impl impl;
|
||
struct __ViewController__viewDidLoad_block_desc_1* Desc;
|
||
__Block_byref_num2_0 *num2; // by ref
|
||
__ViewController__viewDidLoad_block_impl_1(void *fp, struct __ViewController__viewDidLoad_block_desc_1 *desc, __Block_byref_num2_0 *_num2, int flags=0) : num2(_num2->__forwarding) {
|
||
impl.isa = &_NSConcreteStackBlock;
|
||
impl.Flags = flags;
|
||
impl.FuncPtr = fp;
|
||
Desc = desc;
|
||
}
|
||
};
|
||
static long __ViewController__viewDidLoad_block_func_1(struct __ViewController__viewDidLoad_block_impl_1 *__cself, NSInteger num3) {
|
||
__Block_byref_num2_0 *num2 = __cself->num2; // bound by ref
|
||
|
||
return (num2->__forwarding->num2) + num3;
|
||
}
|
||
static void __ViewController__viewDidLoad_block_copy_1(struct __ViewController__viewDidLoad_block_impl_1*dst, struct __ViewController__viewDidLoad_block_impl_1*src) {_Block_object_assign((void*)&dst->num2, (void*)src->num2, 8/*BLOCK_FIELD_IS_BYREF*/);}
|
||
|
||
static void __ViewController__viewDidLoad_block_dispose_1(struct __ViewController__viewDidLoad_block_impl_1*src) {_Block_object_dispose((void*)src->num2, 8/*BLOCK_FIELD_IS_BYREF*/);}
|
||
|
||
static struct __ViewController__viewDidLoad_block_desc_1 {
|
||
size_t reserved;
|
||
size_t Block_size;
|
||
void (*copy)(struct __ViewController__viewDidLoad_block_impl_1*, struct __ViewController__viewDidLoad_block_impl_1*);
|
||
void (*dispose)(struct __ViewController__viewDidLoad_block_impl_1*);
|
||
} __ViewController__viewDidLoad_block_desc_1_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_1), __ViewController__viewDidLoad_block_copy_1, __ViewController__viewDidLoad_block_dispose_1};
|
||
|
||
__attribute__((__blocks__(byref))) __Block_byref_num2_0 num2 = {(void*)0,(__Block_byref_num2_0 *)&num2, 0, sizeof(__Block_byref_num2_0), 22};
|
||
NSInteger(*testBlock2)(NSInteger num3) = ((long (*)(NSInteger))&__ViewController__viewDidLoad_block_impl_1((void *)__ViewController__viewDidLoad_block_func_1, &__ViewController__viewDidLoad_block_desc_1_DATA, (__Block_byref_num2_0 *)&num2, 570425344));
|
||
(num2.__forwarding->num2) = 24;
|
||
```
|
||
|
||
外面的 num2 被 `__block` 修饰后变为了对象。通过 num2 对象的 `__forwarding` 指针,再访问 num2 即可修改值。
|
||
|
||
|
||
|
||
#### 2. __block 修饰对象
|
||
|
||
对` __block` 修饰的对象,clang 转换为 c++ 后如下:
|
||
|
||
<img src="./../assets/BlockChangeVariableUse__BlockOnObject.png" style="zoom:30%">
|
||
|
||
|
||
|
||
分析发现:
|
||
|
||
- 对于 `__block` 修饰对象数据,对于生成的结构体也不一样。`__Block_byref_obj_0` 中含有2个操作内存的成员变量`__Block_byref_id_object_copy`、`__Block_byref_id_object_dispose`
|
||
- 其他的逻辑和 `__block` 修饰基本数据类型一致
|
||
|
||
|
||
|
||
注意:
|
||
|
||
<img src="./../assets/NSMutableArrayDonotNeedBlockToUseArray.png" style="zoom:30%">
|
||
|
||
|
||
|
||
- block 外定义的 NSMutableArray,block 内只是使用数组则不需要` __block`
|
||
|
||
- 如果在 block 里操作指针,则需要加 `__block`
|
||
|
||
注意:`__weak` 只可以用来修饰对象,(终端用 clang 处理)否则 clang 会报错 `warning: 'objc_ownership' only applies to Objective-C object or block pointer types; type here is 'int' [-Wignored-attributes]`
|
||
|
||
|
||
|
||
Demo:知道 `__block` 的本质之后,下面打印的 age 的地址是 struct 里面哪个的值?
|
||
|
||
```objectivec
|
||
__block int age = 27;
|
||
MyBlock block = ^{
|
||
age = 28;
|
||
};
|
||
NSLog(@"%p", &age);
|
||
```
|
||
|
||
知道转换为c++后的效果,我们可以在代码中按照结构体,自己定义并转接到 block
|
||
|
||
```objectivec
|
||
struct __Block_byref_age_0 {
|
||
void *__isa; // 0x0000000105231f70 +8
|
||
struct __Block_byref_age_0 *__forwarding; // 0x0000000105231f78 + 8
|
||
int __flags; // 0x0000000105231f80 +4
|
||
int __size; // 0x0000000105231f84 + 4
|
||
int age; // 0x0000000105231f88
|
||
};
|
||
|
||
struct __block_impl {
|
||
void *isa;
|
||
int Flags;
|
||
int Reserved;
|
||
void *FuncPtr;
|
||
};
|
||
|
||
struct __main_block_desc_0 {
|
||
size_t reserved;
|
||
size_t Block_size;
|
||
void (*copy)(void);
|
||
void (*dispose)(void);
|
||
};
|
||
|
||
struct __main_block_impl_0 {
|
||
struct __block_impl impl;
|
||
struct __main_block_desc_0* Desc;
|
||
struct __Block_byref_age_0 *age; // by ref
|
||
};
|
||
|
||
|
||
typedef void(^MyBlock)(void);
|
||
int main(int argc, const char * argv[]) {
|
||
@autoreleasepool {
|
||
__block int age = 27;
|
||
MyBlock block = ^{
|
||
age = 28;
|
||
};
|
||
struct __main_block_impl_0 *blockImpl = (__bridge struct __main_block_impl_0 *)block;
|
||
NSLog(@"%p", &age);
|
||
}
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
我们将断点设置到 NSLog 这里,打印出自定义结构体 `__main_block_impl_0` 中的 age 。
|
||
|
||
<img src="./../assets/Block-variableAddress.png" style="zoom:30%">
|
||
|
||
```c
|
||
// 0x0000000105231f70
|
||
struct __Block_byref_age_0 {
|
||
void *__isa; // 地址:0x0000000105231f70 长度:+8
|
||
struct __Block_byref_age_0 *__forwarding; // 地址:0x0000000105231f78 长度:+8
|
||
int __flags; // 地址:0x0000000105231f80 长度:+4
|
||
int __size; // 地址:0x0000000105231f84 长度:+4
|
||
int age; // 地址:0x0000000105231f88
|
||
};
|
||
|
||
```
|
||
|
||
将地址打印出来。该地址就是 `__Block_byref_age_0` 结构体的地址,也就是结构体内第一个 `isa` 的地址。我们计算下,规则如下:
|
||
|
||
- 指针长度8个字节
|
||
|
||
- int 长度4个字节
|
||
|
||
算出来 age 的地址为 `0x0000000105231f88` ,此时 Xcode 打印出的地址也是 `0x105231f88`。其实也就是 `blockImple->age->age` 的地址
|
||
|
||
block 内部对变量的值修改其实就是对 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` 函数会自动释放引用的 auto 变量,类似于 release
|
||
|
||
|
||
|
||
#### 3. 什么情况下需要 __block
|
||
|
||
局部变量:基本数据类型、对象数据类型
|
||
|
||
|
||
|
||
#### 4. 什么情况下不需要 __block
|
||
|
||
- 全局变量(不截获)
|
||
- 静态全局变量(不截获)
|
||
- 静态局部变量(截获指针)
|
||
|
||
|
||
|
||
|
||
|
||
## 五、`__forwarding` 的设计
|
||
|
||
Demo1
|
||
|
||
```objective-c
|
||
__block int age = 27;
|
||
NSLog(@"1: age is %d, address is %p", age, &age);
|
||
|
||
age = 30;
|
||
NSLog(@"4: age is %d, address is %p", age, &age);
|
||
// console
|
||
1: age is 27, address is 0x7ff7bb181c28
|
||
4: age is 30, address is 0x7ff7bb181c28
|
||
```
|
||
|
||
利用 `xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 ViewController.m -o ViewController-arm64.cpp` 转成 c++ 研究下
|
||
|
||
```c++
|
||
struct __Block_byref_age_0 {
|
||
void *__isa;
|
||
__Block_byref_age_0 *__forwarding;
|
||
int __flags;
|
||
int __size;
|
||
int age;
|
||
};
|
||
|
||
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
|
||
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
|
||
int hegith = 175;
|
||
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 27};
|
||
NSLog((NSString *)&__NSConstantStringImpl__var_folders_vf_05htlrfx42qck4yh6bsnh3yr0000gn_T_ViewController_450701_mi_0, (age.__forwarding->age), &(age.__forwarding->age));
|
||
(age.__forwarding->age) = 30;
|
||
NSLog((NSString *)&__NSConstantStringImpl__var_folders_vf_05htlrfx42qck4yh6bsnh3yr0000gn_T_ViewController_450701_mi_1, (age.__forwarding->age), &(age.__forwarding->age));
|
||
}
|
||
```
|
||
|
||
可以看到:
|
||
|
||
- age 没有在任何一个 block 中访问(也就不存在变量捕获的问题),仅仅是被 `__block` 修饰,编译器也会转为结构体 `struct __Block_byref_age_0`
|
||
|
||
- 通过 `__Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 27};` 可以看出此时 `__Block_byref_age_0` 结构体中的 `__forwarding` 成员变量指针指向栈空间上 age 的地址(也就是结构体 `__Block_byref_age_0` 自身的地址)
|
||
|
||
大概如下
|
||
|
||
```c++
|
||
// 栈上创建 __Block_byref_age_0 结构体实例
|
||
__Block_byref_age_0 age = {
|
||
.__isa = NULL,
|
||
.__forwarding = &age, // 指向自己(栈地址)
|
||
.__flags = 0,
|
||
.__size = sizeof(__Block_byref_age_0),
|
||
.age = 27
|
||
}
|
||
```
|
||
|
||
- NSLog 打印 age 的值和地址的时候,都是 `(age.__forwarding->age), &(age.__forwarding->age))`通过结构体变量的成员变量 `__forwarding` 指针指向的自身,再去访问成员变量的
|
||
- `(age.__forwarding->age) = 30;` 赋值的时候也是通过结构体变量的成员变量 `__forwarding` 指针指向的自身去赋值的
|
||
|
||
Tips:
|
||
|
||
在 C 和 Objective-C 中,结构体位于栈还是堆上,取决于声明方式。
|
||
|
||
- 在栈上创建:当结构体作为局部变量在函数内部声明时,它会直接分配在栈上
|
||
- 在堆上创建:当使用动态内存分配函数(如`malloc`、`calloc`或Objective-C的`alloc`)时,结构体会在堆上分配
|
||
|
||
``` objective-c
|
||
- (void)viewDidLoad {
|
||
[super viewDidLoad];
|
||
// 栈上
|
||
struct Person {
|
||
char name[20];
|
||
int age;
|
||
};
|
||
struct Person p1; // p1 分配在栈上
|
||
p1.age = 25;
|
||
NSLog(@"栈地址:%p", &p1); // 输出栈地址
|
||
|
||
// 堆上
|
||
struct Student {
|
||
char name[20];
|
||
int age;
|
||
};
|
||
|
||
struct Student *st = malloc(sizeof(struct Student)); // p2 指向堆内存
|
||
st->age = 30;
|
||
NSLog(@"堆地址:%p", st); // 输出堆地址
|
||
free(st); // 需要手动释放
|
||
}
|
||
```
|
||
|
||
Demo2 : `__block` 如何修改外部变量
|
||
|
||
```objective-c
|
||
- (void)viewDidLoad {
|
||
[super viewDidLoad];
|
||
__block int age = 27;
|
||
NSLog(@"1: age = %d, address is %p", age, &age);
|
||
void(^block1)(void) = ^{
|
||
age = 28;
|
||
NSLog(@"in block: age = %d, address is %p", age, &age);
|
||
};
|
||
NSLog(@"2: age = %d, address is %p", age, &age);
|
||
block1();
|
||
NSLog(@"3: age = %d, address is %p", age, &age);
|
||
age = 29;
|
||
NSLog(@"4: age = %d, address is %p", age, &age);
|
||
}
|
||
// console
|
||
1: age = 27, address is 0x7ff7b0faebf8
|
||
2: age = 27, address is 0x600000464938
|
||
in block: age = 28, address is 0x600000464938
|
||
3: age = 28, address is 0x600000464938
|
||
4: age = 29, address is 0x600000464938
|
||
```
|
||
|
||
|
||
|
||
<img src="./../assets/BlockChangeOuterValue.png" style="zoom:100%">
|
||
|
||
分析:
|
||
|
||
- 被 `__block` 修饰的 int age 将会被封装为一个结构体,该结构体内有一个 `__forwarding` 成员变量
|
||
|
||
```c++
|
||
struct __Block_byref_age_0 {
|
||
void *__isa;
|
||
__Block_byref_age_0 *__forwarding;
|
||
int __flags;
|
||
int __size;
|
||
int age;
|
||
};
|
||
```
|
||
|
||
- 在给 block 赋值的时候,其成员变量 `__forwarding`的值是由当前结构体对象的地址赋值的
|
||
|
||
````c++
|
||
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 27};
|
||
````
|
||
|
||
- block 通过指针持有栈上的 `__Block_byref_age_0` 结构体
|
||
|
||
- 当发现 block 是函数返回值或者被一个强指针指向的时候,编译器生成 `_Block_copy()` 函数调用,将 block 从栈复制到堆。
|
||
|
||
- 如果 block 捕获了 `__block` 变量,编译器会调用 `_Block_object_assign()`,将 `__Block_byref_age_0` 结构体从栈复制到堆。同时修改 `__Block_byref_age_0` 结构体的 `__forwarding` 指针,指向堆上的结构体地址。
|
||
|
||
- block 内部代码,将被封装为一个新的函数 `__ViewController__viewDidLoad_block_func_0`,其内部通过结构体指针` _cself` 的 age 成员变量,获取到 `__Block_byref_age_0` 指针,该指针命名为 age。然后通过 age 指针访问到结构体的 `__forwarding` 成员变量,该成员变量指向的是结构体自己,然后再访问 age 拿到真正的 age 进行修改。
|
||
|
||
```c++
|
||
static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself) {
|
||
__Block_byref_age_0 *age = __cself->age; // bound by ref
|
||
(age->__forwarding->age) = 28;
|
||
NSLog((NSString *)&__NSConstantStringImpl__var_folders_7x_g921y52j5yb_w5hsn3fb3m8r0000gn_T_ViewController_bccea7_mi_1, (age->__forwarding->age), &(age->__forwarding->age));
|
||
}
|
||
```
|
||
|
||
- 第一行输出 `1: age = 27, address is 0x7ff7b0faebf8` 是因为此时 age 在栈上,高地址 `0x7ff7b0faebf8`
|
||
|
||
- 第二行输出 `2: age = 27, address is 0x600000464938` 是因为 block 被拷贝到堆上,内部对于使用到的` __block` 变量也会拷贝到堆上,是通过一个结构体对象来实现的。由于在栈上,地址变为 `0x600000464938`,相较于栈上的地址,地址变低了。
|
||
|
||
- 将 block 从栈拷贝到堆上时,block 所捕获的 `__block` 变量也会从栈拷贝到堆上,但是此时我们在该函数的作用域内(即 block 外)仍然是可以对 age 变量进行修改的
|
||
|
||
- 第三行输出 `in block: age = 28, address is 0x600000464938` 是因为此时 age 在堆上,低地址 `0x600000464938`。通过结构体 `__ViewController__viewDidLoad_block_impl_0`的 age 成员变量指向的 `__Block_byref_age_0` 指针,再通过指针指向的 `__forwarding` 指向自己,再访问 age 来修改值
|
||
|
||
- 为了将上述修改进行同步,在将 `__block` 变量从栈拷贝到堆上时,栈上的 `__Block_byref_val_0` 结构体的 `__forwarding` 指针将会指向堆上的 `__Block_byref_val_0` 结构体。所以此时,`age` 变量(即`age.__forwarding->age`变量)的地址改变了
|
||
|
||
- 第四行输出 `3: age = 28, address is 0x600000464938` 是因为此时 age 在堆上,低地址 `0x600000464938`,且值为28
|
||
|
||
- 第五行输出 `4: age = 29, address is 0x600000464938` 是因为此时通过栈上的 age 结构体,通过成员变量 `__forwarding` 指向对上的结构体地址,然后再通过指向堆上的结构体的 age 成员变量已经被修改为 29 了
|
||
|
||
通过 Demo1 和 Demo2 总结下:` __forwarding` 的作用是什么?为什么这么设计
|
||
|
||
<img src="./../assets/block_forwarding.png" style="zoom:15%">
|
||
|
||
|
||
|
||
- 当 block在栈中时,栈上的 `__Block_byref_age_0` 结构体内的 `__forwarding` 指针指向栈上的结构体自己
|
||
|
||
- 当 block 被复制到堆中时,栈中的`__Block_byref_age_0` 结构体也会被复制到堆中一份,而此时栈中的 `__Block_byref_age_0` 结构体的成员变量 `__forwarding` 指针指向的就是堆中的 `__Block_byref_age_0`结构体,堆中 `__Block_byref_age_0`结构体内的 `__forwarding` 指针依然指向自己,此时再访问成员变量 age 就可以修改堆上的值
|
||
|
||
|
||
|
||
完整流程:
|
||
|
||
- 初始阶段:`__block` 变量(结构体)在栈上,`__forwarding` 指向栈地址
|
||
- block 复制到堆时:`__block` 结构体被复制到堆,原来栈上结构体变量 `__forwarding` 指针被重定向到堆副本(指向堆上的地址)
|
||
- 最终结果:所有代码(无论 block 内外)通过 `__forwarding` 访问堆上的副本,因此地址看似“不变”,实际是 `__forwarding` 指针在幕后重定向
|
||
|
||
这种设计既保证了性能(避免不必要的堆分配),又确保了 Block 内外对 `__block` 变量修改的同步性。
|
||
|
||
所以存在2方面的优点:
|
||
|
||
- 性能优化:如果 Block 未被复制到堆(例如仅在栈上使用),则无需为 `__block` 变量分配堆内存。`__forwarding` 指针的初始设计允许“按需复制”。
|
||
- 统一访问逻辑:无论 `__block` 变量在栈还是堆,代码中访问 `age` 时,始终通过 `age.__forwarding->age`,编译器隐藏了实际存储位置的差异
|
||
|
||
`__forwarding` 指针的设计是为了解决 **Block 在栈(Stack)和堆(Heap)之间迁移时的内存访问一致性问题**,并确保对 Block 及其捕获变量的操作始终安全可靠
|
||
|
||
**一言以蔽之,`__forwarding` 指针是为了在 `__block` 变量从栈复制到堆上后,在 block 外对 `__block` 变量的修改也可以同步到堆上实际存储的 `__block` 变量的结构体上。也就是抹平栈、堆上对变量操作的差异。**
|
||
|
||
|
||
|
||
Tips:实现 block 拷贝及其捕获对象的函数是 `_Block_copy`,工作流程如下:
|
||
|
||
- 检查 block 类型
|
||
|
||
首先通过 Block 的 `flags` 字段判断其当前存储位置:
|
||
|
||
- 全局 Block(`BLOCK_IS_GLOBAL`):直接返回原 Block,无需复制(全局 Block 存储在数据区,生命周期与程序一致)。
|
||
- 堆 Block(`BLOCK_NEEDS_FREE`):增加引用计数后返回原 Block(堆 Block 已由 ARC 管理)。
|
||
- 栈 Block:需要复制到堆上,进入下一步流程。
|
||
|
||
```c++
|
||
void *_Block_copy(const void *arg) {
|
||
struct Block_layout *src = (struct Block_layout *)arg;
|
||
if (src == NULL) return NULL;
|
||
|
||
// 全局 Block 直接返回
|
||
if (src->flags & BLOCK_IS_GLOBAL) {
|
||
return src;
|
||
}
|
||
|
||
// 堆 Block 增加引用计数
|
||
if (src->flags & BLOCK_NEEDS_FREE) {
|
||
src->flags |= BLOCK_REFCOUNT_MASK;
|
||
return src;
|
||
}
|
||
|
||
// 栈 Block 复制到堆
|
||
// ...
|
||
}
|
||
```
|
||
|
||
- 分配堆内存并复制结构体值
|
||
|
||
为栈 Block 分配堆内存,并复制其内容:
|
||
|
||
- 内存分配:使用 malloc 分配与栈上结构体一样大的堆空间
|
||
- 结构体复制:将栈上的 block_layout 包含函数指针等都复制到堆上
|
||
|
||
```c++
|
||
struct Block_layout *dest = malloc(src->descriptor->size);
|
||
memmove(dest, src, src->descriptor->size); // 复制结构体
|
||
```
|
||
|
||
- 更新 block 标志位
|
||
|
||
复制后设置 block 的标志位:
|
||
|
||
- 标记为堆 block:`BLOCK_NEEDS_FREE` 表示需要手动释放
|
||
- 初始化引用计数:引用计数设为 1(后续通过 `_Block_release` 管理)
|
||
|
||
```c++
|
||
dest->flags &= ~BLOCK_REFCOUNT_MASK; // 清除原有引用计数
|
||
dest->flags |= BLOCK_NEEDS_FREE | 1; // 标记为堆 Block,引用计数=1
|
||
```
|
||
|
||
- 处理捕获的变量。调用 `descriptor` 中的 `copy` 助手函数,处理捕获的变量
|
||
|
||
- 对象类型变量:对 `__strong` 修饰的对象调用 `retain`(符合 ARC 规则)
|
||
- `__block` 变量:将栈上的 `__Block_byref_xxx` 结构体复制到堆,并更新 `__forwarding` 指针
|
||
|
||
- 返回堆上的 block:最终返回堆上的 block,供后续使用
|
||
|
||
|
||
|
||
## 六、Block 内存引用
|
||
|
||
对于` __block` 修饰的变量进行研究
|
||
|
||
Demo0
|
||
|
||
<img src="./../assets/BlockVariableDemo0.png" style="zoom:100%">
|
||
|
||
可以看到:
|
||
|
||
- block 访问了外部的变量,则会在 block 的底层实现即 `__ViewController__viewDidLoad_block_impl_0` 内增加一个 `__Block_byref_age_0 *age` 成员变量
|
||
- 被 `__block` 修饰的基础数据类型 int 会被编译器自动创建为一个结构体 `__Block_byref_age_0` ,其内部的成员变量 age 存储真实的值
|
||
|
||
Demo1
|
||
|
||
<img src="./../assets/BlockVariableDemo1.png" style="zoom:100%">
|
||
|
||
<img src="./../assets/block-strong-object-memoery.png" style="zoom:30%">
|
||
|
||
|
||
|
||
Demo2
|
||
|
||
<img src="./../assets/BlockVariableDemo2.png" style="zoom:100%">
|
||
|
||
<img src="./../assets/block-weak-object-memoery.png" style="zoom:30%" >
|
||
|
||
|
||
|
||
Test0:
|
||
|
||
```c++
|
||
int age = 20;
|
||
void (^block)(void) = ^(void) {
|
||
NSLog(@"%p", &age);
|
||
};
|
||
|
||
|
||
struct __ViewController__viewDidLoad_block_impl_0 {
|
||
struct __block_impl impl;
|
||
struct __ViewController__viewDidLoad_block_desc_0* Desc;
|
||
int age;
|
||
__ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
|
||
impl.isa = &_NSConcreteStackBlock;
|
||
impl.Flags = flags;
|
||
impl.FuncPtr = fp;
|
||
Desc = desc;
|
||
}
|
||
};
|
||
```
|
||
|
||
Test1:
|
||
|
||
```c++
|
||
__strong NSObject *obj = [[NSObject alloc] init];
|
||
NSLog(@"1 %p", obj);
|
||
void (^block)(void) = ^(void) {
|
||
NSLog(@"%p", obj);
|
||
};
|
||
block();
|
||
|
||
|
||
struct __ViewController__viewDidLoad_block_impl_0 {
|
||
struct __block_impl impl;
|
||
struct __ViewController__viewDidLoad_block_desc_0* Desc;
|
||
NSObject *__strong obj;
|
||
__ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, NSObject *__strong _obj, int flags=0) : obj(_obj) {
|
||
impl.isa = &_NSConcreteStackBlock;
|
||
impl.Flags = flags;
|
||
impl.FuncPtr = fp;
|
||
Desc = desc;
|
||
}
|
||
};
|
||
```
|
||
|
||
Test2:
|
||
|
||
```c++
|
||
_block NSObject *obj = [[NSObject alloc] init];
|
||
__weak NSObject *weakObj = obj;
|
||
NSLog(@"1 %p", weakObj);
|
||
void (^block)(void) = ^(void) {
|
||
NSLog(@"%p", weakObj);
|
||
};
|
||
block();
|
||
|
||
|
||
struct __Block_byref_obj_0 {
|
||
void *__isa;
|
||
__Block_byref_obj_0 *__forwarding;
|
||
int __flags;
|
||
int __size;
|
||
void (*__Block_byref_id_object_copy)(void*, void*);
|
||
void (*__Block_byref_id_object_dispose)(void*);
|
||
NSObject *__strong obj;
|
||
};
|
||
|
||
struct __ViewController__viewDidLoad_block_impl_0 {
|
||
struct __block_impl impl;
|
||
struct __ViewController__viewDidLoad_block_desc_0* Desc;
|
||
NSObject *__weak weakObj;
|
||
__ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, NSObject *__weak _weakObj, int flags=0) : weakObj(_weakObj) {
|
||
impl.isa = &_NSConcreteStackBlock;
|
||
impl.Flags = flags;
|
||
impl.FuncPtr = fp;
|
||
Desc = desc;
|
||
}
|
||
};
|
||
```
|
||
|
||
Test3:
|
||
|
||
```objective-c
|
||
NSObject *obj = [[NSObject alloc] init];
|
||
__block __weak NSObject *weakObj = obj;
|
||
|
||
NSLog(@"1 %p", weakObj);
|
||
void (^block)(void) = ^(void) {
|
||
NSLog(@"%p", weakObj);
|
||
};
|
||
|
||
struct __Block_byref_weakObj_0 {
|
||
void *__isa;
|
||
__Block_byref_weakObj_0 *__forwarding;
|
||
int __flags;
|
||
int __size;
|
||
void (*__Block_byref_id_object_copy)(void*, void*);
|
||
void (*__Block_byref_id_object_dispose)(void*);
|
||
NSObject *__weak weakObj;
|
||
};
|
||
|
||
struct __ViewController__viewDidLoad_block_impl_0 {
|
||
struct __block_impl impl;
|
||
struct __ViewController__viewDidLoad_block_desc_0* Desc;
|
||
__Block_byref_weakObj_0 *weakObj; // by ref
|
||
__ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, __Block_byref_weakObj_0 *_weakObj, int flags=0) : weakObj(_weakObj->__forwarding) {
|
||
impl.isa = &_NSConcreteStackBlock;
|
||
impl.Flags = flags;
|
||
impl.FuncPtr = fp;
|
||
Desc = desc;
|
||
}
|
||
};
|
||
```
|
||
|
||
可以看出:
|
||
|
||
- Test0:如果基础数据类型,没有用 `__block` 修饰,则 block 底层的结构体 `__ViewController__viewDidLoad_block_impl_0` 内,只会增加一个基础成员变量 `int age`
|
||
|
||
- Test1:如果是一个 strong 指针指向的对象,则会在 block 底层的结构体 `__ViewController__viewDidLoad_block_impl_0` 内,只会增加一个 strong 对象类的成员变量 `NSObject *__strong obj;`
|
||
|
||
- Test2: 如果一个对象被 `__block` 修饰,但是是强指针类型的,那么生成的被 `__block` 修饰的结构体 `__Block_byref_obj_0` 内只会有一个强指针指向的成员变量;如果一个仅仅是 weak 指针的对象(没有被 `__block` 修饰),那么在 block 底层的结构体 `__ViewController__viewDidLoad_block_impl_0` 内,只会存在一个 weak 指针的成员变量 ` NSObject *__weak weakObj;`
|
||
|
||
- Test3: 如果一个对象被 `__block` 修饰,同时是弱指针类型的,则在 block 底层的结构体 `__ViewController__viewDidLoad_block_impl_0` 内,仅仅会存在一个强指针指向的成员变量 ` __Block_byref_weakObj_0 *weakObj` ,指向被 `__block` 底层生成的结构体 `__Block_byref_weakObj_0`, `__Block_byref_weakObj_0` 结构体内会存在一个 weak 指针指向的成员变量,指向堆上的 weakObj
|
||
|
||
|
||
|
||
通过上面例子的源码可以看到:
|
||
|
||
- block 结构体里面的针对变量生成的结构体新对象,都是用 strong 指针指向的
|
||
- block 所捕获的对象是` __weak` 还是` __strong` 决定了新生成结构体对象里面的对象内存访问修饰符。
|
||
|
||
```c
|
||
int main(int argc, const char * argv[]) {
|
||
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
|
||
__attribute__((__blocks__(byref))) __Block_byref_p_0 p = {(void*)0,(__Block_byref_p_0 *)&p, 33554432, sizeof(__Block_byref_p_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"))};
|
||
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_p_0 *)&p, 570425344));
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
|
||
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 __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
|
||
_Block_object_assign((void*)&dst->p, (void*)src->p, 8/*BLOCK_FIELD_IS_BYREF*/);
|
||
}
|
||
```
|
||
|
||
如果 `__block` 修饰 `__strong` 则表示 block_impl 结构体中的 person 成员变量指向一个新的结构体 `__Block_byref_person_0`。这个线是强引用。
|
||
|
||
`__Block_byref_person_0` 结构体成员变量 person 真正的 Person 对象的引用关系要看 block 外部 person 的修饰是 `__strong` 还是 `__weak`,因为从栈上拷贝到堆上,会调用 block 的 desc 的 `__main_block_copy_0`,本质上调用的是 `_Block_object_assign`
|
||
|
||
`__Block_byref_id_object_copy_131` 方法里的 40 代表什么?
|
||
|
||
```c
|
||
|
||
struct __Block_byref_p_0 {
|
||
void *__isa; 8
|
||
__Block_byref_p_0 *__forwarding; 8
|
||
int __flags; 4
|
||
int __size; 4
|
||
void (*__Block_byref_id_object_copy)(void*, void*); 8
|
||
void (*__Block_byref_id_object_dispose)(void*); 8
|
||
Person *p; 8
|
||
};
|
||
|
||
__attribute__((__blocks__(byref))) __Block_byref_p_0 p = {
|
||
0,
|
||
&p,
|
||
33554432,
|
||
sizeof(__Block_byref_p_0),
|
||
__Block_byref_id_object_copy_131,
|
||
__Block_byref_id_object_dispose_131,
|
||
((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"))
|
||
|
||
};
|
||
```
|
||
|
||
`__Block_byref_p_0` 结构体地址上偏移40就是 p 对象。
|
||
|
||
|
||
|
||
|
||
|
||
## 七. 循环引用
|
||
|
||
self 是一个局部变量,block 访问 self,即存在捕获变量的效果。
|
||
|
||
为什么会存在循环引用?block 会对截获的变量是对象类型,会把所有权也进行捕获。为什么 strong 类型的对象,会造成对象和 block 的循环引用
|
||
|
||
看个 Demo
|
||
|
||
<img src="./../assets/blockCaptureStrongObjectError.png" style="zoom:40%" />
|
||
|
||
可以看到 block 放在堆上的时候(被抢指针指向、作为返回值)的时候,如果 block 内部访问了强指针指向的对象,则会发生循环引用。
|
||
|
||
可以看到 Person 对象的 dealloc 方法没有执行,里面的打印信息没有输出。
|
||
|
||
### 1. ARC 下
|
||
|
||
`__weak`、`__unsafe_unretained` 修饰 `__block` 所修饰的变量。区别在于:
|
||
|
||
- `__weak` 不会产生强引用,指向的对象销毁时,会自动给指针置为 nil
|
||
|
||
- `__unsafe_retained` 不会产生强引用,不安全。当指向的对象销毁时,指针地址值不变。
|
||
|
||
```objectivec
|
||
@interface Person : NSObject
|
||
@property (nonatomic, assign) NSInteger age;
|
||
@property (nonatomic, copy) void (^block)(void);
|
||
- (void)test;
|
||
@end
|
||
|
||
@implementation Person
|
||
- (void)dealloc
|
||
{
|
||
NSLog(@"%s", __func__);
|
||
}
|
||
- (void)test
|
||
{
|
||
__weak typeof(self) weakself = self;
|
||
self.block = ^{
|
||
weakself.age = 23;
|
||
};
|
||
self.block();
|
||
NSLog(@"age:%ld", (long)self.age);
|
||
}
|
||
@end
|
||
|
||
Person *p = [[Person alloc] init];
|
||
[p test];
|
||
```
|
||
|
||
|
||
|
||
<img src="./../assets/WeakSolveBlockRetainCycle.png" style="zoom:40%" />
|
||
|
||
|
||
|
||
方法1: `__weak` 修饰。`__weak typeof(self) weakself = self;`
|
||
|
||
方法2: `__unsafe_retained` 修饰。`__unsafe_unretained typeof(self) weakself = self;`
|
||
|
||
方法3: `__block` 修饰。因为此时会构成3角关系。所以需要调用 block,block 内部需要将对象设置为 nil。虽然` __block` 方案也可以解决循环引用的问题,但是缺点是该 block 需要执行,方案会有限制。
|
||
|
||
```objectivec
|
||
__block Person *weakself = [[Person alloc] init];
|
||
p.block = ^{
|
||
weakself.age = 23;
|
||
NSLog(@"%ld", weakself.age);
|
||
weakself = nil;
|
||
};
|
||
p.block();
|
||
```
|
||
|
||
`__unsafe_retained` 因为不安全所以不推荐,`__block` 因为使用繁琐,且必须等到调用 block 才会释放内存,所以不推荐。ARC 下最佳用 `__weak`
|
||
|
||

|
||
|
||
|
||
|
||
### 2. MRC 下
|
||
|
||
方法1: `__unsafe_retained` 修饰。`__unsafe_unretained typeof(self) weakself = self;`
|
||
|
||
方法2: `__block` 修饰。MRC 下不会对 block 内部的对象引用计数 +1
|
||
|
||
|
||
|
||
## 八、为什么加 weakself、strongself
|
||
|
||
weakSelf 是为了使 block 不持有 self,避免 Retain Circle 循环引用。在 Block 内如果需要访问 self 的方法、变量,建议使用 weakSelf。
|
||
|
||
strongSelf 的目的是因为一旦进入 block 执行,假设不允许 self 在这个执行过程中释放,就需要加入 strongSelf。
|
||
|
||
block 执行完后这个 strongSelf 会自动释放,没有不会存在循环引用问题。如果在 Block 内需要多次 访问 self,则需要使用 strongSelf。
|
||
|
||
1. **防止对象在 block 执行过程中被释**
|
||
|
||
- 弱引用的风险:`weakself`是对`self`的弱引用,不会增加其引用计数。如果在Block开始执行后,`self`被其他部分释放,后续对`weakself`的访问将指向无效内存,导致崩溃。
|
||
- 强引用的保障:通过`__strong`修饰的`strongSelf`,在 block 内部临时强引用`self`,确保在 block 执行期间`self`不会被释放。即使外部已经不再持有`self`,只要 block 在执行,`strongSelf`会保持`self`存活。
|
||
|
||
2. ### **避免悬垂指针(Dangling Pointer)**
|
||
|
||
```objective-c
|
||
// bad
|
||
__weak typeof(self) weakself = self;
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
// 假设此时 weakself 已被释放
|
||
[weakself doSomething]; // 访问已释放对象,崩溃!
|
||
});
|
||
|
||
// good
|
||
__weak typeof(self) weakself = self;
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
// 假设此时 weakself 已被释放
|
||
__strong typeof(self) strongSelf = weakself;
|
||
if (strongSelf) {
|
||
[strongSelf doSomething]; // 访问已释放对象,崩溃!
|
||
}
|
||
});
|
||
```
|
||
|
||
|
||
|
||
## 九、总结
|
||
|
||
1. block 本质是什么?封装了函数调用及其调用环境的 OC 对象。本质实现是一个结构体。
|
||
2. `__block` 的作用是什么?可以对 block 外部的变量进行捕获,可以修改。但是需要注意内存管理相关问题。比如`__weak`、`__unsafe_unretained`、`__block`
|
||
3. 修改 NSMutableArray 不需要加 `__block`? 是的,如果修改 NSMutableArray 指针比如`array = nil` 则需要加`__block `
|
||
4. block 属性修饰词为什么是 copy?没有进行 copy 操作的时候,block 就不会在堆上,对于 block 生命周期以及所使用到的内存,没办法灵活控制(由栈控制,出栈就死)。因为 block 的高频使用场景就是作为方法参数传递、作为类的属性值,所以最常见的场景是:赋值的地方不是使用的地方,所以要捕获周围环境参数和管理所捕获的内存、以及自身内存。
|
||
5. 为什么会产生循环引用?
|
||
1. 如果当前当前 block 对于某个变量进行捕获,变量也是强引用类型的,block 捕获变量后,block 对变量是强引用关系,当前对象(VC)对 block 是强引用关系,变量也是 VC 强引用的,就产生了循环引用。
|
||
2. 用 __block 修饰:
|
||
- MRC 下不会产生循环引用
|
||
- ARC 下会产生循环引用,可以采用断环的方式解决。 |