13

在编写接受块作为参数的方法时,我是否需要做一些特殊的事情,例如在执行之前将块复制到堆中?例如,如果我有以下方法:

- (void)testWithBlock:(void (^)(NSString *))block {
    NSString *testString = @"Test";
    block(testString);
}

block我应该在调用它之前,还是在输入方法时做任何事情?或者上面是使用传入块的正确方法吗?另外,以下调用方法的方式是否正确,或者我应该在传递之前对块做些什么?

[object testWithBlock:^(NSString *test){
    NSLog(@"[%@]", test);
}];

需要在哪里复制块?如果我不使用 ARC,这会有什么不同?

4

4 回答 4

19

当您收到一个块作为方法参数时,该块可能是在堆栈上创建的原始块,也可能是副本(堆上的一个块)。据我所知,没有办法说。所以一般的经验法则是,如果你要在接收它的方法中执行块,你不需要复制它。如果您打算将该块传递给另一个方法(可能会或可能不会立即执行它),那么您也不需要复制它(如果它打算保留它,接收方法应该复制它)。但是,如果您打算以任何方式将块存储到某个地方以供以后执行,则需要复制它。许多人使用的主要示例是某种作为实例变量保存的完成块:

typedef void (^IDBlock) (id);
@implementation MyClass{
    IDBlock _completionBlock;
}

但是,如果要将它添加到任何类型的集合类中,例如 NSArray 或 NSDictionary,则还需要复制它。否则,当您稍后尝试执行该块时,您将收到错误(很可能是 EXC_BAD_ACCESS)或数据损坏。

执行块时,首先测试块是否为nil. Objective-c 将允许您传递nil给块方法参数。如果那个块是 nil,当你尝试执行它时你会得到 EXC_BAD_ACCESS。幸运的是,这很容易做到。在您的示例中,您将编写:

- (void)testWithBlock:(void (^)(NSString *))block {
    NSString *testString = @"Test";
    if (block) block(testString);
}

复制块有性能考虑。与在堆栈上创建块相比,将块复制到堆中并非易事。一般来说,这不是什么大问题,但如果你迭代地使用一个块或迭代地使用一堆块并在每次执行时复制它们,它会造成性能损失。因此,如果您的方法- (void)testWithBlock:(void (^)(NSString *))block;处于某种循环中,如果您不需要复制该块,则复制该块可能会损害您的性能。

您需要复制块的另一个地方是如果您打算调用该块本身(块递归)。这并不常见,但如果您打算这样做,则必须复制该块。请在此处查看我关于 SO 的问题/答案:Recursive Blocks In Objective-C

最后,如果你要存储一个块,你需要非常小心地创建保留周期。块将保留传递给它的任何对象,如果该对象是实例变量,它将保留实例变量的类(self)。我个人喜欢积木并一直使用它们。但是,Apple 不为他们的 UIKit 类使用/存储块,而是坚持使用目标/动作或委托模式是有原因的。如果您(创建块的类)保留正在接收/复制/存储块的类,并且在该块中您引用自己或任何类实例变量,则您创建了一个保留循环(classA -> classB - > 块 -> A 类)。这非常容易做到,而且我已经做过太多次了。此外,“泄密” 在 Instruments 中没有抓住它。解决这个问题的方法很简单:只需创建一个临时的__weak变量(对于 ARC)或__block变量(非 ARC),并且块不会保留该变量。因此,例如,如果“对象”复制/存储块,则以下将是一个保留周期:

[object testWithBlock:^(NSString *test){
    _iVar = test;
    NSLog(@"[%@]", test);
}];

但是,要解决这个问题(使用 ARC):

__weak IVarClass *iVar = _iVar;
[object testWithBlock:^(NSString *test){
    iVar = test;
    NSLog(@"[%@]", test);
}];

你也可以这样做:

__weak ClassOfSelf _self = self;
[object testWithBlock:^(NSString *test){
    _self->_iVar = test;
    NSLog(@"[%@]", test);
}];

请注意,许多人不喜欢上面的方法,因为他们认为它很脆弱,但它是访问变量的有效方式。 更新- 如果您尝试使用“->”直接访问变量,当前编译器现在会发出警告。出于这个原因(以及安全原因),最好为要访问的变量创建一个属性。因此,_self->_iVar = test;您可以使用:_self.iVar = test;

更新(更多信息)

通常,最好将接收块的方法视为负责确定是否需要复制块,而不是调用者。这是因为接收方法可能是唯一知道块需要保持多长时间或是否需要复制的方法。您(作为程序员)在编写调用时显然会知道这些信息,但如果您在不同的对象中考虑调用者和接收者,调用者将块交给接收者并完成它。因此,它不需要知道在块消失后对块做了什么。另一方面,它' 很可能调用者可能已经复制了块(也许它存储了块并且现在将它交给另一个方法)但是接收者(也打算存储块)仍然应该复制块(即使块正如已经复制的那样)。接收者无法知道该块已经被复制,它接收到的一些块可能被复制,而其他块可能没有。因此接收者应该总是复制一个它打算保留的块?说得通?这本质上是良好的面向对象设计实践。基本上,谁拥有信息,谁就负责处理它。因此接收者应该总是复制一个它打算保留的块?说得通?这本质上是良好的面向对象设计实践。基本上,谁拥有信息,谁就负责处理它。因此接收者应该总是复制一个它打算保留的块?说得通?这本质上是良好的面向对象设计实践。基本上,谁拥有信息,谁就负责处理它。

块在 Apple 的 GCD(Grand Central Dispatch)中被广泛使用,以轻松启用多线程。一般来说,当你在 GCD 上分发一个块时,你不需要复制它。奇怪的是,这有点违反直觉(如果你考虑一下的话),因为如果你异步调度一个块,通常创建块的方法会在块执行之前返回,这通常意味着块会过期,因为它是堆栈对象。我不认为 GCD 将块复制到堆栈中(我在某处读过,但无法再次找到它),相反,我认为线程的寿命可以通过放在另一个线程上来延长。

Mike Ash 有大量关于块、GCD 和 ARC 的文章,您可能会发现它们很有用:

于 2012-05-06T20:09:51.193 回答
7

这一切看起来都不错。不过,您可能需要仔细检查 block 参数:

@property id myObject;
@property (copy) void (^myBlock)(NSString *);

……

- (void)testWithBlock: (void (^)(NSString *))block
{
    NSString *testString = @"Test";
    if (block)
    {
        block(test);
        myObject = Block_copy(block);
        myBlock = block;
    }
}

...

[object testWithBlock: ^(NSString *test)
{
    NSLog(@"[%@]", test);
}];

应该没事。而且我相信他们甚至试图逐步淘汰Block_copy(),但他们还没有。

于 2012-05-03T02:20:38.900 回答
4

正如块编程主题指南在“复制块”下所说:

通常,您不需要复制(或保留)块。仅当您希望在声明它的范围内销毁块后使用该块时,您才需要制作一份副本。

在您所描述的情况下,您基本上可以将块简单地视为您的方法的参数,就像它是一个int或其他原始类型一样。当方法被调用时,堆栈上的空间将被分配给方法参数,因此在方法的整个执行过程中块将在堆栈上(就像所有其他参数一样)。当堆栈帧在方法返回时从堆栈顶部弹出时,分配给块的堆栈内存将被释放。因此,在您的方法执行期间,该块保证是活动的,因此这里没有要处理的内存管理(在 ARC 和非 ARC 情况下)。换句话说,你的代码很好。您可以简单地调用方法内的块。

正如引用的文本所暗示的那样,唯一需要显式复制块的时候是当您希望它可以从创建它的范围之外访问时(在您的情况下,超出您的方法的堆栈帧的生命周期)。例如,假设您需要一个从 Web 获取一些数据并在获取完成时运行代码块的方法。您的方法签名可能如下所示:

- (void)getDataFromURL:(NSURL *)url completionHandler:(void(^)(void))completionHandler;

由于数据获取是异步发生的,因此您需要保留块(可能在类的属性中),然后在完全获取数据后运行块。在这种情况下,您的实现可能如下所示:

@interface MyClass

@property (nonatomic, copy) void(^dataCompletion)(NSData *);

@end



@implementation MyClass
@synthesize dataCompletion = _dataCompletion;

- (void)getDataFromURL:(NSURL *)url completionHandler:(void(^)(NSData *fetchedData))completionHandler {
    self.dataCompletion = completionHandler;
    [self fetchDataFromURL:url]; 
}

- (void)fetchDataFromURL:(NSURL *)url {
    // Data fetch starts here 
}

- (void)finishedFetchingData:(NSData *)fetchedData {
    // Called when the data is done being fetched
    self.dataCompletion(fetchedData)
    self.dataCompletion = nil; 
}

在此示例中,使用具有copy语义的属性将对Block_copy()块执行 a 并将其复制到堆中。这发生在行中self.dataCompletion = completionHandler。因此,块从-getDataFromURL:completionHandler:方法的堆栈帧移动到堆,这允许它稍后在finishedFetchingData:方法中被调用。在后一种方法中,该self.dataCompletion = nil行使属性无效并将 a 发送Block_release()到存储的块,从而释放它。

以这种方式使用属性很好,因为它基本上会为您处理所有的块内存管理(只需确保它是一个copy(或strong)属性,而不仅仅是一个retain),并且在非 ARC 和 ARC 情况下都可以工作。相反,如果您想使用原始实例变量来存储您的块并在非 ARC 环境中工作,那么Block_copy()如果您想将块保持在比作为参数传递的方法的生命周期。上面使用 ivar 而不是属性编写的相同代码如下所示:Block_retain()Block_release()

@interface MyClass {
    void(^dataCompletion)(NSData *);
}

@end



@implementation MyClass

- (void)getDataFromURL:(NSURL *)url completionHandler:(void(^)(NSData *fetchedData))completionHandler {
    dataCompletion = Block_copy(completionHandler);
    [self fetchDataFromURL:url]; 
}

- (void)fetchDataFromURL:(NSURL *)url {
    // Data fetch starts here 
}

- (void)finishedFetchingData:(NSData *)fetchedData {
    // Called when the data is done being fetched
    dataCompletion(fetchedData)
    Block_release(dataCompletion);
    dataCompletion = nil;
}
于 2012-05-05T04:34:25.330 回答
0

你知道有两种块:

  1. 存储在堆栈中的块,您明确写为 ^{...} 的块,并且在创建它们的函数返回后立即消失,就像常规堆栈变量一样。当您在它所属的函数返回后调用堆栈块时,就会发生不好的事情。

  2. 堆中的块,当您复制另一个块时获得的块,只要其他对象保留对它们的引用,这些块就会存活,就像常规对象一样。

你复制一个块的唯一原因是当你得到一个块时,或者可能是一个堆栈块(显式本地块^{...},或你不知道来源的方法参数),并且您想延长其有限的堆栈块之一的寿命,并且编译器还没有为您完成这项工作。

想一想:在实例变量中保留一个块。

在集合中添加一个块,例如 NSArray。

这些是在不确定块是否已经是堆块时应该复制块的常见示例。

请注意,当在另一个块中调用一个块时,编译器会为您执行此操作。

于 2012-05-05T06:03:28.810 回答