「OC」小白书读书笔记——Block的相关知识(下)
「OC」小白书读书笔记——Block的相关知识(下)
引入
前面我们说到block当中捕获变量的底层原理,以及block对应的各种不同的类,现在开始讲一下block在大括号之中生成,却能够超出变量存在域可存在原因,我们看一下下面的例子
#import <Foundation/Foundation.h>
typedef int (^MultiplierBlock)(int);
// 定义一个函数,用于创建一个 block,并捕获局部变量 factor
MultiplierBlock createMultiplierBlock(int factor) {
// 这里的 factor 是局部变量
// block 捕获了 factor 的值,并在闭包中保存了一份拷贝
MultiplierBlock block = ^(int number) {
return factor * number;
};
// 返回前,将 block 从栈复制到堆上,确保它在函数返回后依然有效
return [block copy];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 调用 createMultiplierBlock 创建一个 block,捕获的 factor 值为 5
MultiplierBlock multiplier = createMultiplierBlock(5);
// 在 main 函数中调用 multiplier,即使 createMultiplierBlock 中的 factor 早已不复存在
int result = multiplier(4);
NSLog(@"Result: %d", result); // 输出结果为 20 (5 * 4)
}
return 0;
}
Block 的存储转换
- 默认情况下,block 在函数中创建时位于栈上,生命周期与局部变量类似。但在本例中,我们调用了
[block copy]
,将其复制到堆上。 - 这样做保证了 block 能够在函数返回后继续存在,因为堆内存的生命周期不依赖于当前函数的作用域。
如同上图所示这个被copy到堆上的block就是_NSConcreteMallocBlock
类,一般来说,在需要持有 block 时,只需要执行一次 copy 操作就足够了,多次调用 copy 不会导致重复复制,只是增加引用计数。
__block修饰符的作用
我们知道常规的局部变量,只能被block捕获其初始值,在修改之后并没办法对其捕获的值进行更新。我们故然可以使用静态局部变量来避免这个问题,不过因为静态局部变量在程序运行过程中是不会被释放的,所以还是要尽量少用。那还有什么别的方法来实现这个需求呢?这就是我们要讲的__block
关键字。
若在1个Block中使用__block
变量,则当该Block从栈复制到堆时,使用的所有__block
变量也必定配置在栈上。这些__block
变量也全部被从栈复制到堆。此时,Block持有__block
变量。即使在该Block已复制到堆的情形下,复制Block也对所使用的__block
变量没有任何影响。
在多个Block中使用__block
变量时,由于最初所有的Block都会被配置在栈上,所以__block
变量也会配置在栈上。当任何一个Block从栈复制到堆时,__block
变量会一并从栈复制到堆,并且被该Block持有。当其余的Block从栈复制到堆时,被复制的Block会持有__block
变量,并增加__block
变量的引用计数,如图所示。
若是在堆上废弃那么对应的block也需要被释放,内容如下:
__block
不管是修饰基础数据类型还是修饰对象数据类型,底层都是将它包装成一个对象(我这里取个名字叫__blockObj
),然后block结构体中有个指针指向__blockObj
。既然是一个对象,那block内部如何对它进行内存管理呢?
当block在栈上时,block内部并不会对__blockObj
产生强引用。
当block调用copy
函数从栈拷贝到堆中时,它同时会将__blockObj
也拷贝到堆上,并对__blockObj
产生强引用。
在 Objective-C 中,_Block_object_assign
和 _Block_object_dispose
是与 block 内存管理相关的内部函数。它们用于处理 block 在运行时的内存管理,尤其是在涉及 block 捕获和块内对象引用时。
_Block_object_assign
作用:
_Block_object_assign
是用于将一个对象赋值给一个 block 中的局部变量(例如捕获的 __block
变量)。当 block 捕获一个对象时,它需要确保对这个对象的引用在 block 执行期间是有效的。通过 _Block_object_assign
,block 会对捕获的对象进行管理,确保对象的生命周期与 block 的生命周期保持一致。
主要用途:
- 赋值与引用计数管理:它会增加捕获对象的引用计数,以确保对象在 block 执行期间不会被提前释放。这样,block 内部对捕获对象的引用不会导致对象意外被销毁。
- 引用传递:如果对象是
__block
变量,_Block_object_assign
确保 block 在内部维护正确的对象引用,而不会导致循环引用。
_Block_object_dispose
作用:
_Block_object_dispose
是用来释放 block 内部捕获的对象引用的函数。它会在 block 被销毁时,正确地减少捕获对象的引用计数,确保不会造成内存泄漏。
主要用途:
- 释放捕获对象:当 block 执行完毕并且被销毁时,
_Block_object_dispose
会减少捕获对象的引用计数,避免内存泄漏。 - 清理操作:在 block 销毁时进行清理,确保在 block 使用的对象不再被引用时,能正确释放。
__forwarding
指针
block int val = 10;
void (^blk) (void) = ^{val = 1; };
在我们使用__block
修饰符进行修饰变量的时候,我们将OC代码转化为C语言代码,可以看到代码量剧增,其中我们用__block
修饰的变量名成功就就会变成一个结构体类型的变量,__Block_byref_val_0
结构体类型的自动变量,即栈上生成的__Block_byref_val_0
结构体实例。这个结构声明如下:
struct __Block_byref_val_0 {
void *isa; // 标识符,一般不直接使用
struct __Block_byref_val_0 *forwarding; // 转发指针,始终指向最新的存储位置
int flags; // 标志位,用于记录状态
int size; // 结构体的大小
int val; // 被 __block 修饰的变量值
};
我们来讲一下这个__forwarding
指针的作用:当 block 从栈复制到堆上后,局部 __block 变量也必须从栈复制到堆上,否则在 block 执行时会访问无效的内存。__forwarding 指针的更新正是实现这一过程的关键,它确保了 block 内所有对 __block
变量的引用都指向堆上的有效拷贝,而不会停留在即将销毁的栈内存中。
这样,无论 block 在栈上还是堆上执行,所有对该 __block 变量的访问都通过 __forwarding 指针进行,确保访问的是最新、有效的内存区域。
循环引用
接下来将一个我认为比较重要的一个点就是关于block之中产生的循环引用问题
self.str = @"aa";
self.block = ^(void) {
NSLog(@"%@",self.str);
};
self.block();
运行以上代码,我们可以看到编译器出现以下警告
这条警告提示你在 block 中对 self 的捕获是强引用(strong capture),这可能会导致循环引用(retain cycle)。
以下是解决循环引用的几个方法
- 1.
weak-strong-dance
(最常用的方法) - 2.
__block修饰对象,同时置nil
强弱共舞
为解决这个问题,一个很简单也最常用的方法,就是使用weak关键词对self进行修饰
@interface ViewController ()
@property (nonatomic, copy) void (^block)(void);
@property (nonatomic, copy) NSString *name;
@end
- (void)viewDidLoad {
[super viewDidLoad];
self.name = @"man";
__weak typeof(self) weakSelf = self;
self.block = ^(void){
NSLog(@"%@",weakSelf.name);
};
self.block();
}
但如果在block的基础上还嵌套了一个block,那很有可能在执行block中的嵌套时,weak修饰的对象已经被置为nil。这时候我们如何去解决这个问题呢?我们可以使用 __strong 在内部 block 中对弱引用进行一次临时提升,能确保在整个内部 block 执行期间,对象的引用计数保持在至少 1,从而保证对象不会被提前释放。以下例子:
// MyObject.h
#import <Foundation/Foundation.h>
@interface MyObject : NSObject
@property (nonatomic, copy) void (^block)(void);
@end
// MyObject.m
#import "MyObject.h"
@implementation MyObject
- (instancetype)init {
self = [super init];
if (self) {
NSLog(@"MyObject init");
}
return self;
}
- (void)dealloc {
NSLog(@"MyObject dealloc");
}
@end
// main.m
#import <Foundation/Foundation.h>
#import "MyObject.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 创建对象并设置 block
MyObject *obj = [[MyObject alloc] init];
__weak typeof(obj) weakObj = obj;
obj.block = ^{
NSLog(@"Inside block: %@", weakObj);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"Inside delayed block: %@", weakObj);
});
};
// 执行 block,此时 obj 仍存在
obj.block();
}
// 此时 obj 已经超出 autoreleasepool 的范围,应该被销毁
// 为了观察延迟 block 的输出,我们让主运行循环等待一段时间
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
return 0;
}
我们来看以上程序,在延时的block之中,我们并没有使用__strong
对weakObj
进行再持有得到的以下结果
现在修改一下代码:
#import <Foundation/Foundation.h>
#import "MyObject.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 创建对象并设置 block
MyObject *obj = [[MyObject alloc] init];
__weak typeof(obj) weakObj = obj;
obj.block = ^{
__strong typeof(obj) strongObj = weakObj;
NSLog(@"Inside block: %@", strongObj);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"Inside delayed block: %@", strongObj);
});
};
// 执行 block,此时 obj 仍存在
obj.block();
}
// 此时 obj 已经超出 autoreleasepool 的范围,应该被销毁
// 为了观察延迟 block 的输出,我们让主运行循环等待一段时间
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
return 0;
}
其中strongObj是一个临时变量
,在block的作用域内
,当block执行完就会释放strongObj
,这种方式属于打破self对block的强引用
,依赖于中间者模式
,属于自动置为nil
,也就是自动释放
。
__block修饰变量后置nil
#import <Foundation/Foundation.h>
#import "MyObject.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 创建对象并设置 block
MyObject *obj = [[MyObject alloc] init];
__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,此时 obj 仍存在
obj.block();
}
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
return 0;
}
可以看到我们将对象赋值给一个__block
修饰的变量,在block结束后对blockObj
进行手动释放
参考文章
OC中block的底层实现原理
OC底层知识点之 - Block底层原理