OC-Block
关于OC中的block作为属性时,为什么要要用copy修饰
@property (nonatomic, copy) void (^completionBlock)(void);
很多文章包括AI都会给出类似结论
- Block 默认分配在栈上,如果没有
copy
,当方法退出后,Block 会被销毁。- 使用
copy
修饰符确保 Block 存储在堆上,避免栈上 Block 在方法返回后被销毁,确保 Block 在属性中能够正常存活和使用。copy
是为了让 Block 在对象的生命周期内持久化,尤其是当 Block 需要在方法执行后继续使用时。
然而,随着技术的迭代优化,这些知识恐怕已经过时了,我们来看下面的demo
@interface ViewController ()@property (nonatomic, copy) void(^demoBolck)(void);@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];// Do any additional setup after loading the view.void (^tmpBlock)(void) = ^{NSLog(@"bolck called");};NSLog(@"tmpBlock %@", tmpBlock);_demoBolck = tmpBlock;NSLog(@"demoBolck %@", tmpBlock);_demoBolck();
}@end
输出结果是这样的
tmpBlock <__NSGlobalBlock__: 0x1067cf490>
demoBolck <__NSGlobalBlock__: 0x1067cf490>
bolck called
我们看到,未引用任何外部变量的block初始化之后就是 “__NSGlobalBlock__”,在全局内存中存储,然后被当前页面的demoBlock属性引用,虽然设置了copy,但是并没有复制(内存没变)。因为他已经在全局内存了,生命周期和APP是一致的,后面随时可以调用,没有复制的必要了。我们甚至使用weak修饰这个属性也可以在后续代码中调用block:
@property (nonatomic, weak) void(^demoBolck)(void);
说到这里我们稍微展开一下,根据GPT的回答,block的存储方式有三种,原文大致如下:
- 栈内存:用于存储局部变量和不捕获外部变量的
block
。栈上的block
会在离开作用域时自动销毁,名字:__NSStackBlock__。- 堆内存:用于存储捕获外部变量的
block
,以及所有动态分配的对象,名字:__NSMallocBlock__。- 全局内存:用于存储常量和全局变量,且包括不捕获外部变量的
block,名字
__NSGlobalBlock__。
有没有发现,关于栈内存和全局内存的说明是有重复的,理解起来好像是“不捕获外部变量的 block
”既可以在全局内存存储也可以在栈内存存储。这里触发了他的知识盲区,反复询问也没说明白。所以我们需要继续自己做实验来验证。
既然说如果block引用了“外部变量”就会自动到堆内存,我们试一下
@interface ViewController ()@property (nonatomic, copy) void(^demoBolck)(void);@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];// Do any additional setup after loading the view.NSString *pageName = @"ViewController";void (^tmpBlock)(void) = ^{NSLog(@"bolck called in %@", pageName);};NSLog(@"tmpBlock %@", tmpBlock);_demoBolck = tmpBlock;NSLog(@"demoBolck %@", tmpBlock);_demoBolck();
}@end
打印结果
tmpBlock <__NSMallocBlock__: 0x7b0c000fc3c0>
demoBolck <__NSMallocBlock__: 0x7b0c000fc3c0>
bolck called in ViewController
的确,引用了外部变量之后,block直接被存储到了堆内存(名字是__NSMallocBlock__),这种情况下属性的copy依然没什么卵用,copy、strong都没有什么变化,且后续调用都正常。
还做了其他实验,简单说一下
- 如果block仅引用了一个全局变量,block还会存在全局内存中
- 如果block引用了当前类的一个属性,block会存在堆内存中,和局部变量一致
众多实验中,唯独没有找到block存储在栈内存中的情况,本人孤陋寡闻,望熟悉的大哥不吝赐教。
总结
1、早期的程序设计中,如果仅仅是局部作用的block,大概率是存在栈内存中的,用完就被释放掉了。然后如果被传递给了某个类来引用,需要开发者自己通过设置属性为“copy”类型实现复制到堆内存中,以便启用引用计数来管理其生命周期,所以大家都用copy来修饰block属性。
2、现阶段的环境下(我用的Xcode Version 16.2 (16C5032a),iOS18.1,模拟器),苹果做了优化。直接根据block是否引用变量,以及所引用的变量的生命周期来控制block的存储位置,和我们给的属性修饰(copy、strong)没关系了。
为什么这么做呢?我们使用block的时候,大多数情况不会在当前作用域创建并直接调用,也就是大概率是将来的某个时候回调,那他就不是临时的,就会和我们的一些变量关联起来,需要一起管理其生命周期。如果还是老逻辑,那大概率都会从栈中复制到堆,折腾一次,优化概率远效率浪费的概率。所以这种设计逐渐被抛弃了。我没找到官方文档,仅个人猜测。
接下来
1、低版本的iOS系统我们大概率都要支持几个的,所以当下并不一定所有的版本都启用了这个机制,具体从什么时候变的我不知道,没找到机器。所以还是推荐大家继续使用copy来修饰block属性
2、 如果大家一起探讨这个问题,就要加上版本这个变量,新版本什么样,老版本什么样