84 KiB
block 底层原理
大家写 OC 肯定用过不少 block。有这样4个问题:
- block 原理是什么,系统是如何实现的?
- __block 的作用是什么?
- block 作为属性时,为什么用 copy 修饰?
- block 在修改 NSMutableArray 的时候,需要加 __block 吗?
带着问题探究本文。
一、block 本质探索
1. 实验探索
Demo
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++
ViewController.m 的 viewDidLoad 函数为 _I_ViewController_viewDidLoad
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 的结构体
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 结构体的第一个成员,所以上述代码等价于
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 结构体
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。在
((void (*)(__block_impl *, NSInteger, NSInteger))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1, 2);
简化为
block->FuncPtr(block, 1, 2);
最后在 viewDidLoad 函数中通过结构体 impl 的成员 FuncPtr,调用了函数。
第二个参数:
__ViewController__viewDidLoad_block_desc_0_DATA 可以看成是一个 block 信息的描述,占用了 sizeof(struct __ViewController__viewDidLoad_block_impl_0) 大小的空间。
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:
((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 这个结构体的第一个变量地址(结构体特性),然后访问第一个结构体变量内的成员变量,等价于,直接访问结构体第一个成员变量的结构体变量
举个例子
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 字节)来获取值。
struct block { // baseAddress
struct first {
isa;
Flags;
FuncPtr; // offset
Desc;
}
struct second;
// ...
}
上述的代码,等价于 block->impl.FuncPtr
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
类似于下面代码
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 信息,然后查看
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(@"");
}
2. 结论
通过探索发现:
-
block 本质上就是一个 oc 对象,也有 isa 指针
-
block 是封装了函数调用和函数调用环境的 OC 对象
二、block 变量捕获
1. auto 变量捕获
在 C 和 Objective-C 编程语言中,
auto关键字用于声明自动存储期的变量。自动存储期的变量会在定义它们的块(block)或作用域(scope)中自动创建,并在退出该作用域时自动销毁。这是变量存储期的默认行为,因此auto关键字实际上是可选的,但有时候为了清晰起见,开发者可能会显式使用它。
Demo1:
一个最简单的 block,参数和返回值都是 void,内部仅一条打印语句。
void(^printBlock)(void) = ^ {
NSLog(@"Hello block");
};
printBlock();
用 xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp 转换为 c++
概括如下:
Demo2: 捕获外部变量
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++ 代码
代码分析:
- 可以看到我们编写的 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 变量捕获
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++ 代码
对代码进行分析:
- 可以看到我们编写的 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. 全局变量捕获
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++ 代码
代码分析:
- 可以看到我们编写的 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 会被捕获吗?
#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++ 代码
代码分析:
-
可以看到我们编写的 block 被声明为一个
__Person__play_block_impl_0类型的结构体 -
结构体内有个构造函数,见22893行代码。
-
因为 objective-c 的方法,默认会携带2个参数,
self和_cmd,等价于void play(Person *self, SEL _cmd),所以22917行代码 调用构造函数的时候,self 会被传递进去。查看 c++ 代码,可以看到 OC 的 play 方法被转换为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方法等价于- (void)play:(id)self selector:(SEL)_cmd { //... }所以 self 是局部变量,block 为了访问局部变量,会发生变量捕获。
-
block 为了局部变量 self 的将来访问,结构体内部也增加了一个 Person 类型的 self,所以存在 self 的变量捕获。
所以,答案是会,因为 self 就是局部变量。 一个 oc 方法转换为 void test(Person *self, SEL _cmd) 形式,所以 self 也是局部变量,会被捕获。
Tips:判断会不会捕获的本质就是判断某个对象、变量是不是局部变量,是局部变量则会捕获,否则不会捕获。
举个例子:
// 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
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) {
}
可以看到没有捕获任何变量。但对上述代码进行调整
// Person.m
- (void)testBlockCapture {
void (^test)(void) = ^(void) {
NSLog(@"%@", _hobby);
};
test();
}
继续调为 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 验证下
也就是说: __NSGlobalBlock__ -> NSBlock -> NSObject。
继续验证,Demo2
同时利用指令 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 在内存中的排布如下图:
2. 如何判断 block 属于什么类型
Demo:
由于 ARC 默认会做一些优化,为了彻底的研究 block,我们将工程的 ARC 关掉(Build Setting 里 Automatic Reference Counting 设置为 No)
分析:
-
block1 是
__NSGlobalBlock__,此类型的 block 用结构体实例的内容不依赖于执行时的状态,所以整个程序只需要1个实例。因此存放于全局变量相同的数据区域即可。 -
block2 是
__NSStackBlock__. -
但为什么执行 block2 的时候发生了 crash?
猜测由于在 test 方法内给 block2 赋值,也就是在栈上定义和捕获了栈上的变量 age。等 test 方法结束,可能栈上的数据消失或者内存指向某个不可预知的地方,所以这个情况下调用 block2 会 crash。
如何解决该问题?
这类问题概括就是:__NSStackBlock__ 栈上的 block 及其捕获的数据出了栈后,内存不稳定,将来某个时间调用 block,可能会发生 crash。需要把数据和内存稳定下来,不要交由栈自动处理。想办法把栈上的数据移动到堆上,由程序员自己管理内存。
当 __NSStackBlock__ 调用 copy 方法后会变为 __NSMallocBlock__。如下图:
Demo 也同时发现,当对 __NSGlobalBlock__ 调用 copy ,不会变为 __NSMallocBlock__ 。
3. 总结
| block 类型 | 环境 |
|---|---|
__NSGlobalBlock__ |
没有访问 auto 变量 |
__NSStackBlock__ |
访问了 auto 变量 |
__NSMallocBlock__ |
__NSStackBlock__ 调用了 copy 方法 |
调用 copy 方法
| Block 类 | 原本位置 | 复制效果 |
|---|---|---|
__NSConcreteStackBlock__ |
栈 | 栈复制到堆 |
__NSConcreteGlobalBlock__ |
程序的数据段 | 什么也不做 |
__NSConcreteMallocBlock__ |
堆 | 引用计数+1 |
Demo
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,函数调用结束后可能一些相关数据就释放了,存在潜在风险。
MRC 下如果函数返回值是 block,且 block 内做了 auto 变量捕获的逻辑,编译器会报错:Returning block that lives on the local stack。此时 block 应该为 __NSStackBlock__
即使编译器不报错,也存在很大风险,因为 block 创建在栈上,函数返回后栈内存可能被回收,导致后续访问野指针。
需要:
- 复制到堆:使用
copy方法将栈 block 复制到堆,延长生命周期 - 自动释放:返回堆 block 时,通常用
autorelease标记,遵循 MRC 的命名规则(非alloc/new/copy方法返回自动释放对象)
改正:
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,看看
也就是说,ARC 模式下,当 block 捕获了 auto 变量,并且作为函数返回值的时候,ARC 会自动调用 copy 方法,将 __NSStackBlock__ 变为 __NSMallocBlock__
Demo1:
MyBlock block;
{
Person *person = [[Person alloc] init];
block = ^{
NSLog(@"block called");
};
NSLog(@"%@", [block class]);
};
MRC 环境: 如果 block 不访问外部局部变量,则__NSGlobalBlock__
ARC 环境:如果 block 不访问外部局部变量,则__NSGlobalBlock__
Demo2:
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:
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__
改为 ARC
说明:ARC 模式下,如果 block 被强指针指向,则会自动调用 copy 方法。
- 捕获了 auto 变量的
__NSStackBlock__,ARC 下调用 copy 会变为__NSMallocBlock__ - 没有捕获变量的
__NSGlobalBlock__,ARC 下调用 copy 依旧为__NSGlobalBlock__
3. block 作为 Cocoa API 中的方法名含有 usingBlock 的方法参数时
NSArray *array = @[];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
4. block 作为 GCD API 的方法参数时
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 销毁后销毁
MRC 下:block 对象捕获了 auto 外部变量,是一种 __NSMallocBlock__,因为是 MRC,所以需要手动管理内存。会发现对象将在离开作用域后立马销毁,不会被 block 所捕获。
MRC,对 block 加 copy,变为 __NSMallocBlock__ 呢?
ARC 下对 block 引用的对象加 __weak 修饰呢?
用指令 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 参数
如果对 Person 不加 __weak 修饰,block 结构体内部将会是__strong。
思考:发现生成的 c++ 代码中,block_desc 里面多了2个成员变量。
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
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
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:
说明:ARC 环境下,GCD 的 block 会自动被拷贝到堆上 __NSMallocBlock__,堆上的 block 会对使用的对象进行 copy,所以 person 引用计数+1,则在 GCD block 执行完毕后才 release
Demo2
- 栈空间上的 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
因为 GCD 是给 MainQueue 添加任务的,所以是串行,2个任务前后按照3s、1s 后打印。由于最晚的一个任务是访问强指针对象,所以不会释放。等到 GCD 全部执行完后,Person 才释放。
Demo4
在给 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
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
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 源码
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++ 对象后,编译器会自动设置此标志
- isa:指向块类型的指针(栈块为
- 释放条件:
- 排除全局 block 和堆 block:全局block
__NSConcreteGlobalBlock和堆上的 block 无需释放。- 栈块:
存储在栈内存中,生命周期与函数作用域绑定。
需要手动管理:通过
_Block_copy()复制到堆时,需用_Block_release()平衡引用计数。 - 堆块:
由
_Block_copy()动态分配在堆内存,受 ARC 管理。 自动管理:ARC 会自动插入objc_release()调用,使用标准 Objective-C 对象释放机制。
- 栈块:
存储在栈内存中,生命周期与函数作用域绑定。
需要手动管理:通过
- 有效性检验:确保块结构完整且引用计数有效
- 排除全局 block 和堆 block:全局block
- 引用计数管理:
self->reserved--引用计数减 1- 引用计数归0的时候,调用 dispose_helper 方法。释放 block 捕获的对象、内存
- 释放 block 结构体内存
- 一般来说 block 由栈管理,但是被 copy、strong 等强引用作为属性或者参数,则会调用 _Block_copy 拷贝到堆上。本函数管理堆化后栈 block 的引用计数
Demo2
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
为什么上面的代码会 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 崩溃(访问野指针)。
解决方案:
-
将栈 block 改为堆 block。block 修饰改为 strong
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); } -
复制 block 到堆
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]); } -
栈帧不要回收。开启一个 RunLoop 晚退出3秒
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
分析:
-
block 本质就是结构体
-
void (^__strong strongBlock)(void) = weakBlock;看上去左侧是一个__strong修饰,其实就是结构体,所以不会像对象一样,底层调用 setter 方法。所以 block 的赋值本就是按照结构体的成员变量一个个字段简单赋值而已。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 后的关键操作
内存标记
// 典型内存分配器实现
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 内存页时:
- 触发缺页一场(Page Fault)
- 内核检查异常地址
- 发送
SIGSEGV信号 - 进程终止(崩溃)
3. iOS/Macos 优化
1. 堆内存保护优化
-
Malloc Scribble:释放后填充
0x55(调试模式默认启用) -
Zone-based Protection:
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 修饰基本数据类型
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++ 看看
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
__block int age = 27;
MyBlock block = ^{
age = 28;
};
转为 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,并修改值。
QA:为什么__block 变量的 __Block_byref_age_0 结构体并不在 block 结构体 __main_block_impl_0 中?
因为这样做可以在多个 block 中使用 __block 变量。
看个有趣的例子,验证下 __block 的效果
转换成 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++ 后如下:
分析发现:
- 对于
__block修饰对象数据,对于生成的结构体也不一样。__Block_byref_obj_0中含有2个操作内存的成员变量__Block_byref_id_object_copy、__Block_byref_id_object_dispose - 其他的逻辑和
__block修饰基本数据类型一致
注意:
-
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 里面哪个的值?
__block int age = 27;
MyBlock block = ^{
age = 28;
};
NSLog(@"%p", &age);
知道转换为c++后的效果,我们可以在代码中按照结构体,自己定义并转接到 block
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 。
// 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
__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++ 研究下
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自身的地址)大概如下
// 栈上创建 __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)时,结构体会在堆上分配
- (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 如何修改外部变量
- (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
分析:
-
被
__block修饰的 int age 将会被封装为一个结构体,该结构体内有一个__forwarding成员变量struct __Block_byref_age_0 { void *__isa; __Block_byref_age_0 *__forwarding; int __flags; int __size; int age; }; -
在给 block 赋值的时候,其成员变量
__forwarding的值是由当前结构体对象的地址赋值的__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 进行修改。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 的作用是什么?为什么这么设计
-
当 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:需要复制到堆上,进入下一步流程。
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(
-
分配堆内存并复制结构体值
为栈 Block 分配堆内存,并复制其内容:
- 内存分配:使用 malloc 分配与栈上结构体一样大的堆空间
- 结构体复制:将栈上的 block_layout 包含函数指针等都复制到堆上
struct Block_layout *dest = malloc(src->descriptor->size); memmove(dest, src, src->descriptor->size); // 复制结构体 -
更新 block 标志位
复制后设置 block 的标志位:
- 标记为堆 block:
BLOCK_NEEDS_FREE表示需要手动释放 - 初始化引用计数:引用计数设为 1(后续通过
_Block_release管理)
dest->flags &= ~BLOCK_REFCOUNT_MASK; // 清除原有引用计数 dest->flags |= BLOCK_NEEDS_FREE | 1; // 标记为堆 Block,引用计数=1 - 标记为堆 block:
-
处理捕获的变量。调用
descriptor中的copy助手函数,处理捕获的变量- 对象类型变量:对
__strong修饰的对象调用retain(符合 ARC 规则) __block变量:将栈上的__Block_byref_xxx结构体复制到堆,并更新__forwarding指针
- 对象类型变量:对
-
返回堆上的 block:最终返回堆上的 block,供后续使用
六、Block 内存引用
对于 __block 修饰的变量进行研究
Demo0
可以看到:
- block 访问了外部的变量,则会在 block 的底层实现即
__ViewController__viewDidLoad_block_impl_0内增加一个__Block_byref_age_0 *age成员变量 - 被
__block修饰的基础数据类型 int 会被编译器自动创建为一个结构体__Block_byref_age_0,其内部的成员变量 age 存储真实的值
Demo1
Demo2
Test0:
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:
__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:
_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:
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决定了新生成结构体对象里面的对象内存访问修饰符。
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 代表什么?
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
可以看到 block 放在堆上的时候(被抢指针指向、作为返回值)的时候,如果 block 内部访问了强指针指向的对象,则会发生循环引用。
可以看到 Person 对象的 dealloc 方法没有执行,里面的打印信息没有输出。
1. ARC 下
__weak、__unsafe_unretained 修饰 __block 所修饰的变量。区别在于:
-
__weak不会产生强引用,指向的对象销毁时,会自动给指针置为 nil -
__unsafe_retained不会产生强引用,不安全。当指向的对象销毁时,指针地址值不变。
@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];
方法1: __weak 修饰。__weak typeof(self) weakself = self;
方法2: __unsafe_retained 修饰。__unsafe_unretained typeof(self) weakself = self;
方法3: __block 修饰。因为此时会构成3角关系。所以需要调用 block,block 内部需要将对象设置为 nil。虽然 __block 方案也可以解决循环引用的问题,但是缺点是该 block 需要执行,方案会有限制。
__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。
-
防止对象在 block 执行过程中被释
- 弱引用的风险:
weakself是对self的弱引用,不会增加其引用计数。如果在Block开始执行后,self被其他部分释放,后续对weakself的访问将指向无效内存,导致崩溃。 - 强引用的保障:通过
__strong修饰的strongSelf,在 block 内部临时强引用self,确保在 block 执行期间self不会被释放。即使外部已经不再持有self,只要 block 在执行,strongSelf会保持self存活。
- 弱引用的风险:
-
避免悬垂指针(Dangling Pointer)
// 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]; // 访问已释放对象,崩溃! } });
九、总结
- block 本质是什么?封装了函数调用及其调用环境的 OC 对象。本质实现是一个结构体。
__block的作用是什么?可以对 block 外部的变量进行捕获,可以修改。但是需要注意内存管理相关问题。比如__weak、__unsafe_unretained、__block- 修改 NSMutableArray 不需要加
__block? 是的,如果修改 NSMutableArray 指针比如array = nil则需要加__block - block 属性修饰词为什么是 copy?没有进行 copy 操作的时候,block 就不会在堆上,对于 block 生命周期以及所使用到的内存,没办法灵活控制(由栈控制,出栈就死)。因为 block 的高频使用场景就是作为方法参数传递、作为类的属性值,所以最常见的场景是:赋值的地方不是使用的地方,所以要捕获周围环境参数和管理所捕获的内存、以及自身内存。
- 为什么会产生循环引用?
- 如果当前当前 block 对于某个变量进行捕获,变量也是强引用类型的,block 捕获变量后,block 对变量是强引用关系,当前对象(VC)对 block 是强引用关系,变量也是 VC 强引用的,就产生了循环引用。
- 用 __block 修饰:
- MRC 下不会产生循环引用
- ARC 下会产生循环引用,可以采用断环的方式解决。
