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、 如果大家一起探讨这个问题,就要加上版本这个变量,新版本什么样,老版本什么样

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值