Using Blocks in iOS 4: Designing with Blocks

In the first part of this series, we learned how to declare and call basic Objective-C blocks. The motivation was to understand how to effectively use the new APIs in iOS 4 that take blocks as parameters. In this installment we're going to shift our focus toward writing our own methods that take blocks. By understanding how to use blocks in your own code, you'll have another design technique in your repertoire. And you might just find that blocks make your code easier to read and maintain.

Writing Methods That Take Blocks

We left off last time with a challenge: Write a Worker class method that takes a block and repeatedly calls it a given number of times, passing in the iteration count each time. If we wanted to triple the numbers 1 through 5, for example, here's how we would call the method with an inline block:

[Worker repeat:5 withBlock:^(int number) {
    return number * 3;
}];

I often design classes this way. I start by writing the code that calls a fictitious method I need, simply as a way of shaping the API before committing to it. Once the method call feels right, I go ahead and implement the method. In this case, the method name repeat:withBlock:just doesn't feel right to me. (I know, that's the name I used in the previous article, but I've changed my mind.) The name is confusing because the method doesn't actually repeat the same thing. It iterates from 1 to the given number, and passes the block the iteration count.

So let's get started on the right foot by renaming it:

[Worker iterateFromOneTo:5 withBlock:^(int number) {
    return number * 3;
}];

There, I'm pretty happy with a class method named iterateFromOneTo:withBlock: that takes two parameters: an int representing the number of times to call the supplied block and the block itself. Now let's implement the method.

For starters, how do we declare the iterateFromOneTo:withBlock: method? Well, we need to know the types of both parameters. The first parameter is easy; it's an int. The second parameter is a block, and all blocks have an associated type. In this example, the method will take any block that accepts a single int parameter (the iteration count) and returns an int value as the result. Here's the actual block type:

int (^)(int)

Given the name of the method and its parameter types, we're ready to declare the method. This being a Worker class method, we declare it in the Worker.h file:

@interface Worker : NSObject {
}

+ (void)iterateFromOneTo:(int)limit withBlock:(int (^)(int))block;

@end

Now, block parameters can be difficult to parse at first sight. The trick is to remember that all method parameters in Objective-C have two components: the parameter type in parentheses followed by the name of the parameter. In this example, the limit parameter is an int type and the block parameter is an int (^)(int) block type. (You can name the parameter whatever you like; block isn't a special name.)

The implementation of the method over in the Worker.m file is pretty straightforward:

#import "Worker.h"

@implementation Worker

+ (void)iterateFromOneTo:(int)limit withBlock:(int (^)(int))block {
    for (int i = 1; i <= limit; i++) {
        int result = block(i);
        NSLog(@"iteration %d => %d", i, result);
    }
}

@end

It spins through a loop, calling the block with the iteration count each time through the loop and printing the block's result. Remember that once we have a block variable in scope, calling the block is just like calling a function. In this case, the block parameter references a block. So when block(i) is executed on the highlighted line, it invokes the code in the block. When the block returns, control picks back up on the next line.

We can now call the iterateFromOneTo:withBlock: method with an inline block, like so:

[Worker iterateFromOneTo:5 withBlock:^(int number) {
    return number * 3;
}];

Alternatively, instead of using an inline block, we could pass the method a predefined block variable of the appropriate type:

int (^tripler)(int) = ^(int number) {
    return number * 3;
};

[Worker iterateFromOneTo:5 withBlock:tripler];

In either case, the numbers 1 through 5 are tripled and we get this output:

iteration 1 => 3
iteration 2 => 6
iteration 3 => 9
iteration 4 => 12
iteration 5 => 15

Of course we can pass blocks that perform any arbitrary computation. Want to square all the numbers? No problem, just give the method a different block:

[Worker iterateFromOneTo:5 withBlock:^(int number) {
    return number * number;
}];

Now that our code works, let's tidy it up a bit.

Typedef Is Your Friend

Declaring block types can get messy in a hurry. Even in our simple example the function pointer syntax leaves a lot to be desired:

+ (void)iterateFromOneTo:(int)limit withBlock:(int (^)(int))block;

Imagine the block taking multiple parameters, some of which are pointer types, and you're quickly in for a world of hurt. To improve readability and eliminate duplication in the .h and .m files, we can revise our Worker.h file to use a typedef, like so:

typedef int (^ComputationBlock)(int);

@interface Worker : NSObject {
}

+ (void)iterateFromOneTo:(int)limit withBlock:(ComputationBlock)block;

@end

typedef is a C keyword that assigns a synonym to a type. Think of it as the nickname for a type that has a cumbersome real name. In this case, we've defined ComputationBlock to refer to a block type that takes an int and returns an int. Then, when defining theiterateFromOneTo:withBlock: method, we simply use ComputationBlock as the block parameter type.

Likewise, in the Worker.m file, we can simplify the code by using ComputationBlock:

#import "Worker.h"

@implementation Worker

+ (void)iterateFromOneTo:(int)limit withBlock:(ComputationBlock)block {
    for (int i = 1; i <= limit; i++) {
        int result = block(i);
        NSLog(@"iteration %d => %d", i, result);
    }
}

@end

Ah, much better! The code is easier to read and doesn't duplicate the block type syntax across files. In fact, you can use ComputationBlock in place of the actual block type anywhere in your application that imports Worker.h.

You'll run into similar typedefs in the new iOS 4 APIs. For example, the ALAssetsLibrary class defines the following method:

- (void)assetForURL:(NSURL *)assetURL      
        resultBlock:(ALAssetsLibraryAssetForURLResultBlock)resultBlock 
       failureBlock:(ALAssetsLibraryAccessFailureBlock)failureBlock

This method takes two blocks: one to invoke if the asset was found and one to invoke if the asset was not found. They're typedef'd as follows:

typedef void (^ALAssetsLibraryAssetForURLResultBlock)(ALAsset *asset);
typedef void (^ALAssetsLibraryAccessFailureBlock)(NSError *error);

You can then use ALAssetsLibraryAssetForURLResultBlock and ALAssetsLibraryAccessFailureBlock within your application to refer to the respective block types.

I recommend always using a typedef when writing a public method that takes a block. It helps keep your code tidy and expresses the intent of the block to developers who may use your code.

Another Look At Closures

You may recall that blocks are closures. We briefly touched on closures in the first part of this series, but the example wasn't particularly interesting. And I hinted that closures would become more useful when we started passing blocks around to methods. Well, now that we know how to write methods that take blocks, let's try another closure example:

int multiplier = 3;

[Worker iterateFromOneTo:5 withBlock:^(int number) {
    return number * multiplier;
}];

We're using the same iterateFromOneTo:withBlock: method we wrote earlier, but in this example the block has a subtle, yet very important, difference. Rather than hard-coding the multiplier inside the block as in previous examples, this block uses a local multiplier variable declared outside of the block. The result of running the method is the same as before; it triples the numbers 1 through 5:

iteration 1 => 3
iteration 2 => 6
iteration 3 => 9
iteration 4 => 12
iteration 5 => 15

That this code runs at all is an example of the power of closures. The code defies typical scoping rules. In particular, consider that the multiplier local variable is out of scope when the block is called from inside the iterateFromOneTo:withBlock: method.

Remember, however, that blocks also capture their surrounding state. When a block is declared it automatically takes a (read-only) snapshot of all the variables in scope that the block uses. Because our block uses the multiplier variable, the variable's value is captured in the block to be used later. That is, the multiplier variable has become a part of the state of the block. And when the block is passed to theiterateFromOneTo:withBlock: method, the block's state goes along for the ride.

OK, so what if we wanted to modify the multiplier variable inside the block? Say, for example, each time the block was called we wanted the multiplier to become the result of the last computation. You might be tempted to just assign to multiplier inside the block, like this:

int multiplier = 3;

[Worker iterateFromOneTo:5 withBlock:^(int number) {
    multiplier = number * multiplier;
    return multiplier;  // compile error!
}];

But the compiler won't let you get away with that. You'll get the error "Assignment of read-only variable 'multiplier'". This happens because a block effectively gets a const copy of stack (local) variables that it uses. These variables are immutable inside the block.

If you want to be able to modify an externally-declared variable inside a block, you need to prefix the variable with the new __block storage type modifier, like so:

__block int multiplier = 3;

[Worker iterateFromOneTo:5 withBlock:^(int number) {
    multiplier = number * multiplier;
    return multiplier;
}];

NSLog(@"multiplier  => %d", multiplier);

This code compiles and runs as follows:

iteration 1 => 3
iteration 2 => 6
iteration 3 => 18
iteration 4 => 72
iteration 5 => 360
multiplier  => 360

It's important to note that after the block runs, the value of the multiplier variable has been changed to 360. In other words, the block doesn't modify a copy of the variable. Any variable declared using the block modifier is passed by reference into the block. In fact, blockvariables are shared with all other blocks that may use that variable. A word of caution is in order here: block is not to be used casually. There is a marginal cost involved in moving things to the heap and, unless you really need to modify a variable, you shouldn't just make it a block variable.

Writing Methods That Return Blocks

Every once in a while it's handy to write a method that creates and returns a block. Let's look at a contrived (and dangerous!) example:

+ (ComputationBlock)raisedToPower:(int)y {
    ComputationBlock block = ^(int x) {
        return (int)pow(x, y);
    };
    return block;  // Don't do this!
}

This method simply creates a block that computes x raised to the power y, then returns it. It uses our old typedef'd friend ComputationBlock.

Here's how we expect to use it:

ComputationBlock block = [Worker raisedToPower:2];
block(3);  // 9
block(4);  // 16
block(5);  // 25

The block we get back remembers that we want everything raised to the power of the exponent (2 in this case). When we call the block with a number, it should return the result of raising the number to the power of the exponent. As it stands, though, if we were to run this code it would blow up with an EXC_BAD_ACCESS runtime error.

What gives? Well, the key to fixing the problem lies in understanding how blocks are allocated. A block starts its life on the stack because allocating memory on the stack is relatively fast. Stack variables, however, are destroyed when they're popped off the stack. This happens when returning from a method.

Looking back out our raisedToPower: method, we see that we're creating a block (on the stack) and returning it. That in turn causes the scope within which the block was declared to be destroyed, which includes the block. So when we go to use the returned block variable, it's a time bomb.

The fix is to move the block from the stack to the heap before returning it. That sounds complicated, but it's actually very easy. We simply call copy on the block and the block will be automatically moved to the heap. Here's the revised method that works as expected:

+ (ComputationBlock)raisedToPower:(int)y {
    ComputationBlock block = ^(int x) {
        return (int)pow(x, y);
    };
    return [[block copy] autorelease];
}

Notice that since we called copy, we must balance it with an autorelease in this case to be good stewards of memory. Otherwise the calling code would need to remember to release the block, which goes against the ownership conventions.

It's not often that you need to copy a block, but returning a block allocated inside a method is one case where it's critical to make a copy to move the block to the heap. Be careful out there!

Putting It All Together

OK, let's put some of what we learned together into a slightly more practical example. Suppose we're designing a simple class that will play movies. Users of this class want to receive a callback when a movie is done playing so they can perform application-specific logic. It turns out that blocks are a convenient way to handle callbacks.

Let's start by writing the code from the perspective of a developer using this class:

MoviePlayer *player = 
    [[MoviePlayer alloc] initWithCallback:^(NSString *title) {
        NSLog(@"Hope you enjoyed %@", title);
}];

[player playMovie:@"Inception"];

So we need a MoviePlayer class with two methods: initWithCallback: and playMovie:. The inititializer needs to take a block, stash it away, and then call the block at the end of the playMovie: method. The block takes one parameter (the movie title) and returns nothing. We'lltypedef the callback block type and use a property to hang onto the callback block. Remember, blocks are objects and you can use them as instance variables and/or properties. We'll use a property in this case to demonstrate the point.

Here's MoviePlayer.h:

typedef void (^MoviePlayerCallbackBlock)(NSString *);

@interface MoviePlayer : NSObject {
}

@property (nonatomic, copy) MoviePlayerCallbackBlock callbackBlock;

- (id)initWithCallback:(MoviePlayerCallbackBlock)block; 
- (void)playMovie:(NSString *)title;

@end

And here's the corresponding MoviePlayer.m:

#import "MoviePlayer.h"

@implementation MoviePlayer

@synthesize callbackBlock;

- (id)initWithCallback:(MoviePlayerCallbackBlock)block {
    if (self = [super init]) {
        self.callbackBlock = block;
    }
    return self;
}

- (void)playMovie:(NSString *)title {
    // play the movie
    self.callbackBlock(title);
}

- (void)dealloc {
    [callbackBlock release];
    [super dealloc];
}

@end

In initWithCallback:, we assign the supplied block to the callbackBlock property. Because the property was declared to use copy semantics, the block is automatically copied, which moves it onto the heap. Then when the playMovie: method is invoked, we call the block passing in the movie title.

Now let's say a developer wants to tie our MoviePlayer class into an application that manages a queue of movies you plan to watch. Once you've watched a particular movie, it should be removed from your queue. Here's a trivial implementation that also demonstrates a closure:

NSMutableArray *movieQueue = 
    [NSMutableArray arrayWithObjects:@"Inception", 
                                     @"The Book of Eli", 
                                     @"Iron Man 2", 
                                     nil];

MoviePlayer *player = 
    [[MoviePlayer alloc] initWithCallback:^(NSString *title) {
        [movieQueue removeObject:title];
}];

for (NSString *title in [NSArray arrayWithArray:movieQueue]) {
    [player playMovie:title];
};

Notice that the block uses the local movieQueue variable, which becomes part of the state of the block. When the block is called it removes the movie title from the movieQueue array even though it's out of scope by that time. After all the movies have been played, the movieQueue will be empty.

A couple important things worth noting here:

  • The movieQueue variable is an array pointer, and we're not modifying where it points. We're modifying its contents. Therefore, we don't need to use the __block modifier.
  • To iterate through the movie queue we needed to create a copy of the movieQueue variable. Otherwise, if we used movieQueue directly, we'd be removing elements while trying it iterate through it. This causes an exception because enumeration in Objective-C is designed to be safe.
  • Instead of using a block, we could have declared a protocol, written a delegate class, and registered the delegate as a callback. Using an inline block in this example is simply more compact and convenient.
  • New functionality can be added without changing the MoviePlayer class. Another developer might pass in a block that tweets the movie title to your Twitter account, marks the title as being played in a Core Data store, or prompts you to rate the movie. 

Next Steps

That's a wrap for this series. Thanks for tuning in! There's more to blocks, but what we've learned so far represents the majority of uses. If you want to dig even deeper, I suggest working through the following resources:

As a final takeaway, I hope you've seen how blocks offer a different style of programming that can inform the design of your application. When and where you use them is a judgement call, like any other design decision. Give blocks a try, and I look forward to your comments.

Have fun!

(Thanks to Matt Drance (@drance) and Daniel Steinberg (@dimsumthinking) for reviewing drafts of this article.)

iOS Developer Training

Want to learn how to build iOS apps from scratch? Consider attending an upcoming public iPhone/iPad Programming Studio, or scheduling a private course, taught by two experienced iOS developers. You'll come away ready to create your first iPhone/iPad app, or improve your existing app. It's a lot of fun!

原文地址:https://www.cnblogs.com/lisa090818/p/4208629.html