Effective Objective-C 2.0: Item 37: Understand Blocks

Item 37: Understand Blocks

Blocks provide closures. This language feature was added as an extension to the GCC compiler and is available in all modern Clang versions (the compiler project used for Mac OS X and iOS development). The runtime component required for blocks to function correctly is available in all versions of Mac OS X since 10.4 and iOS since 4.0. The language feature is technically a C-level feature and therefore can be used in C, C++, Objective-C, and Objective-C++ code compiled under a supported compiler and run with the block runtime present.

Block Basics

A block is similar to a function but is defined inline to another function and shares the scope of that within which it is defined. The symbol used to denote a block is the caret, followed by a scope block that contains the block’s implementation. For example, a simple block looks like this:

^{
    // Block implementation here
}

A block is simply another value and has an associated type. Just like an intfloat, or Objective-C object, a block can be assigned to a variable and then used like any other variable. The syntax for a block type is similar to that of a function pointer. Following is a simple example of a block that takes no parameters and returns nothing:

void (^someBlock)() = ^{
    // Block implementation here
};

This block defines a variable whose name is someBlock. This might look strange because the variable name is right in the middle, but once you understand the syntax, it is easy to read. The structure of the block type syntax is as follows:

return_type (^block_name)(parameters)

To define a block that returns an int and takes two ints as parameters, you would use the following syntax:

int (^addBlock)(int a, int b) = ^(int a, int b){
    return a + b;
};

A block can then be used as if it were a function. So, for example, the addBlock could be used like this:

int add = addBlock(2, 5); //< add = 7

The powerful feature that blocks allow is capturing the scope in which they are declared. This means that any variables available to the scope in which the block is declared are also available inside the block. For example, you could define a block that used another variable like this:

int additional = 5;
int (^addBlock)(int a, int b) = ^(int a, int b){
    return a + b + additional;
};
int add = addBlock(25); //< add = 12

By default, any variable captured by a block cannot be modified by the block. In the example, if the variable called additional were changed within the block, the compiler would issue an error. However, variables can be declared as modifiable by giving them the __block qualifier. For example, a block can be used in array enumeration (see Item 48) to determine how many numbers in an array are less than 2:

NSArray *array = @[@0, @1, @2, @3, @4, @5];
__block NSInteger count = 0;
[array enumerateObjectsUsingBlock:
    ^(NSNumber *number, NSUInteger idx, BOOL *stop){
        if ([number compare:@2] == NSOrderedAscending) {
            count++;
        }
    }];
// count = 2

This example also shows the use of an inline block. The block passed to theenumerateObjectsUsingBlock: method is not assigned to a local variable but rather declared inline to the method call. This commonly used coding pattern with blocks shows why they are so useful. Before blocks became part of the language, the preceding code would have to be done by passing a function pointer or selector name that the enumeration method could call. State would have to be passed in and out manually, usually through an opaque void pointer, thereby introducing additional code and splitting up the method somewhat. Declaring a block inline means that the business logic is all in one place.

When it captures a variable of object type, a block implicitly retains it. It will be released when the block itself is released. This leads to a point about blocks that is important to understand. A block itself can be considered an object. In fact, blocks respond to many of the selectors that other Objective-C objects do. Most important to understand is that a block is reference counted just like other objects. When the last reference to a block is removed, it is deallocated. In doing so, any objects that the block captures are released to balance out the block’s retain of them.

If the block is defined in an instance method of an Objective-C class, the self variable is available along with any instance variables of the class. Instance variables are always writable and do not need to be explicitly declared with __block. But if an instance variable is captured by either reading or writing to it, the self variable is implicitly captured also, because the instance variable relates to that instance. For example, consider following block within a method on a class called EOCClass:

@interface EOCClass

- (void)anInstanceMethod {
    // ...
    void (^someBlock)() = ^{
        _anInstanceVariable = @"Something";
        NSLog(@"_anInstanceVariable = %@", _anInstanceVariable);
    };
    // ...
}

@end

The particular instance of EOCClass that had the anInstanceMethod method run on is referred to as the self variable. It is easy to forget that self is captured by this sort of block because it is not used explicitly in the code. However, accessing an instance variable is equivalent to the following:

self->_anInstanceVariable = @"Something";

This is why the self variable is captured. More often than not, properties (see Item 6) will be used to access instance variables, and in this case, the self variable is explicit:

self.aProperty = @"Something";

However, it is important to remember that self is an object and is therefore retained when it is captured by the block. This situation can often lead to retain cycles being introduced if the block is itself retained by the same object to which self refers. See Item 40 for more information.

The Guts of a Block

Every object in Objective-C occupies a certain region of memory. This memory region is a different size for every object, depending on the number of instance variables and associated data contained. A block too is an object itself, since the first variable within the region of memory that a block is defined in is a pointer to a Class object, called the isa pointer (see Item 14). The rest of the memory a block uses contains the various bits of information it needs to function correctly. Figure 6.1 shows the layout of a block in detail.

Image

Figure 6.1 The memory layout of a block object

The most important thing to note in the layout is the variable called invokea function pointer to where the implementation of the block resides. The prototype of the function takes at least avoid*, which is the block itself. Recall that blocks are a simple replacement for function pointers where state is passed using an opaque void pointer. The block is wrapping up what was previously done using standard C language features into a succinct and simple-to-use interface.

The descriptor variable is a pointer to a structure that each block has, declaring the overall size of the block object and function pointers for copy and dispose helpers. These helpers are run when a block is copied and disposed of, for example, to perform any retaining or releasing, respectively, of captured objects.

Finally, a block contains copies of all the variables it captures. These copies are stored after the descriptor variable and take up as much space as required to store all the captured variables.Note that this does not mean that objects themselves are copied but rather only the variables holding pointers. When the block is run, the captured variables are read from this region of memory, which is why the block needs to be passed as a parameter into the invoke function.

Global, Stack, and Heap Blocks

When blocks are defined, the region of memory they occupy is allocated on the stack. This means that the block is valid only within the scope in which it is defined. For example, thefollowing code is dangerous:

void (^block)();
if ( /* some condition */ ) {
    block = ^{
        NSLog(@"Block A");
    };
else {
    block = ^{
        NSLog(@"Block B");
    };
}
block();

The two blocks that are defined within the if and else statements are allocated within stack memory. When it allocates stack memory for each block, the compiler is free to overwrite this memory at the end of the scope in which that memory was allocated. So each block is guaranteed to be valid only within its respective if-statement section. The code would compile without error but at runtime may or may not function correctly. If it didn’t decide to produce code that overwrote the chosen block, the code would run without error, but if it did, a crash would certainly occur.

To solve this problem, blocks can be copied by sending the block the copy message. In doing so, the block is copied from the stack to the heap. Once this has happened, the block can be used outside the scope in which it was defined. Also, once it has been copied to the heap, a block becomes a reference-counted object. Any subsequent copies do not perform a copy but instead simply increment that block’s reference count. When a heap block is no longer referenced, it needs to be released either automatically, if using ARC, or through an explicit call to release, if using manual reference counting. When the reference count drops to zero, the heap block is deallocated just like any other object is. A stack block does not need to be explicitly released, as this is handled by virtue of the fact that stack memory is automatically reclaimed: the reason that the example code was dangerous.

With this in mind, you can simply apply a couple of copy method calls to make that code safe:

void (^block)();
if ( /* some condition */ ) {
    block = [^{
        NSLog(@"Block A");
    } copy];
else {
    block = [^{
        NSLog(@"Block B");
    } copy];
}
block();

This code is now safe. If manual reference counting were used, the block too would need to be released after it is finished with.

Global blocks are another category, along with stack and heap blocks. Blocks that don’t capture any state, such as variables from the enclosing scope, do not need any state to run. The entire region of memory these blocks use is known in full at compile time; so global blocks are declared within global memory rather than being created on the stack each time they are used. Also, it is a no-op to copy a global block, and a global block is never deallocated. Such blocks are, in effect, singletons. The following is a global block:

void (^block)() = ^{
    NSLog(@"This is a block");
};

All the information required to execute this block is known at compile time; therefore, it can be a global block. This is purely an optimization to reduce unnecessary work that might be done if such simple blocks were treated like more complex blocks that require work to be done when they are copied and disposed of.

Things to Remember

Image Blocks are lexical closures for C, C++, and Objective-C.

Image Blocks can optionally take parameters and optionally return values.

Image Blocks can be stack allocated, heap allocated, or global. A stack-allocated block can be copied onto the heap, at which point it is reference counted just like standard Objective-C objects.

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值