本文已参与新人创作礼活动,一起开启掘金创作之路。
block
参考文章:
一.本质
Block是将函数及其执行上下文封装起来的对象。
block本质上也是一个OC对象,它内部也有个isa指针
block是封装了函数调用以及函数调用环境的OC对象
二.变量捕获
为了保证block内部能够正常访问外部的变量,block有个变量捕获机制
-
对于全局变量,
不会捕获到 block 内部,访问方式为直接访问; -
对于 auto 类型的局部变量,
会捕获到 block 内部,block 内部会自动生成一个成员变量,用来存储这个变量的值,访问方式为值传递; -
对于 static 类型的局部变量,
会捕获到 block 内部,block 内部会自动生成一个成员变量,用来存储这个变量的地址,访问方式为指针传递; -
对于对象类型的局部变量,block 会
连同它的所有权修饰符一起捕获。\
为什么局部变量需要捕获,全局变量不用捕获呢?
- 作用域的原因,全局变量哪里都可以直接访问,所以不用捕获;
- 局部变量,外部不能直接访问,所以需要捕获;
- auto 类型的局部变量可能会销毁,其内存会消失,block 将来执行代码的时候不可能再去访问那块内存,所以捕获其值;
- static 变量会一直保存在内存中, 所以捕获其地址即可。
Q:self 会不会捕获到 block 内部?
会捕获。
OC 方法都有两个隐式参数,方法调用者self和方法名_cmd。
参数也是一种局部变量。
Q:_name 会不会捕获到 block 内部?
会捕获。
不是将_name变量进行捕获,而是直接将self捕获到 block 内部,因为_name是 Person 类的成员变量,_name来自当前的对象/方法调用者self(self->_name)。
如果使用self.name即调用self的getter方法,即给self对象发送一条消息,那还是要访问到self。self是局部变量,不是全局变量,所以self会捕获到 block 内部。
三.类型(block的内存管理)
block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型
impl.isa = NSConcteteStackBlock, isa会标记block是哪种类型
- 全局block = NSConcreteGlobalBlock 存放在内存的已初始化数据区域中
- 栈block = NSConcreteStackBlock 存放在内存的栈上面
- 堆上面的block = NSConcreteMallocBlock 存放在内存的堆上面
我们在何时对Block进行copy操作
对于不同类型的Block,copy的效果
- NSConcreteGlobalBlock(全局block) - copy后什么也不做
- NSConcreteStackBlock(栈block) - copy后会在堆上产生一个block
- NSConcreteMallocBlock(堆block) - copy后会增加其引用计数 问题:
P类中有个assign修饰的block,假如在方法A中,我们P.block = ^(int){***} 因为方法A是在栈上,执行完在内存中就销毁了,假如在后面我们又调用了P.block 就会崩溃!!!
比如现在声明一个成员变量Block,而在栈上创建这个Block,同时赋值给成员变量的Block。 如果没有对成员变量的Block进行Copy操作的话,当我们通过成员变量去访问对应的Block的时候, 可能会因为栈对应的函数退出之后在内存当中就销毁掉了,继续访问就会引起内存崩溃
遇到一个Block,我们怎么知道这个Block的存储位置呢?
(1)Block不访问外界变量(包括栈中和堆中的变量)
Block 既不在栈又不在堆中,在代码段中,ARC和MRC下都是如此。此时为全局块。
(2)Block访问外界变量
MRC 环境下:访问外界变量的 Block 默认存储栈中。
ARC 环境下:访问外界变量的 Block 默认存储在堆中(实际是放在栈区,然后ARC情况下自动又拷贝到堆区),自动释放。
四.block的copy
1.在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况
-
block作为函数返回值时
-
将block赋值给__strong指针时
-
block作为Cocoa API中方法名含有usingBlock的方法参数时
-
block作为GCD API的方法参数时
2.MRC下block属性的建议写法
@property (copy, nonatomic) void (^block)(void);
3.ARC下block属性的建议写法
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);
五.对象类型的auto变量
1.当block内部访问了对象类型的auto变量时
如果block是在栈上,将不会对auto变量产生强引用
2.如果block被拷贝到堆上
会调用block内部的copy函数
copy函数内部会调用_Block_object_assign函数
_Block_object_assign函数会根据auto变量的修饰符( **strong、 **weak、 __unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用
3.如果block从堆上移除
会调用block内部的dispose函数
dispose函数内部会调用_Block_object_dispose函数 _Block_object_dispose函数会自动释放引用的auto变量(release)
4.__weak问题解决
(1)在使用clang转换OC为C++代码时,可能会遇到以下问题
cannot create __weak reference in file using manual reference
(2)解决方案:支持ARC、指定运行时系统版本,比如
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
六.__block
-
__block 可以用于解决 block 内部无法修改 auto 变量值的问题;
-
__block 不能修饰全局变量、静态变量;
-
编译器会将 __block 变量包装成一个对象(
struct __Block_byref_age_0(byref:按地址传递)); -
加 __block 修饰不会修改变量的性质,它还是 auto 变量;
-
一般情况下,对被捕获变量进行赋值(赋值!=使用)操作需要添加 __block 修饰符。比如给数组添加或者删除对象,就不用加 __block 修饰;
-
在 MRC 下使用 __block 修饰对象类型,在 block 内部不会对该对象进行 retain 操作,所以在 MRC 环境下可以通过 __block 解决循环引用的问题。
1.__block的内存管理
(1)当block在栈上时,并不会对__block变量产生强引用
(2)当block被copy到堆时
会调用block内部的copy函数
copy函数内部会调用_Block_object_assign函数
_Block_object_assign函数会对__block变量形成强引用(retain)
(3)当block从堆中移除时
会调用block内部的dispose函数 dispose函数内部会调用_Block_object_dispose函数
_Block_object_dispose函数会自动释放引用的__block变量(release)
2.__forwarding指针
__block 的__forwarding指针存在的意义?
为什么要通过 age 结构体里的__forwarding指针拿到 age 变量的值,而不直接 age 结构体拿到 age 变量的值呢?
__block 的__forwarding是指向自己本身的指针,为了不论在任何内存位置,都可以顺利的访问同一个 __block 变量。
- block 对象 copy 到堆上时,内部的 __block 变量也会 copy 到堆上去。为了防止 age 的值赋值给栈上的 __block 变量,就使用了
__forwarding; - 当 __block 变量在栈上的时候,__block 变量的结构体中的
__forwarding指针指向自己,这样通过__forwarding取到结构体中的 age 给它赋值没有问题; - 当 __block 变量 copy 到堆上后,栈上的
__forwarding指针会指向 copy 到堆上的 _block 变量结构体,而堆上的__forwarding指向自己;
这样不管我们访问的是栈上还是堆上的 __block 变量结构体,只要是通过__forwarding指针访问,都是访问到堆上的 __block 变量结构体;给 age 赋值,就肯定会赋值给堆上的那个 __block 变量中的 age。
3.对象类型的auto变量、__block变量
(1)当block在栈上时,对它们都不会产生强引用
(2)当block拷贝到堆上时,都会通过copy函数来处理它们
__block变量(假设变量名叫做a)
_Block_object_assign((void** )&dst->a, (void )src->a, 8/ BLOCK_FIELD_IS_BYREF /);
对象类型的auto变量(假设变量名叫做p)
_Block_object_assign((void** )&dst->p, (void )src->p, 3/ BLOCK_FIELD_IS_OBJECT /);
(3)当block从堆上移除时,都会通过dispose函数来释放它们
__block变量(假设变量名叫做a)
_*Block_object_dispose((void** )src->a, 8/ BLOCK_FIELD_IS_BYREF /);
对象类型的auto变量(假设变量名叫做p)
_*Block_object_dispose((void** )src->p, 3/ BLOCK_FIELD_IS_OBJECT /);
4.被__block修饰的对象类型
(1)当__block变量在栈上时,不会对指向的对象产生强引用
(2)当__block变量被copy到堆时
会调用__block变量内部的copy函数
copy函数内部会调用_Block_object_assign函数
_Block_object_assign函数会根据所指向对象的修饰符( **strong、 **weak、 __unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain)
(3)如果__block变量从堆上移除
会调用__block变量内部的dispose函数
dispose函数内部会调用_Block_object_dispose函数
_Block_object_dispose函数会自动释放指向的对象(release)
七.循环引用问题
1.ARC
注意:- ARC下 用
__block解决(必须要调用 block):
缺点:必须要调用 block,而且 block 里要将指针置为 nil。如果一直不调用 block,对象就会一直保存在内存中,造成内存泄漏。
2.MRC
- MRC下可以 用
__block解决(在 MRC 下使用 __block 修饰对象类型,在 block 内部不会对该对象进行 retain 操作,所以在 MRC 环境下可以通过 __block 解决循环引用的问题)
让 block 在使用完毕后立刻释放
一、Objective-C 中 Block 的立即释放
1. 避免循环引用
循环引用会让 block 的引用计数始终大于 0,进而导致内存无法释放。
objective-c
__weak typeof(self) weakSelf = self;
self.completionBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
// 使用 strongSelf 访问 self 成员
}
};
2. 主动置空强引用
在 block 执行完成之后,把持有 block 的强引用设置为nil。
objective-c
void (^block)(void) = ^{
// 执行任务
};
block(); // 执行 block
block = nil; // 释放 block
3. 使用局部变量而非实例变量
局部变量的作用域结束后,系统会自动释放其内存。
objective-c
- (void)doSomething {
void (^localBlock)(void) = ^{
// 局部 block
};
localBlock(); // 执行后局部变量出栈,block 被释放
}
4. 使用 dispatch_once 执行单次任务
对于只需执行一次的 block,可以借助dispatch_once来保证其只执行一次,并且会自动释放。
objective-c
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行一次的代码
});
二、Swift 中闭包的立即释放
1. 避免循环引用(弱引用 / 无主引用)
swift
self.completionHandler = { [weak self] in
guard let self = self else { return }
// 使用 self
}
2. 主动置空强引用
swift
var closure: (() -> Void)? = {
// 闭包任务
}
closure?() // 执行闭包
closure = nil // 释放闭包
3. 使用局部闭包
swift
func doSomething() {
let localClosure = { [weak self] in
guard let self = self else { return }
// 使用 self
}
localClosure() // 执行后闭包被释放
}
4. 使用 autoreleasepool 加速内存回收
对于占用内存较大的操作,可以把闭包放在autoreleasepool中执行,以此加速内存的回收。
swift
autoreleasepool {
let heavyClosure = {
// 执行占用内存大的任务
}
heavyClosure()
// 闭包在此作用域结束后被释放
}
三、底层机制与注意事项
1. Block 的内存管理机制
- 栈 Block(
__NSStackBlock__):在栈上分配内存,超出作用域后会被自动释放,但这种 block 无法复制到堆上。 - 堆 Block(
__NSMallocBlock__):通过复制操作从栈转移到堆,需要通过释放引用的方式来回收内存。 - 全局 Block(
__NSGlobalBlock__):存储在全局数据区,在程序结束时才会被释放。
2. 常见导致 Block 无法释放的情况
- 实例变量对 block 有强引用,同时 block 内部又捕获了实例的强引用,从而形成循环引用。
- block 被添加到集合(如
NSArray)中,而集合的生命周期较长。 - 使用
NSTimer或CADisplayLink时,target 对 block 有强引用。
3. 验证 Block 是否被释放
可以通过在 deinit 方法中添加打印语句来验证 block 是否被释放。
objective-c
// Objective-C
- (void)dealloc {
NSLog(@"Object deallocated");
}
// Swift
deinit {
print("Object deallocated")
}
四、特殊场景处理
1. 异步任务中的 Block
在使用dispatch_async等异步方法时,要保证在任务执行完成后,不再有强引用指向 block。
objective-c
__block typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 后台任务
dispatch_async(dispatch_get_main_queue(), ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
// 更新 UI
}
// 任务完成,无强引用后 block 会被释放
});
});
2. NSTimer/CADisplayLink 中的 Block
在使用基于 block 的定时器时,要在合适的时机 invalidate 定时器。
swift
// Swift
var timer: Timer?
func startTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.doSomething()
}
}
func stopTimer() {
timer?.invalidate()
timer = nil // 释放 timer 及其 block
}
总结
要让 block 在使用后立即释放,核心在于:
-
防止循环引用,采用弱引用或无主引用来捕获 self。
-
执行完毕后,主动断开对 block 的强引用。
-
优先使用局部变量而非实例变量来持有 block。
-
针对特殊场景(如定时器、异步任务),在合适的时机终止对 block 的引用。