文章目录
- Block底层实现
- 全局变量捕获
- block类型总结
- 四种自动将栈上操作复制到堆上的操作
-
- 1.Block做函数或方法的返回值
- 2.将Block需要强引用时
- [3.当Block作为参数传给Cocoa API时](#3.当Block作为参数传给Cocoa API时)
- 4.当Block作为参数传给GCD的API时
- Block属性在MRC与ARC下的写法区别
- MRC环境下
- ARC环境下
- 示例
- Block存储转换
- 三层拷贝详细讲解
Block底层实现
在main函数中书写如下代码:
objc
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^block)(void) = ^{
printf("block~~~\n");
};
block();
}
return 0;
}
接下来我们在终端cd进入文件目录,执行
clang -rewrite-objc main.m -o main.cpp将OC文件编译为C++文件。
main.cpp文件内容删除掉其他系统代码之后,核心部分如下:
objc
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("block~~~\n");
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = {0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
void (*block)(void) =
((void (*)())&__main_block_impl_0(
(void *)__main_block_func_0,
&__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
观察上面代码可以看到,Block本质是一个结构体对象。核心结构如下:
objc
struct __block_impl {//不block基础结构体,可以理解为所有block的父结构体
void *isa;//指向不Block类型
int Flags;//block标识
int Reserved;//预留字段
void *FuncPtr;//block执行函数
};
objc
struct __main_block_impl_0 //block结构体
void(^block)(void)
objc
static void __main_block_func_0(...)//block被执行函数
^{
printf("block~~~\n");
}
objc
struct __main_block_desc_0//block描述结构,记录block大小,复制函数,销毁函数

block执行过程:
objc
block();//实际编译如下
((__block_impl* )block)->FuncPtr(...)
block本质就是结构体加函数指针
我们看一下核心部分:
objc
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);//创建一个Block结构体对象,可以理解为 Block tmp;
struct __main_block_impl_0 *blk = &tmp;
Tmp:block对象
__main_block_func_0:block中的代码
__main_block_desc_0_DATA:block描述信息
假设一个block中捕获了一个int类型的变量,那么其C语言实现如下:

objc
__main_block_func_0//clang自动生成的函数名
static void __main_block_func_0(struct __main_block_impl_0*__scelf) {
printf("block~~~\n");
}
__文件名_block_func_编号
全局变量捕获
objc
#import <Foundation/Foundation.h>
int c = 1000;
static int d = 10000;
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
static int b = 100;
void (^block)(void) = ^{
NSLog(@"a = %d", a);
NSLog(@"b = %d", b);
NSLog(@"c = %d", c);
NSLog(@"d = %d", d);
};
a = 20;
b = 200;
c = 2000;
d = 20000;
block();
}
return 0;
}
输出结果如下:

通过终端转换为C++代码如下:
objc
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
int *b;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int *_b, int flags=0) : a(_a), b(_b) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
我们可以看见block中只捕获了两个变量,原因如下:
- 全局变量捕获
因为是全局变量,无论静态全局变量或者是全局普通变量,在哪里都可以被直接访问,所以在block内部即便是不进行捕获也可以直接访问,所以我们打印这些变量时就是最新的值
- 静态局部变量的捕获
定义的静态全局变量b被block捕获之后,在block结构体之中是以int* b的形式存储的,也就是说block捕获的其实是变量b的地址,在block内部是通过b的地址去获取修改b的值,所以block的外更改b的值会影响block内部捕获的b的值,block内部更改b的值也会影响block外面b的值。
- 普通局部变量的捕获
普通局部变量的捕获就是在一个函数或者代码块中定义类似的auto类型的变量,和局部变量不同的是,普通局部变量被block捕获后,在block底层结构体中是以int a,形式存储的,值捕获,也就是内部重新创建了一个变量用来存储捕获的值,此时block内部于外部其实是两个不同的变量,存储着相同的值。
为什么这里只是存储值,不是存储地址呢,原因就是auto类型的变量出了作用域就被自动释放了,如果指针引用会造成悬空指针。
我们看一下下面这个问题:
objc
- (void)blockTest{
// 第一种
void (^block1)(void) = ^{
NSLog(@"%p",self);
};
// 第二种
void (^block2)(void) = ^{
self.name = @"Jack";
};
// 第三种
void (^block3)(void) = ^{
NSLog(@"%@",_name);//访问实例变量时,编译器会自动转换为self->_name
};
// 第四种
void (^block4)(void) = ^{
[self name];
};
}
上述代码哪些捕获了self?
答案是全部捕获了self,OC的调用[self blockTest]时,底层都会被编译器转换成objc_msgSend(self, @selector(blockTest));可以看出,self实际是作为参数传给函数objc_msgSend的,也就是说在方法执行时,self的本质就是一个函数参数。在函数调用时,参数会被压入栈帧中,虽然self这个指针变量在方法结束后会被销毁,但是销毁的是栈上的指针变量self,而不是self指向的对象。
block类型总结
我们探索一下block的类:
objc
int main(int argc, const char * argv[]) {
@autoreleasepool {
int num = 100;
void (^block1)(void) = ^{
NSLog(@"----");
};
NSLog(@"block1的类 ------ %@", [block1 class]);
NSLog(@"block2的类 ------ %@", [^{
NSLog(@"----%d", num);
} class]);
NSLog(@"block3的类 ------ %@", [[^{
NSLog(@"----%d", num);
} copy] class]);
}
return 0;
}
输出结果如下:


__NSGlobalBlock__
如果一个block中没有访问任何变量,那么该block就是__NSGlobalBlock__,在内存中是存在数据区的,即全局区或者静态区。__NSGlobalBlock__类型的block调用copy方法其实啥都没干。就相当于一个单例。
objc
- (void)test{
void (^block)(void) = ^{
NSLog(@"-----");
};
NSLog(@"--- %@",[block class]);
NSLog(@"--- %@",[[block class] superclass]);
NSLog(@"--- %@",[[[block class] superclass] superclass]);
NSLog(@"--- %@",[[[[block class] superclass] superclass] superclass]);
}
输出结果如下:

__NSStackBlock__
如果block捕获了局部变量,那么它就是一个__NSStackBlock__,他存储在栈区,栈区的特点就是自动释放,即他的内存不受开发者控制,系统自动释放。
NSMallocBlock__
一个__NSStackBlock__类型的block调用copy方法,那么就从栈上复制到堆上。如果对一个__NSMallocBlock__类型的变量做copy操作,那么该block的引用计数➕1。完整复制。
__NSStackBlock完整继承链:
__NSMallocBlock__↓
__NSMallocBlock__//这一步其实在runtime内部存在,是中间的私有类↓
NSBlock↓
NSObject
四种自动将栈上操作复制到堆上的操作
1.Block做函数或方法的返回值
objc
- (void(^)(void))createBlock {
int num = 101;
return ^{
NSLog(@"zl");
};
}
编译器会在返回前自动调用copy操作,将其从栈上复制到堆上。
2.将Block需要强引用时
如果我们将一个栈上的Block赋值给一个使用strong修饰符修饰的变量,编译器同样会自动复制到堆上。
objc
- (void)test {
int a = 10;
// 定义一个 block,并赋值给一个强指针变量 myBlock
void (^myBlock)(void) = ^{
NSLog(@"a = %d", a);
};
myBlock();
}
当block被赋值给一个strong类型的变量时,ARC会确保block的内存安全,y因此自动将其复制到堆上。
3.当Block作为参数传给Cocoa API时
许多Cocoa API都要求传入的block必须是堆上的Block,如果传入栈上的系统会自动将其复制。
objc
[UIView animateWithDuration:1.0f animations:^{
// 这里传入的 block 会被自动复制到堆上
}];
Block在这里不是立即执行,而是被保存起来,如果block还是在栈上的话,函数返回时栈空间释放,block失效
4.当Block作为参数传给GCD的API时
GCD的API也要求block必须在堆上,这样才能在异步执行中长期有效。
objc
dispatch_async(dispatch_get_main_queue(), ^{
//传递给GCD的block会自动从栈上复制到堆上,便于在调度队列中安全的使用
});
Block属性在MRC与ARC下的写法区别
MRC环境下
建议使用copy:栈上的block不会自动复制到堆上,所以最好使用copy修饰
objc
@property (nonatomic, copy) void(^block)(void);
避免因为栈内存释放导致的崩溃或未定义行为
ARC环境下
copy和strong都可以,编译器都会自动将其从栈上复制到堆上。
objc
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);
示例
objc
typedef int (^mutiplierBlock)(int);
mutiplierBlock createMutiplierBlock(int factor);
int main(int argc, const char * argv[]) {
@autoreleasepool {
//调用创建block,捕获factor的值为5
mutiplierBlock multiplier = createMutiplierBlock(5);
int result = multiplier(4);//调用block,虽然创建函数中的factor局部变量早已释放,但是值拷贝了复制本
NSLog(@"result = %d", result);//输出结果为5 * 4
}
return 0;
}
//定义一个函数用于创建一个block,并捕获局部变量factor
mutiplierBlock createMutiplierBlock(int factor) {
mutiplierBlock block = ^(int number) {//block执行变量捕获,值拷贝副本
return factor * number;
};
return [block copy];//返回前将栈上的block拷贝到堆上
}
Block存储转换
- 默认情况下,block在函数中创建时位于栈上,生命周期与局部变量类似。调用copy将其复制到栈上


如图所示,被copy到堆上的block就是__NSConcreteMallocBlock类,多次重复拷贝不会导致重复复制,只是增加引用计数。
__block修饰符作用
常规的局部变量只能被block捕获初始值,在修改之后不会更新已捕获的值,使用静态局部变量的话,会消耗不必不要的内存,所以尽量不使用。使用__block关键字可以解决这个问题。
如果在一个Block中使用该修饰词,当该block从栈上复制到堆时,使用的所有__block变量也将全部从栈上复制到堆上,此时Block持有__block变量。
我们看一个例子:
objc
__block int a = 10;
void (^block)(void) = ^{
NSLog(@"%d",a);
};
编译器不会简单复制,而是生成一个byref结构体。
类似于__Block_byref_a
objc
struct __Block_byref_a {
void *__isa;
__Block_byref_a *__forwarding;
int __flags;
int __size;
int a;
};//Block捕获的是__Block_byref_a *

在多个Block中使用__block变量时,由于最初所有的block都会被配置在栈上,所以__block变量也会配置在栈上,在copy时都会一并复制到堆上,并被Block持有。

这里第二个复制例子,只复制了Block1,却导致Block0也复制到了堆上,这是因为两个Block共享同一个
__block变量结构,而该结构被迁移到堆时,引用它的栈上Block也被必须一起迁移。

__block不论修饰基础数据类型还是对象数据类型,底层都是将们包装成一个对象,这里我们暂时叫做__blockObj,block结构体中有一个指针指向该对象。当block在栈上时,block内部不会对__blockObj产生强引用。当block被copy到堆上时,__blockObj也会被拷贝到堆上,并对__blockObj产生强引用。
在OC中,_Block_object_assign和_Block_object_dispose是与block内存管理相关的内部函数,用于处理block在运行时的内存管理。
_block_object_assign
作用:
用于将一个对象赋值给一个block中的局部变量(例如捕获的__block变量),当block捕获一个对象时,它需要确保对这个对象的引用在block执行期间有效。通过该函数即可实现block对捕获的对象进行管理。
主要用途:
- 复制与引用计数管理:会增加对象的引用计数,确保不提前释放对象。
- 引用传递:
_Block_object_assign确保block在内部维护正确的对象引用,确保不会循环引用
_Block_object_dispose
作用:
是用来释放block内部捕获的对象引用的函数,会在block被销毁时正确的减少捕获对象的引用计数。避免造成内存泄漏
主要用途:
- 释放捕获对,避免内存泄漏
- 清理操作:在block销毁时进行清理,确保在block中使用的对象不在被引用时可以正确释放

__forwarding指针
objc
__block int val = 10;
void (^blk) (void) = ^{
val = 1;
};
我们使用__block修饰符修饰变量的时候,该变量底层会变成一个结构体类型的变量,__block_byref_val_0结构体类型的自动变量,即栈上生成的__Block_byref_val_0结构体实例
objc
struct __Block_byref_val_0 {
void* __isa;//用于兼容OC对象结构,不咋用
struct __Block_byref_val_0* __forwarding;//指向真正的数据位置
int __flags;//标志位,用于记录当前状态,是否复制到堆,是否需要释放,是否包含对象类型
int __size;//bref结构体大小
int val;//原始变量
}
/*
Stack
│
└── byref_va l
forwarding → Heap_byref_val
Heap
│
├── HeapBlock
│
└── Heap_byref_va;
forwarding → 自己
Stack_byref.forwarding → Heap_byref //关键变化
*/
当block从栈上复制到堆上后,局部
__Block变量也必须复制到堆上。不然block执行时会访问无效内存。它确保了block内所有对__block变量的引用都指向堆上的有效拷贝,而不会停留在即将销毁的栈内存中。

无论block是在栈上执行还是在堆上执行,所有对该
__block变量的访问都通过__forwarding指针,确保访问的是最新最有效的内存区域。
循环引用
示例如下:
objc
- (void)test {
self.name = @"pop";
self.myBlock = ^(void) {
NSLog(@"%@", self.name);
};
}
我们可以看到如下警告:

上面警告内容就是提示我们在block中对self的捕获是强引用。可能会导致循环引用(retain cycle)
常用解决办法:
- weak - strong
___block修饰对象,同时置为nil- 传
递对象self作为block的参数,提供给block内部使用 - 使用
NSProxy
objc
__weak typeof(self) weakSelf = self;
self.viewModel.researchSong = ^{
__strong typeof(self) strongSelf = weakSelf;
}
如果block内部没有嵌套block,那么只用
__weak就行,如果嵌套了那么就需要再使用__strong,后面这个strongSelf只是临时变量内部block执行完成之后就释放strongSelf。这种方式依赖于中介者模式,属于自动置为nil。
objc
__block typeof(obj) blockObj = obj;
obj.block = ^{
NSLog(@"Inside block: %@", blockObj);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"Inside delayed block: %@", blockObj);
blockObj = nil;
});//将对象复制给一个__block修饰的变量,在block结束后对其手动释放
}
需要手动只为nil(必须)
NSProxy虚拟类
一个抽象根类,与NSObject同级
| 特性 | 说明 |
|---|---|
| 根类 | 与 NSObject 同级 |
| 不能直接实例化 | 必须子类化 |
| 专门用于消息转发 | 不负责普通对象行为 |
| 实现代理对象 | 常用于 AOP、远程代理 |
就是一个专门用于拦截并转发消息的对象。
在OC的消息转发机制中,有三个阶段:
- 动态方法解析:
objc
resolveInstanceMethod:
- 快速转发:
objc
forwardingTargetForSelector:
- 完整转发:
objc
methodSignatureForSelector: forwardInvocation:
NSProxy主要依赖于第三阶段,
objc
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel;//返回方法签名
- (void)forwardInvocation:(NSInvocation *)invocation;//真正的消息转发
三层拷贝详细讲解
objc
__block NSObject *obj = [[NSObject alloc] init];
void (^block)(void) = ^{
NSLog(@"%@",obj);
};
如果变量使用__block修饰,就会触发三层拷贝
- 第一层拷贝
objc
_Block_copy//将block从栈复制到堆上
Stack
├─ Block
└─ __block struct
拷贝后:
Heap
├─ Block
└─ __block struct (引用栈结构体)
此时__NSStackBlock__ → __NSMallocBlock__
- 第二层拷贝
objc
_Block_byref_copy//复制__block结构体
objc
Stack Heap
┌───────────────────────┐ ┌───────────────────────┐
│ byref_obj_stack │ │ byref_obj_heap │
│ obj │ │ obj │
└───────────────────────┘ └───────────────────────┘
同时更新__forwarding指针,让所有访问都堆结构体
- 第三层拷贝
objc
_Block_object_assign//处理结构体中的对象变量,retain对象

objc
objc_retainBlock//触发block copy
_Block_copy//复制block本身,如果没有捕获变量,这一步结束
_Block_object_assign//如果Block捕获变量,Runtime会调用该方法判断变量类型,并处理(普通对象、__block变量、block)
_Block_byref_copy//如果捕获的是__block变量,调用该方法复制__block结构体
_Block_object_dispose//如果__block变量还有,上一步copy内部还会调用_Block_object_assign
_Block_byref_release//释放block捕获的变量