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

原文链接: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:

  1. 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).
  2. 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.
  3. 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:

  1. The linkURLs array inside ImageListViewController. To protect this, we’ll structure our code so that this is only ever touched in the main thread.
  2. 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.

  1. 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.
  2. When you create a dispatch queue, don’t forget to release it! For this queue we’ll release it when the ImageManager is deallocated.
  3. 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!
  4. 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.
  5. 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!

时间: 2024-10-17 10:59:53

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

[编写高质量iOS代码的52个有效方法](十)Grand Central Dispatch(GCD)

[编写高质量iOS代码的52个有效方法](十)Grand Central Dispatch(GCD) 参考书籍:<Effective Objective-C 2.0> [英] Matt Galloway 先睹为快 41.多用派发队列,少用同步锁 42.多用GCD,少用performSelector系列方法 43.掌握GCD及操作队列的使用时机 44.通过Dispatch Group机制,根据系统资源状况来执行任务 45.使用dispatch_once来执行只需要运行一次的线程安全代码 46.不

iOS开发-多线程之GCD(Grand Central Dispatch)

Grand Central Dispatch(GCD)是一个强有力的方式取执行多线程任务,不管你在回调的时候是异步或者同步的,可以优化应用程序支持多核心处理器和其他的对称多处理系统的系统.开发使用的过程中只需要将执行的任务并添加到到适当的Dispatch Queue中,GCD就能生成必要的线程并计划执行任务.Dispatch Queue更简单而且在实现符合需求的多线程任务时更有效率.Dispatch  Queue一般来说有三种方式,如下图: Serial执行的时候的先进先出,Concurrent

iOS多线程编程之Grand Central Dispatch(GCD)介绍和使用

介绍: Grand Central Dispatch 简称(GCD)是苹果公司开发的技术.以优化的应用程序支持多核心处理器和其它的对称多处理系统的系统.这建立在任务并行运行的线程池模式的基础上的.它首次公布在Mac OS X 10.6 ,iOS 4及以上也可用. 设计: GCD的工作原理是:让程序平行排队的特定任务.依据可用的处理资源,安排他们在不论什么可用的处理器核心上运行任务. 一个任务能够是一个函数(function)或者是一个block. GCD的底层依旧是用线程实现,只是这样能够让程序

IOS学习之十七:Grand Central Dispatch(GCD)编程基础

IOS学习之十七:Grand Central Dispatch(GCD)编程基础 有过编程经验的人,基本都会接触到多线程这块. 在java中以及Android开发中,大量的后台运行,异步消息队列,基本都是运用了多线程来实现. 同样在,在ios移动开发和Android基本是很类似的一种模型. 但是很多时候,在应用开发中,我们会发现本身并没有自己编码去处理一些并发的事件,去开辟新的子线程等等. (虽然一般的调用sdk发起一个网络请求,系统都是会默认给你新起一个线程去处理的). 整个程序看上去基本就是

iOS多线程编程(四)------ GCD(Grand Central Dispatch)

一.简介 是基于C语言开发的一套多线程开发机制,也是目前苹果官方推荐的多线程开发方法,用起来也最简单,只是它基于C语言开发,并不像NSOperation是面向对象的开发,而是完全面向过程的.如果使用GCD,完全由系统管理线程,我们不需要编写线程代码.只需定义想要执行的任务,然后添加到适当的调度队列(dispatch_queue).GCD会负责创建线程和调度你的任务,系统会直接提供线程管理. 二.任务和队列 GCD中有两个核心概念 (1)任务:执行什么操作 (2)队列:用来存放任务 GCD的使用就

ios 多线程开关,有关线程的一些用法和详细讲解,NSThread , NSOperation ,Grand Central Dispatch ( GCD )

IOS支持的多线程技术: 一.Thread: 1)显式创建线程:NSThreed 2)隐式创建线程:NSObject 二.Cocoa operations: NSOperation类是一个抽象类,因为我们必须使用它的两个子类. 1)NSInvocationOperation 2)NSBlockOperation ———————————————————————————— 3)NSOperationQueue(继承于NSObject) 三.Grand Central Dispatch (GCD):

【转载】iOS多线程编程之Grand Central Dispatch(GCD)介绍和使用

[转载]http://blog.csdn.net/totogo2010/article/details/8016129 iOS多线程编程之Grand Central Dispatch(GCD)介绍和使用 分类: iOS开发进阶2012-09-25 16:22 35382人阅读 评论(32) 收藏 举报 目录(?)[+] 介绍: Grand Central Dispatch 简称(GCD)是苹果公司开发的技术,以优化的应用程序支持多核心处理器和其他的对称多处理系统的系统.这建立在任务并行执行的线程

[转]iOS多线程编程之Grand Central Dispatch(GCD)介绍和使用

介绍: Grand Central Dispatch 简称(GCD)是苹果公司开发的技术,以优化的应用程序支持多核心处理器和其他的对称多处理系统的系统.这建立在任务并行执行的线程池模式的基础上的.它首次发布在Mac OS X 10.6 ,iOS 4及以上也可用. 设计: GCD的工作原理是:让程序平行排队的特定任务,根据可用的处理资源,安排他们在任何可用的处理器核心上执行任务. 一个任务可以是一个函数(function)或者是一个block. GCD的底层依然是用线程实现,不过这样可以让程序员不

iOS 多线程编程之Grand Central Dispatch(GCD)

介绍: Grand Central Dispatch 简称(GCD)是苹果公司开发的技术,以优化的应用程序支持多核心处理器和其他的对称多处理系统的系统.这建立在任务并行执行的线程池模式的基础上的.它首次发布在Mac OS X 10.6 ,iOS 4及以上也可用. 设计: GCD的工作原理是:让程序平行排队的特定任务,根据可用的处理资源,安排他们在任何可用的处理器核心上执行任务. 一个任务可以是一个函数(function)或者是一个block. GCD的底层依然是用线程实现,不过这样可以让程序员不