【问题标题】:Passing blocks in Objective-C在 Objective-C 中传递块
【发布时间】:2012-05-03 02:08:01
【问题描述】:

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

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

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

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

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

【问题讨论】:

    标签: objective-c automatic-ref-counting objective-c-blocks


    【解决方案1】:

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

    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; 处于某种循环中,如果您不需要复制该块,则复制该块可能会损害您的性能。

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

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

    【讨论】:

    • 接受稍后执行的完成处理程序的方法怎么样?调用者或方法应该复制块吗?
    • 该方法应该复制块。调用者不必知道块是如何被处理的。如果你把责任放在调用者身上,并且方法改变了它的实现(决定存储块而不是立即执行它)你必须找到该方法的每个调用者并让他们复制它。不好玩。该方法将比调用者更清楚是否需要复制块。不幸的是,调用者有责任确保它不会创建保留循环,我认为这就是为什么 Apple 几乎从不存储来自其公共 API 的块的原因。
    • @Radu 我已经更新了我的答案以添加更多信息。我还提供了一些文章链接,这些文章比这里更深入地讨论了块。
    • 那么如果你使用GCD的blocks,你不需要复制它们吗?除了复制它们之外,GCD 是否会施展魔法让它们保持活力?
    • 啊哈,我在 Apple 的文档中发现“幸运的是,采用块参数的系统例程会为您处理这个问题,因此您很少需要担心这个问题,除非您正在实现自己的 API块。”
    【解决方案2】:

    这一切看起来都不错。不过,您可能需要仔细检查 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(),但他们还没有。

    【讨论】:

    • 那么如果我不使用ARC,我是否需要在进入方法时制作块的副本,但ARC会自动执行此操作?或者即使我不使用 ARC 也会类似吗?
    • 如果您将块保留在当前方法的执行之后,您只需要复制它。如果您将其存储在非本地强块变量中,ARC 将为您复制它。如果您将其存储为 id,那么 ARC 只会保留它,这还不够好。
    • @KenThomases,我不确定我是否完全理解。你能发布一个我需要复制它的例子吗?
    • 已更新以包含您需要 copy 的情况。
    【解决方案3】:

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

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

    在您描述的情况下,您基本上可以将块简单地视为您的方法的参数,就像它是 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() 并将其复制到堆中。这发生在self.dataCompletion = completionHandler 行中。因此,块从-getDataFromURL:completionHandler: 方法的堆栈帧移动到堆,这允许稍后在finishedFetchingData: 方法中调用它。在后一种方法中,self.dataCompletion = nil 行使属性无效并向存储的块发送Block_release(),从而释放它。

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

    @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;
    }
    

    【讨论】:

    • 你可以用[completionHandler copy]代替Block_copy(completionHandler)[dataCompletion release]代替Block_release(dataCompletion)
    【解决方案4】:

    你知道有两种块:

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

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

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

    思考:在实例变量中保存一个块。

    在 NSArray 等集合中添加块。

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

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

    【讨论】:

      猜你喜欢
      • 2012-09-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-07-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-09-30
      相关资源
      最近更新 更多