在块内,__block 变量和静态变量之间的实际区别是啥?

Posted

技术标签:

【中文标题】在块内,__block 变量和静态变量之间的实际区别是啥?【英文标题】:Within a block, what is the practical difference between a __block variable and a static variable?在块内,__block 变量和静态变量之间的实际区别是什么? 【发布时间】:2013-01-26 21:41:24 【问题描述】:

我想在单个块的多次调用中重用一个对象引用,我很好奇:以下两种方法之间的实际区别是什么?

使用__block 变量:

__block Widget *widget = [self buildNewWidget];

for(Gadget *gadget in self.gadgets) 
    [self useGadget:gadget withCallback:^
        if([widget isBroken]) 
            widget = [self buildNewWidget];
        

        gadget.widget = widget;
    ];

使用static 变量:

for(Gadget *gadget in self.gadgets) 
    [self useGadget:gadget withCallback:^
        static Widget *widget;

        if(!widget || [widget isBroken]) 
            widget = [self buildNewWidget];
        

        gadget.widget = widget;
    ];

显然,这两个代码块从语义角度来看是不同的,但(实际上)我相信它们做的是相同的基本工作。我的猜测是,从内存管理角度、性能角度或其他角度来看是有区别的。任何能说明这些差异(或解释为什么它们没有不同)的见解都会有所帮助。

【问题讨论】:

现在不能输入完整的答案,但是如果你把它们放在一个将被多次调用的函数中,它们的行为会有所不同(前者每次都会创建一个新的小部件,后者不会)。 @jtbandes 我认为这不是真的;我很想看到更多关于你的意思的解释。基于实验,这两种实现都有效地“记住”了最后一个构建的小部件并保持引用直到调用下一个块。我错过了什么吗? 下一个块,是的——但封闭范围也很重要。 __block Widget * 变量是局部范围的,但 static Widget * 是文件的全局变量(即使它在块外不可见)。 对不起,我现在明白你的意思了。这也是拉米回答他的方向。你是完全正确的——如果这是一个可以被多次调用的方法的两个实现,那么行为是非常不同的。好点子。 您的代码似乎假定为 ARC;值得明确说明。 【参考方案1】:

一个例子胜过千言万语:

(是的,这是一个非常简单的例子,但它基本上等同于你所做的......)

for (int i = 0; i < 3; i++)

    // Your example encompasses this scope,
    // not taking into account that we may execute this code multiple times:

    // Call the block
    (^
        // Every instance/execution of this block will have the same object.
        static Obj *o;

        // Initialize static object
        static dispatch_once_t once;
        dispatch_once(&once, ^
            o = [Obj new];
        );

        NSLog(@"Object is: %@", o);
    )();

// Output:
//   Object is: <Obj: 0x100109fd0>
//   Object is: <Obj: 0x100109fd0>
//   Object is: <Obj: 0x100109fd0>

for (int i = 0; i < 3; i++)

    __block Obj *o = [Obj new];

    // Call the block
    (^
        // This block uses the object from its enclosing scope, which may be different.
        NSLog(@"Object is: %@", o);
    )();

// Output:
//   Object is: <Obj: 0x105100420>
//   Object is: <Obj: 0x1003004f0>
//   Object is: <Obj: 0x105300000>

【讨论】:

【参考方案2】:

出于此答案的目的,假设两个示例都包含在 -(void)useGadgetsOnWidgets ... 中。

假设 ARC,您的应用程序是单线程的并且代码是不可重入的(即useGadgetsOnWidgets 不会调用自身),并且在方法返回后不使用该块,主要区别在于:

使用static 变量,widget 将永远存在。这意味着小部件在对-useGadgetsOnWidgets 的调用中被重用(这可能是好是坏),但也意味着小部件将永远保留。您可以通过将小部件从循环/块中拉出来更改此设置(我也在开始时将其初始化为更类似于 __block 版本:

-(void)useGadgetsOnWidgets 
  static Widget *widget;
  widget = [self buildNewWidget];
  for(Gadget *gadget in self.gadgets) 
    [self useGadget:gadget withCallback:^
      if([widget isBroken]) 
        widget = [self buildNewWidget];
      
      gadget.widget = widget;
    ];
  
  widget = nil;

还有第三种变体,它在一定程度上是线程安全的,并假定在方法返回后不使用该块:

-(void)useGadgetsOnWidgets 
  Widget *widget = [self buildNewWidget];
  Widget ** pWidget = &widget;
  for(Gadget *gadget in self.gadgets) 
    [self useGadget:gadget withCallback:^
      if([*pWidget  isBroken]) 
        *pWidget = [self buildNewWidget];
      
      gadget.widget = *pWidget ;
    ];
  

这似乎比使用static 变量(实际上只是一个全局变量)要好一些,但它仍然很棘手。也不是我想教给新手程序员的技术(但话又说回来,任何类型的多线程也不是)。

编辑:对于您描述的问题,比其中任何一个更好的解决方案是将小部件缓存在self 上的 ivar/property 中:

-(Widget*)workingWidget 
  // Assuming _cachedWidget is an ivar
  if ([_cachedWidget isBroken]) 
    _cachedWidget = [self buildWidget];
  
  return _cachedWidget;


-(void)useGadgetsOnWidgets 
  for(Gadget *gadget in self.gadgets) 
    [self useGadget:gadget withCallback:^
      gadget.widget = [self workingWidget];
    ];
  

【讨论】:

您使用 useGadgetsOnWidgets 的“第三种变体”进入了一个奇怪的地方,但我喜欢您的最后一次重构,在使用 [self workingWidget] 编辑之后...感觉就像一种非常“可可”的方式来解决这个问题。 "使用静态变量,小部件将永远存在。" - 这是否意味着它占用的内存空间与 __block 重用的变量完全不同?您知道这种差异是否会对性能产生影响吗? 是的。 static 使其有效地成为全局变量,__block 使其以可以复制到堆的方式包装。 (当然,Widget 本身并没有什么不同;只是变量的位置不同。)static 应该稍微快一些,因为包装了访问__block 的代码无论变量是否被复制到堆中,变量都需要工作,因此它要经过一个额外的间接层。我没有做过任何基准测试,但它可能无关紧要(按方法调用的顺序)。【参考方案3】:

正如所写,这两个代码片段的工作方式不同,并且它们具有不同的最终结果。

第二组代码是等待发生的故障。如果由于使用了静态变量,此代码同时在两个不同的线程上运行,它将失败。此代码也将失败,因为您从未初始化静态变量。第一次到达if 语句时,应用程序可能会崩溃。

由于循环的每次迭代似乎都依赖于widget 的当前值,因此您需要在循环之前初始化一个局部变量。由于需要在块内修改此变量,因此您需要将变量设为__block 变量。这意味着您的第一组代码是正确的代码。

【讨论】:

老实说,我不记得 static 是否设置为 nil。我从不依赖这些东西。显式初始化变量既简单又清晰。即使变量被初始化,它仍然是脆弱的代码,不是线程安全的。 因为如果它被初始化为 nil ,它只会在多线程中变得不安全。相反,如果他第一次调用它,因为 !widget 为 true,它将被分配。 @RamyAlZuhouri 如果静态没有显式初始化为nil,则此代码的第二次使用将从上一次使用该代码的最后一个值开始。这是非常不可能的。 他可能想一直使用同一个小部件,除非它坏了。 为了让这个对话集中在正轨上,让我们假设我可以保证(比如通过互斥锁或其他实现细节)这些块永远不会同时执行。静态指针被隐式初始化为 nil。无论哪个块首先执行,都会首先调用“buildNewWidget”,这对于我的实现来说是完全可以接受的。【参考方案4】:

__block 使变量在块内可用,就像它是全局的一样。如果你在块内使用它,它不会被复制,而是被引用,因为它就像全局一样,它仍然是活动的。但是下次你调用该代码块时,另一个变量将被压入堆栈。

static 使变量仅在范围内可见,并且它在程序的整个执行过程中仍然存在。但是,如果您再次调用该代码块,变量将是相同的。

【讨论】:

static 与其他变量一样,仅在定义它的大括号内可见。在这种情况下,static 仅在回调块内可见。也许这就是你所说的“功能”。 我认为这不太正确 - 范围内的任何变量都可以在块内使用,但如果没有 __block,则会对该变量进行复制,因此在块内对其进行更改'不反映在块之外。 当您考虑多次调用这两个代码块时会发生什么时,肯定会有很大的不同,就像它们是可以一次又一次调用的方法的两个不同实现一样。感谢您说明这一根本区别,理解这一点很重要。 @CarlVeazey 如果没有__block 关键字,尝试修改块内的变量是错误的。没有复制。 @rmaddy 你是对的,感谢您的更正!不过,您不需要__block 来引用块范围内的变量,尽管看起来答案已经更新以反映这一点。

以上是关于在块内,__block 变量和静态变量之间的实际区别是啥?的主要内容,如果未能解决你的问题,请参考以下文章

block块中引用成员变量引起内存泄漏问题

为啥实例变量在块内时似乎消失了?

block的基本理解

AFNetworking 未在块内同步返回数据

C 内存管理初步了解

blcok的总结