您的位置:首页 > 移动开发 > IOS开发

Multithreading annd Grand Central Dispatch on ios for Beginners Tutorial-多线程和GCD的入门教程

2016-06-12 18:42 801 查看
原文链接:Multithreading and Grand Central Dispatch on iOS for Beginners Tutorial

Have you ever written an app where you tried to do something,and there was a long pause while the UI was unresponsive?This is usually a sign that your app needs multithreading!In this tutorial,you'll get hands on experience with the core multithreading API available on ios:Grand Central Dispatch.You'll take an app that doesn't use multithreading at all(and hence is very unresponsive),and convert it to use multithreading.You'll be shocked by the difference!

This tutorial assumes you are familiar with the basics of ios development.If you are completely new to ios development,you should check out some of the other tutorials on this site first.Without further ado,take a swig of soda or chew some bubble gum and begin this tutorial at the same time - and you're already on your way to multithreading!



pic:Convert a slow, unresponsive app to a speedy cheetah with Grand Central Dispatch!

Why Should I Care?

“Ahem, so why are you telling me this? Why should I care? I don’t care. What’d you have for lunch today?”

If you’re like a certain puppet, you might still be skeptical why you should care about all this multithreading business.

So let’s show you why with a practical example of an app that doesn’t use multithreading at all.

Download the starter project, open it up with Xcode, and compile and run. You’ll see a free game art pack from vickiwenderlich.com displayed on the screen:





The app is called ImageGrabber, and its job is to go through the HTML of this web page and retrieve all of the images linked within, and display them in a table view so you can look at them more closely.

The cool part is it even downloads zip files and looks for images inside the zip files, such as the free game art zip linked on the site!

Go ahead and tap the “Grab!” button to see if it works.



…waiting…



…waiting…



…waiting…






Tomato-San is angry!

Wow! It finally worked, but that took forever! The app was parsing the HTML, downloading the images and the zip file, and unzipping the zip file, all on the main thread.

The end result was the user had to sit there for a significant amount of time waiting, not sure if the app was working at all!

The consequences of this are dire: the user might quit the app, the OS might terminate the app for taking too long, or you might get an angry Tomato attacking your treehouse.

Luckily, multithreading comes to the rescue! Instead of putting all of this heavy-duty work on the main thread, we’ll move it to the background with some simple APIs provided by Apple.

Multithreading… and cats!

If you’re already familiar with the concept of multithreading, feel free to skip to the next section. But if you’re completely new – read ahead!

When you think of a program running, you can think of it like a cat with big arrow pointing to the line it’s currently on. The cat moves the arrow as the program advances through its logic, one step at a time.




Multithreading is like a bunch of cats with a arrows.
Image credit: Diego Grez

The problem with the Image Grabber app is we’re basically exhausting our poor cat by doing all the work in the main thread. So before the app could redraw the UI or respond to user events, it has to finish all of that time intensive work of downloading files, parsing HTML, etc.




Don't overwork your cat - or the main thread!
Image credit: Diego Grez

So how do we give our overworked cat a break? The solution is simple – buy more cats! (In fact I have a friend who is really good at this!)

That way your main cat can be responsible for updating the UI and responding to user events, while your other cats are going around the background downloading files, parsing HTML, and jumping on the tables (get off!)

This is the gist of multithreaded programming. Just like these cats running around performing tasks, a process is broken down into multiple threads of execution.

On iOS, the methods you’re used to implementing (like viewDidLoad, button tap callbacks, etc.) all run on the main thread. You don’t want to perform time intensive work on the main thread, or else you’ll get an unresponsive UI and an overworked cat!

Kids, Do Not Do This At Home

Let’s take a look at the current code and discuss how it works – and why it’s bad!

The root view controller in the app is WebViewController. When you tap the button (grabTapped) it gets the HTML of the current page, and passes it to the ImageListViewController.

In the ImageListViewController’s viewDidLoad, it creates a new ImageManager and calls process. This class, along with ImageInfo, contain all of the time-intensive code, such as parsing the HTML, pulling down the images off the network, and unzipping files.

Let’s see how these two files work:

ImageManager:processHTML: Uses regular expression matching to search for links in the HTML. This could potentially be time intensive, depending on how large the HTML is. For every zip file it finds, it calls retrieveZip. For every image it finds, it creates a new ImageInfo object, with the initWithSourceURL initializer.

ImageInfo:initWithSourceURL: Calls getImage to retrieve the image over the network with the synchronous [NSData dataWithContentsOfURL:…] method. Much like the [NSString stringWithContentsOfURL:…] method, this method blocks the flow of execution until it’s complete, which could be a very long time! You almost never want to use this method in your apps.

ImageInfo:retrieveZip: Similar to the above, uses the dreaded [NSData dataWithContentsOfURL:…] which halts the thread until it completes (do not use!) When it’s done, it calls processZip.

ImageInfo:processZip: Uses the ZipArchive library to save the downloaded data to disk, unzip it, and look for images inside. Writing to disk and unzipping like this can be a very slow operation, so it’s another instance of work that really shouldn’t be on the main thread.

You might also notice some calls to a delegate method of ImageManager – imageInfosAvailable. This is how the ImageManager notifies the table view when there are new entries to be displayed in the table.

Take a look through and make sure you understand the current flow of execution – and why it’s so bad. You might also find it useful to run it and look at the console log as it runs, and you’ll see some NSLog statements showing where the code is as it runs.

Once you have a good idea of how it currently works, let’s move on and improve it with some multithreading!

Downloading Asynchronously

Let’s start by replacing the slowest operation with asynchronous calls – the downloading of the files.

It’s actually not that difficult to do this with the built-in Apple classes – NSURLRequest and NSURLConnection – but I’m a fan of some wrapper classes that make this even easier – ASIHTTPRequest.

We’re going to use this to asynchronously download the files, so let’s add it to your project.

If you don’t have ASIHTTPRequest already, first download it. Once you have it downloaded, right click your ImageGrabber project entry in groups and files, select New Group, and name the new group ASIHTTPRequest. Then drag all of the files from the ASIHTTPRequest\Classes directory (ASIAuthenticationDialog.h and several others, but IMPORTANT! don’t add the subfolders such as ASIWebPageRequest, CloudFiles, S3, and Tests.) into the new ASIHTTPRequest group. Make sure “Copy items into destination group’s folder (if needed)” is selected, and click Finish.

Also repeat this for the two files in ASIHTTPRequest\External\Reachability, as these are dependencies of the project.

The last step to add ASIHTTPRequest is you need to link your project against a few required frameworks. To do this, click on your ImageGrabber project entry in Groups & Files, click the PromoTest target, choose the Build Phases tab, and expand the Link Binary with Libraries section. Click the plus button in this section, and choose CFNetwork.framework. Then repeat this for SystemConfiguration.framework and MobileCoreServices.framework.

Now it’s time to replace the old bad synchronous code with the new good asynchronouse code!

Open up ImageManager.m and make the following changes:

// Add to top of file
#import "ASIHTTPRequest.h"

// Replace retrieveZip with the following
- (void)retrieveZip:(NSURL *)sourceURL {

NSLog(@"Getting %@...", sourceURL);

__block ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:sourceURL];
[request setCompletionBlock:^{
NSLog(@"Zip file downloaded.");
NSData *data = [request responseData];
[self processZip:data sourceURL:sourceURL];
}];
[request setFailedBlock:^{
NSError *error = [request error];
NSLog(@"Error downloading zip file: %@", error.localizedDescription);
}];
[request startAsynchronous];
}


This sets up an ASIHTTPRequest with a given URL. It sets up a block of code to run when the request finishes, and one to run if the requst fails for some reason.

Then it calls startAsynchronous. This method returns immediately so the main thread can continue going about its business such as animating the UI and responding to user input. In the meantime, the OS will automatically run the code to download the zip file on a background thread, and call one of the callback blocks when it completes or fails!

Similarly, switch to ImageInfo.m and make similar changes there:

// Add to top of file
#import "ASIHTTPRequest.h"

// Replace getImage with the following
- (void)getImage {

NSLog(@"Getting %@...", sourceURL);

__block ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:sourceURL];
[request setCompletionBlock:^{
NSLog(@"Image downloaded.");
NSData *data = [request responseData];
image = [[UIImage alloc] initWithData:data];
}];
[request setFailedBlock:^{
NSError *error = [request error];
NSLog(@"Error downloading image: %@", error.localizedDescription);
}];
[request startAsynchronous];
}


This is pretty much the same as the other code – it runs the download in the background, and when it completes sets the image instance variable to the result.

Let’s see if it works! Compile and run and tap “Grab!” and viola – it quickly switches to the detail tab rather than having a super-long pause! However there’s one major problem:





The images don’t show up in the table view after they’re downloaded! You can get them to show up by scrolling the table up and down (which works because the data for the row is reloaded after it goes offscreen), but that is kind of a hack. How can we fix this?

Introducing NSNotifications

One easy way to send updates from one part of your code to another is Apple’s built-in NSNotification system.

It’s quite simple. You get the NSNotificationCenter singleton (via [NSNotificationCenter defaultCenter]) and:

If you have an update you want to send, you call postNotificationName. You just give it a unique string you make up (such as “com.razeware.imagegrabber.imageupdated”) and an object (such as the ImageInfo that just finished downloading its image).

If you want to find out when this update happens, you call addObserver:selector:name:object. In our case the ImageListViewController will want to know when this happens so it can reload the appropriate table view cell. A good spot to put this is in viewDidLoad.

Don’t forget to call removeObserver:name:object when the view gets unloaded. Otherwise, the notification system might try to call a method on an unloaded view (or worse an unallocated object), which would be a bad thing!

So let’s try this out. Open up ImageInfo.m and make the following modification:

// Add inside getImage, right after image = [[UIImage alloc] initWithData:data];
[[NSNotificationCenter defaultCenter] postNotificationName:@"com.razeware.imagegrabber.imageupdated" object:self];


So once the image is downloaded, we post a notification and pass in this object that just got updated (self).

Next switch to ImageListViewController.m and make the following modifications:

// At end of viewDidLoad
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(imageUpdated:) name:@"com.razeware.imagegrabber.imageupdated" object:nil];

// At end of viewDidUnload
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"com.razeware.imagegrabber.imageupdated" object:nil];

// Add new method
- (void)imageUpdated:(NSNotification *)notif {

ImageInfo * info = [notif object];
int row = [imageInfos indexOfObject:info];
NSIndexPath * indexPath = [NSIndexPath indexPathForRow:row inSection:0];

NSLog(@"Image for row %d updated!", row);

[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];

}


This registers for the notification in viewDidUnload, basically saying “hey call imageUpdated when this notifiation arrives!” It also deregisters appropriately in viewDidUnload.

The imageUpdated callback looks inside the array of imageInfos for the passed in object. Once it finds it, it gets the indexPath of that row, and tells the table view to reload that row.

Compile and run, and now you’ll see the images pop in as they’re downloaded!





Grand Central Dispatch and Dispatch Queues, Oh My!

There’s still a problem with our app. If you tap the “Grab!” button and keep scrolling up and down continuously as soon as the detail view loads, after the zip file downloads you’ll see the entire UI freeze as it’s saving and unzipping the zip file.

This is because the completion block in ASIHTTPRequest gets called in the main thread, and we called the code to process the zip file within the main thread:

[request setCompletionBlock:^{
NSLog(@"Zip file downloaded.");
NSData *data = [request responseData];
[self processZip:data sourceURL:sourceURL]; // Ack - heavy work on main thread!
}];


So how can we run this heavy work in the background?

Well, iOS 3.2 introduced a very simple (and very efficient) way to do this via the Grand Central Dispatch system. Basically, whenever you want to run something in the background, you just call dispatch_async and pass in some code to run.

Grand Central Dispatch will handle all of the details for you – it will create a new thread if it needs to, or reuse an old one if one is available.

When you call dispatch_async, you pass in a dispatch queue. You can think of this as an list that stores all the blocks that you pass in, first in first out.

You can make your own dispatch queues (via dispatch_create), or you can get a special dispatch queue for the main thread (via dispatch_get_main_queue). We’ll be making a background queue called “backgroundQueue” that we’ll use to run processing tasks in the background, like parsing XML or saving/unzipping zip files.

Dispatch Queues, Locks, and Cat Food

A dispatch queue is set up by default to be serial – this means only one block of code from the queue runs at a time. This can be pretty convenient, because you can use this behaviour to protect shared data.

If you aren’t familiar with locks in multithreading, think back to our earlier example about cats. What would happen if two cats wanted to go to the cat food dish at the same time? Big problems, that’s what!

But what if we made all of our cats get in a line instead. And we’d say “hey cat, if you want to access this cat dish, you have to stand in this line!” If only life were this easy!




Maybe GCD really stands for Grand Cat Dispatch?

That’s the basic idea behind using dispatch queues to protect data. You set up your code so that a particular data structure is only accessed by code running within a particular dispatch queue. Then since dispatch queues run blocks serially, you’re guaranteed that only one will access the data structure at a time.

In this app we have two data structures we have to protect:

The linkURLs array inside ImageListViewController. To protect this, we’ll structure our code so that this is only ever touched in the main thread.

The pendingZips variable inside ImageManager. To protect this, we’ll structure our code so that this is only ever touched in our “backgroundQueue”.

OK enough chat about Grand Central Dispatch – let’s try it out!

Grand Central Dispatch in Practice

Start by opening up ImageGrabber.h and make the following changes:

// Add to top of file
#import <dispatch/dispatch.h>

// Add new instance variable
dispatch_queue_t backgroundQueue;


To use Grand Central Dispatch, you first need to import . We also predeclare the dispatch queue we’ll be using to run our background processing tasks.

Next open up ImageGrabber.m and make the following changes:

// 1) Add to bottom of initWithHTML:delegate
backgroundQueue = dispatch_queue_create("com.razeware.imagegrabber.bgqueue", NULL);

// 2) Add to top of dealloc
dispatch_release(backgroundQueue);

// 3) Modify process to be the following
- (void)process {
dispatch_async(backgroundQueue, ^(void) {
[self processHtml];
});
}

// 4) Modify call to processZip inside retrieveZip to be the following
dispatch_async(backgroundQueue, ^(void) {
[self processZip:data sourceURL:sourceURL];
});

// 5) Modify call to delegate at the end of processHTML **AND** processZip to be the following
dispatch_async(dispatch_get_main_queue(), ^(void) {
[delegate imageInfosAvailable:imageInfos done:(pendingZips==0)];
});


These are all simple but important calls, so let’s discuss each one in turn.

This creates the dispatch queue. When you create a dispatch queue you need to give it a unique name as a string. One good way to create unique names is to use reverse DNS notation like this.

When you create a dispatch queue, don’t forget to release it! For this queue we’ll release it when the ImageManager is deallocated.

The old process just ran processHTML directly, hence ran it in the main thread blocking the UI as the HTML was parsed. Now, we run it in the background on the backgroundQueue we created, with a simple call to dispatch_async!

Similarly, after we download the zip we get a callback in the main thread from ASIHTTPRequeset saying “hey, I’m done!” Instead of blocking the UI as we save and unzip the zip file like we did before, now we run it on the background queue. This is also important to make sure that the pendingZips variable is protected.

We want to make sure that we call the delegate method within the context of the main thread. First, to make sure that the linkURLs array in the view controller is only accessed via the main thread, according to our strategy discussion earlier. Second because that method interacts with UIKit objects, and UIKit objects can only be used by the main thread.

That’s it! Compile and run your code, and ImageGrabber should behave much more responsively!





But Wait!

If you’ve been programming on iOS for a while, you may have heard of these fancy things called NSOperations, and operation queues. You might wonder when you should use them, and when you should use Grand Central Dispatch.

Well, NSOperations are simply an API built on top of Grand Central Dispatch. So when you’re using NSOperations, you’re really still using Grand Central Dispatch.

It’s just that NSOperations give you some fancy features that you might like. You can make some operations dependent on other operations, reorder queues after you sumbit items, and other things like that.

In fact, ImageGrabber is already using NSOperations and operation queues! ASIHTTPRequest uses them under the hood, and you can configure the operation queue it uses for different behavior if you’d like.

So which should you use? Whichever makes sense for your app. For this app it’s pretty simple so we just used Grand Central Dispatch directly, no need for the fancy features of NSOperation. But if you need them for your app, feel free to use it!

Where To Go From Here?

Here is a sample project with all of the code from the above tutorial.

You now have some practical experience with using asynchronous operations and grand central dispatch on iOS. But this tutorial has barely scratched the surface – there’s a lot more you can learn!

I’d first suggest listening to the great Apple videos related to Grand Central Dispatch. Both WWDC 2010 and 2011 have some videos that are a great introduction to what’s available.

And if you really want to get into things, Mike Ash has some great articles on Grand Central Dispatch that you might want to check out.

If you have any questions, comments, or suggestions, please join the forum discussion below!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: