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

Grand Central Dispatch Tutorial for Swift: Part 2/2

2015-06-10 14:11 615 查看
原文地址:http://www.raywenderlich.com/79150/grand-central-dispatch-tutorial-swift-part-2

If you're new here, you may want to subscribe to my RSS
feed or follow me on Twitter.
Thanks for visiting!





Learn about concurrency in this Grand Central Dispatch in-depth tutorial series.

Update April 10, 2015: Updated for Xcode 6.3 / Swift 1.2

Update note: This tutorial was updated for iOS 8 and Swift by Bjørn
Ruud. Original
post by Tutorial Team member Derek
Selander.

Welcome to the second and final part of this Grand Central Dispatch tutorial series!

In the first part of this
series, you learned about concurrency, threading, and how GCD works. You made the
PhotoManager
singleton
thread safe for reading and writing of photos using a combination of
dispatch_barrier_async
and
dispatch_sync
.
In addition to all that, you enhanced the UX of the app through the timing of a prompt with
dispatch_after
,
and offloaded the work from the instantiation of a view controller to perform a CPU intensive task with
dispatch_async
.

If you have been following along, you can pick up where you left off with the sample project form Part 1. If you haven’t completed Part 1 or don’t want to reuse your project you candownload
the finished project from the first part of this tutorial here.

It’s time to explore some more GCD!


Correcting the Premature Popup

You may have noticed that when you try to add photos with the Le Internet option, an
alert view pops up well before the images have finished downloading, as shown in the screenshot below:





The fault lies with
PhotoManager
’s
downloadPhotosWithCompletion
which
has been reproduced below:

func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) {
var storedError: NSError!
for address in [OverlyAttachedGirlfriendURLString,
SuccessKidURLString,
LotsOfFacesURLString] {
let url = NSURL(string: address)
let photo = DownloadPhoto(url: url!) {
image, error in
if error != nil {
storedError = error
}
}
PhotoManager.sharedManager.addPhoto(photo)
}

if let completion = completion {
completion(error: storedError)
}
}

Here you call the
completion
closure at the end
of the method — you’re assuming all of the photo downloads have completed. But unfortunately, there’s no guarantee that all the downloads have finished by this point.

The
DownloadPhoto
class’s instantiation method
starts downloading a file from a URL and returns immediatelybefore the download completes. In other words,
downloadPhotosWithCompletion
calls
its own completion closure at the end, as if its own method body were all straight-line synchronous code and every method call that completed had finished its work.

However,
DownloadPhoto(url:)
is asynchronous and
returns immediately — so this approach won’t work.

Instead,
downloadPhotosWithCompletion
should call
its own completion closure only after all the image download tasks have called their own completion closures. The question is: how do you monitor concurrent asynchronous events? You don’t know when they will complete, and they can finish in any order.

Perhaps you could write some hacky code that uses multiple
Bool
values
to keep track of each download, but that doesn’t scale well, and frankly, it makes for pretty ugly code.

Fortunately, this type of multiple asynchronous completion monitoring is exactly what dispatch groups were
designed for.


Dispatch Groups

Dispatch groups notify you when an entire group of tasks completes. These tasks can be either asynchronous or synchronous and can even be tracked from different queues. Dispatch groups also notify you in synchronous or asynchronous fashion when all of the group’s
events are complete. Since items are being tracked on different queues, an instance of
dispatch_group_t
keeps
track of the different tasks in the queues.

The GCD API provides two ways to be notified when all events in the group have completed.

The first one,
dispatch_group_wait
, is a function
that blocks your current thread and waits until either all the tasks in the group have completed, or until a timeout occurs. This is exactly what you want in this case.

Open PhotoManager.swift and replace
downloadPhotosWithCompletion
with
the following implementation:

func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) {
dispatch_async(GlobalUserInitiatedQueue) { // 1
var storedError: NSError!
var downloadGroup = dispatch_group_create() // 2

for address in [OverlyAttachedGirlfriendURLString,
SuccessKidURLString,
LotsOfFacesURLString]
{
let url = NSURL(string: address)
dispatch_group_enter(downloadGroup) // 3
let photo = DownloadPhoto(url: url!) {
image, error in
if let error = error {
storedError = error
}
dispatch_group_leave(downloadGroup) // 4
}
PhotoManager.sharedManager.addPhoto(photo)
}

dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER) // 5
dispatch_async(GlobalMainQueue) { // 6
if let completion = completion { // 7
completion(error: storedError)
}
}
}
}

Taking each numbered comment in turn, you’ll see the following:

Since you’re using the synchronous
dispatch_group_wait
which
blocks the current thread, you use
dispatch_async
to
place the entire method into a background queue to ensure you don’t block the main thread.

This creates a new dispatch group which behaves somewhat like a counter of the number of uncompleted tasks.

dispatch_group_enter
manually notifies a group
that a task has started. You must balance out the number of
dispatch_group_enter
calls
with the number of
dispatch_group_leave
calls
or else your app will crash.

Here you manually notify the group that this work is done. Again, you’re balancing all group enters with an equal amount of group leaves.

dispatch_group_wait
waits until either all of
the tasks are complete or until the time expires. If the time expires before all events complete, the function will return a non-zero result. You could put this into a conditional closure to check if the waiting period expired; however, in this case you specified
for it to wait forever by supplying
DISPATCH_TIME_FOREVER
.
This means, unsurprisingly, it’ll wait forever! That’s fine, because the completion of the photos creation will always complete.

At this point, you are guaranteed that all image tasks have either completed or timed out. You then make a call back to the main queue to run your completion closure. This will append work onto the main thread to be executed at some later time.

Finally, run the completion closure if one was supplied.

Build and run your app, attempt to download multiple images and notice how your app behaves with the completion closure in place.

Note: If the network activities occur too quickly to discern when the completion closure should be called
and you’re running the app on a device, you can make sure this really works by toggling some network settings in the Developer
Section
of the Settings app. Just go to the Network
Link Conditioner
section, enable it, and select a profile. “Very Bad Network” is a good choice.

If you are running on the Simulator, you can use the Network
Link Conditioner included in the Hardware IO Tools for Xcode to change your network speed. This is a good tool to have in your arsenal because it forces you to be conscious of what happens to your apps when connection speeds are less than optimal.

This solution is good so far, but in general it’s best to avoid blocking threads if at all possible. Your next task is to rewrite the same method to notify you asynchronously when all the downloads have completed.

Before we head on to another use of dispatch groups, here’s a brief guide on when and how to use dispatch groups with the various queue types:

Custom Serial Queue: This is a good candidate for notifications when a group of tasks
completes.

Main Queue (Serial): This is a good candidate as well in this scenario. You should
be wary of using this on the main queue if you are waiting synchronously for the completion of all work since you don’t want to hold up the main thread. However, the asynchronous model is an attractive way to update the UI once several long-running tasks finish,
such as network calls.

Concurrent Queue: This as well is a good candidate for dispatch groups and completion
notifications.


Dispatch Groups, Take Two

That’s all well and good, but it’s a bit clumsy to have to dispatch asynchronously onto another queue and then block using dispatch_group_wait.
There’s another way…

Find
downloadPhotosWithCompletion
in PhotoManager.swift and
replace it with this implementation:

func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) {
// 1
var storedError: NSError!
var downloadGroup = dispatch_group_create()

for address in [OverlyAttachedGirlfriendURLString,
SuccessKidURLString,
LotsOfFacesURLString]
{
let url = NSURL(string: address)
dispatch_group_enter(downloadGroup)
let photo = DownloadPhoto(url: url!) {
image, error in
if let error = error {
storedError = error
}
dispatch_group_leave(downloadGroup)
}
PhotoManager.sharedManager.addPhoto(photo)
}

dispatch_group_notify(downloadGroup, GlobalMainQueue) { // 2
if let completion = completion {
completion(error: storedError)
}
}
}

Here’s how your new asynchronous method works:

In this new implementation you don’t need to surround the method in a
dispatch_async
call
since you’re not blocking the main thread.

dispatch_group_notify
serves as the asynchronous
completion closure. This code executes when there are no more items left in the dispatch group and it’s the completion closure’s turn to run. You also specify on which queue to run your completion code. Here, the main queue is the one you want.

This is a much cleaner way to handle this particular job and doesn’t block any threads.


The Perils of Too Much Concurrency

With all of these new tools at your disposal, you should probably thread everything, right!?





Take a look at
downloadPhotosWithCompletion
in
PhotoManager
.
You might notice that there’s a
for
loop in there
that cycles through three iterations and downloads three separate images. Your job is to see if you can run this
for
loop
concurrently to try and speed it up.

This is a job for
dispatch_apply
.

dispatch_apply
acts like a
for
loop
which executes different iterations concurrently. This function is sychronous, so just like a normal
for
loop,
dispatch_apply
returns
only when all of the work is done.

Care must be taken when figuring out the optimal amount of iterations for any given amount of work inside the closure, since many iterations and a small amount of work per iteration can create so much overhead that it negates any gains from making the calls
concurrent. The technique known as striding helps you out here. This is where for
each iteration you do multiple pieces of work.

When is it appropriate to use
dispatch_apply
?

Custom Serial Queue: A serial queue would completely negate the use of
dispatch_apply
;
you might as well just use a normal
for
loop.

Main Queue (Serial): Just as above, using this on a serial queue is a bad idea. Just
use a normal
for
loop.

Concurrent Queue: This is a good choice for concurrent looping, especially if you need
to track the progress of your tasks.

Head back to
downloadPhotosWithCompletion
and
replace it with the following implementation:

func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) {
var storedError: NSError!
var downloadGroup = dispatch_group_create()
let addresses = [OverlyAttachedGirlfriendURLString,
SuccessKidURLString,
LotsOfFacesURLString]

dispatch_apply(addresses.count, GlobalUserInitiatedQueue) {
i in
let index = Int(i)
let address = addresses[index]
let url = NSURL(string: address)
dispatch_group_enter(downloadGroup)
let photo = DownloadPhoto(url: url!) {
image, error in
if let error = error {
storedError = error
}
dispatch_group_leave(downloadGroup)
}
PhotoManager.sharedManager.addPhoto(photo)
}

dispatch_group_notify(downloadGroup, GlobalMainQueue) {
if let completion = completion {
completion(error: storedError)
}
}
}

Your loop is now running concurrently; in the code above, in the call to dispatch_apply,
you supply the amount of iterations with the first parameter, the queue to perform the tasks on in the second parameter and the closure action in the third parameter.

Be aware that although you have code that will add the photos in a thread safe manner, the ordering of the images could be different depending on which thread finishes first.

Build and run, then add some photos from Le Internet. Notice anything different?

Running this new code on the device will occasionally produce marginally faster results. But was all this work worth it?

Actually, it’s not worth it in this case. Here’s why:

You’ve probably created more overhead running the threads in parallel than just running the
for
loop
in the first place. You should use
dispatch_apply
for
iterating over very large sets along with the appropriate stride length.

Your time to create an app is limited — don’t waste time pre-optimizing code that you don’t know is broken. If you’re going to optimize something, optimize something that is noticeable and worth your time. Find the methods with the longest execution times by
profiling your app in Instruments. Check out How
to Use Instruments in Xcode to learn more.

Typically, optimizing code makes your code more complicated for yourself and for other developers coming after you. Make sure the added complication is worth the benefit.

Remember, don’t go crazy with optimizations. You’ll only make it harder on yourself and others who have to wade through your code.


Cancelling Dispatch Blocks

New in iOS 8 and OS X Yosemite is the introduction of dispatch block objects. These
are implemented as a wrapper around ordinary closures and behave just like them. Dispatch block objects can do a number of things, like set a Quality of Service class per object for internal prioritization in a queue, but most notably is the ability to cancel
the execution of block objects. Be aware that a block object can only be cancelled before it reaches the head of a queue and start executing.

Let’s demonstrate this by starting download tasks for several copies of images from Le Internet,
and then cancelling some of them. Select PhotoManager.swift and replace
downloadPhotosWithCompletion
with
the following:

func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) {
var storedError: NSError!
let downloadGroup = dispatch_group_create()
var addresses = [OverlyAttachedGirlfriendURLString,
SuccessKidURLString,
LotsOfFacesURLString]
addresses += addresses + addresses // 1
var blocks: [dispatch_block_t] = [] // 2

for i in 0 ..< addresses.count {
dispatch_group_enter(downloadGroup)
let block = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS) { // 3
let index = Int(i)
let address = addresses[index]
let url = NSURL(string: address)
let photo = DownloadPhoto(url: url!) {
image, error in
if let error = error {
storedError = error
}
dispatch_group_leave(downloadGroup)
}
PhotoManager.sharedManager.addPhoto(photo)
}
blocks.append(block)
dispatch_async(GlobalMainQueue, block) // 4
}

for block in blocks[3 ..< blocks.count] { // 5
let cancel = arc4random_uniform(2) // 6
if cancel == 1 {
dispatch_block_cancel(block) // 7
dispatch_group_leave(downloadGroup) // 8
}
}

dispatch_group_notify(downloadGroup, GlobalMainQueue) {
if let completion = completion {
completion(error: storedError)
}
}
}

The
addresses
array is expanded to hold three
of each address.

This array will hold the created block objects for later use.

dispatch_block_create
creates a new block object.
The first parameter is a flag defining various block traits. The flag used here makes the block inherit its QoS class from the queue it is dispatched to. The second parameter is the block definition in the form of a closure.

Here the block is dispatched asynchronously to the global main queue. For this example using the main queue makes it easier to cancel select blocks since it’s a serial queue. The code that sets up the dispatch blocks is already executing on the main queue so
you are guaranteed that the download blocks will execute at some later time.

The first three downloads are left alone, and the array is sliced to get the rest.

arc4random_uniform
provides an integer between
0 and an upper bound (not inclusive). By using 2 for the upper bound you get either a 0 or a 1, like a coin toss.

If the random number is 1 the block is cancelled. That is, if the block is still in a queue and has not begun executing. Blocks can not be cancelled in the middle of execution.

Since all blocks are added to the dispatch group, remember to remove the cancelled ones.

Build and run, and add images from Le Internet. You’ll see that the app now downloads
three of each image and a random extra amount. The rest have been cancelled after being dispatched to a queue. This is a pretty contrived example but it illustrates how dispatch block objects are used and cancelled nicely.

Dispatch block objects can do a lot more, so be sure to check out the documentation.


Miscellaneous GCD Fun

But wait! There’s more! Here are some extra functions that are a little farther off the beaten path. Although you won’t use these tools nearly as frequently, they can be tremendously helpful in the right situations.


Testing Asynchronous Code

This might sound like a crazy idea, but did you know that Xcode has testing functionality? :] I know, sometimes I like to pretend it’s not there, but writing and running tests is important when building complex relationships in code.

Testing in Xcode is performed on subclasses of
XCTestCase
and
runs any method in its method signature that begins with
test
.
Testing is measured on the main thread, so you can assume that every test happens in a serial manner.

As soon as a given test method completes, XCTest methods will consider a test to be
finished and move onto the next test. That means that any asynchronous code from the previous test will continue to run while the next test is running.

Networking code is usually asynchronous, since you don’t want to block the main thread while performing a network fetch. That, coupled with the fact that tests finish when the test method finishes, can make it hard to test networking code.

Let’s take a brief look at two common techniques for testing asynchronous code: one using semaphores and
one using expectations.


Semaphores

Semaphores are an old-school threading concept introduced to the world by the ever-so-humble Edsger W. Dijkstra. Semaphores are a complex topic because they build upon the intricacies of operating system functions.

If you want to learn more about semaphores, check out this link
which discusses semaphore theory in more detail. If you’re the academic type, a classic software development problem that uses semaphores is the Dining
Philosophers Problem.

Semaphores lets you control the access of multiple consumers into a finite amount of resources. For example, if you created a semaphore with a pool of two resources, at most only two threads could access the critical section at the same time. Other items that
want to use the resource must wait in a FIFO queue.

Open GooglyPuffTests.swift and replace downloadImageURLWithString with
the following implementation:

func downloadImageURLWithString(urlString: String) {
let url = NSURL(string: urlString)
let semaphore = dispatch_semaphore_create(0) // 1
let photo = DownloadPhoto(url: url!) {
image, error in
if let error = error {
XCTFail("\(urlString) failed. \(error.localizedDescription)")
}
dispatch_semaphore_signal(semaphore) // 2
}

let timeout = dispatch_time(DISPATCH_TIME_NOW, DefaultTimeoutLengthInNanoSeconds)
if dispatch_semaphore_wait(semaphore, timeout) != 0 { // 3
XCTFail("\(urlString) timed out")
}
}

Here’s how the semaphore works in the code above:

Create the semaphore. The parameter indicates the value the semaphore starts with. This number is the number of things that can access the semaphore without having to have something increment it first (note that incrementing a semaphore is known as signalling
it).

In the completion closure you tell the semaphore that you no longer need the resource. This increments the semaphore count and signals that the semaphore is available to other resources that want it.

This waits on the semaphore, with a given timeout. This call blocks the current thread until the semaphore has been signalled. A non-zero return code from this function means that the timeout was reached. In this case, the test is failed because it is deemed
that the network should not take more than 10 seconds to return — a fair point!

Run your tests by selecting Product / Test from the menu or use ⌘+U if
you have the default key bindings. They should all succeed in a timely manner.

Disable your connection and run the tests again; if you are running on a device, put it in airplane mode. If you’re running on the simulator then simply turn off your connection. The tests complete with a fail result, after 10 seconds. Great, it worked!

These are rather trivial tests, but if you are working with a server team then these basic tests can prevent a wholesome round of finger-pointing of who is to blame for the latest network issue.


Expectations

The
XCTest
framework provides another solution
to the asynchronous code testing problem in the form ofexpectations. This feature
lets you set up an expectation – something you expect will happen – and then start an asynchronous task. Then you can have the test runner wait until the asynchronous task marks the expectation as fulfilled.

Navigate to GooglyPuffTests.swift and replace
downloadImageURLWithString
with
the following code:

func downloadImageURLWithString(urlString: String) {
let url = NSURL(string: urlString)
let downloadExpectation = expectationWithDescription("Image downloaded from \(urlString)") // 1
let photo = DownloadPhoto(url: url!) {
image, error in
if let error = error {
XCTFail("\(urlString) failed. \(error.localizedDescription)")
}
downloadExpectation.fulfill() // 2
}

waitForExpectationsWithTimeout(10) { // 3
error in
if let error = error {
XCTFail(error.localizedDescription)
}
}
}

Here is how it works:

Create the expectation with
expectationWithDescription
.
The test runner will display the string parameter here in the test log upon failure, so describe what you expect to happen.

Call
fulfill
in the closure that executes asynchronously
to mark it as fulfilled.

The calling thread waits for expectations to be fulfilled by calling
waitForExpectationsWithTimeout
.
If the wait times out, that’s treated as an error.

Build and run the tests. The end result is not very different from using a semaphore, but leveraging the
XCTest
framework
is a cleaner and more readable solution.


Working With Dispatch Sources

A particularly interesting feature of GCD is Dispatch Sources, which are basically
a grab-bag of low-level functionality helping you to respond to or monitor Unix signals, file descriptors, Mach ports, VFS Nodes, and other obscure stuff. All of this is far beyond the scope of this tutorial, but you’ll get a small taste of it by implementing
a dispatch source object and using it in a rather peculiar way.

First-time users of dispatch sources can get quite lost on how to use a source, so the first thing you need to understand how
dispatch_source_create
works.
This is the function prototype for creating a source:

func dispatch_source_create(
type: dispatch_source_type_t,
handle: UInt,
mask: UInt,
queue: dispatch_queue_t!) -> dispatch_source_t!

The first parameter,
type: dispatch_source_type_t
,
is the most important parameter as it dictates what the handle and mask parameters will be. You’ll need to refer to the Xcode
documentation to see what options are available for each
dispatch_source_type_t
parameter.

Here you’ll be monitoring for
DISPATCH_SOURCE_TYPE_SIGNAL
.
As the documentation
says:

A dispatch source that monitors the current process for signals. The handle is a signal number (int). The mask is unused (pass zero for now).

A list of these Unix signals can found in the header file
signal.h
.
At the top there are a bunch of
#define
s. From
that list of signals, you will be monitoring the
SIGSTOP
signal.
This signal is sent when a process receives an unavoidable suspend instruction. This is the same signal that’s sent when you debug your application using the LLDB debugger.

Go to PhotoCollectionViewController.swift and add the following code near
viewDidLoad
.
You’ll want to add these two private properties to the class, and this new block of code at the beginning of
viewDidLoad
,
after the call to the superclass but before the existing line that gets the
ALAssetLibrary
:

#if DEBUG
private var signalSource: dispatch_source_t!
private var signalOnceToken = dispatch_once_t()
#endif

override func viewDidLoad() {
super.viewDidLoad()

#if DEBUG // 1
dispatch_once(&signalOnceToken) { // 2
let queue = dispatch_get_main_queue()
self.signalSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL,
UInt(SIGSTOP), 0, queue) // 3
if let source = self.signalSource { // 4
dispatch_source_set_event_handler(source) { // 5
NSLog("Hi, I am: \(self.description)")
}
dispatch_resume(source) // 6
}
}
#endif

// The other stuff
}

The code is a little involved, so step through the code one comment at a time to see what’s going on:

It’s best to only compile this code while in DEBUG mode since this could give “interested parties” a lot of insight into your app. :] DEBUG is defined by adding -D
DEBUG
under Project Settings -> Build Settings -> Swift Compiler – Custom Flags ->
Other Swift Flags -> Debug
.

Use
dispatch_once
to perform the dispatch source’s
one-time setup.

Here you instantiate the
signalSource
variable.
You indicate that you’re interested in signal monitoring and provide the
SIGSTOP
signal
as the second parameter. Additionally, you use the main queue for handling received events — you’ll discover why shortly.

A dispatch source object won’t be created if you provide malformed parameters. As a result, you should make sure you have a valid dispatch source object before working on it.

dispatch_source_set_event_handler
registers an
event handler closure that is invoked when you receive the signal you’re monitoring for.

By default, all sources start in the suspended state. You must tell the source object to resume when you want to start monitoring for the events.

Build and run your app; pause in the debugger and resume the app immediately. Check out the console, and you’ll see something like this in the debugger:

2014-08-12 12:24:00.514 GooglyPuff[24985:5481978] Hi, I am: <GooglyPuff.PhotoCollectionViewController: 0x7b765ee0>

You app is now debugging-aware! That’s pretty awesome, but how would you use this in real life?

You could use this to debug an object and display data whenever you resume the app; you could also give your app custom security logic to protect itself (or the user’s data) when malicious attackers attach a debugger to your application.

An interesting idea is to use this approach as a stack trace tool to find the object you want to manipulate in the debugger.





Think about that situation for a second. When you stop the debugger out of the blue, you’re almost never on the desired stack frame. Now you can stop the debugger at anytime and have code execute at your desired location. This is very useful if you want to
execute code at a point in your app that’s tedious to access from the debugger. Try it out!

Put a breakpoint on the
NSLog
statement in
viewDidLoad
in
the event handler you just added. Pause in the debugger, then start again; the app will then hit the breakpoint you added. You’re now deep in the depths of your
PhotoCollectionViewController
method.
Now you can access the instance of
PhotoCollectionViewController
to
your heart’s content. Pretty handy!

Note: If you haven’t already noticed which threads are which in the debugger, take a look at them now. The
main thread will always be the first thread followed by libdispatch, the coordinator for GCD, as the second thread. After that, the thread count and remaining threads depend on what the hardware was doing when the app hit the breakpoint.

In the debugger console, type the following:

(lldb) po self.navigationItem.prompt = "WOOT!"

The Xcode 6.3 debugger can sometimes be uncooperative. If you get the message

error: use of unresolved identifier 'self'

then you have to do it the hard way to work around a bug in LLDB. First take note of the address of
self
in
the debug area, then manually cast the value to the type you want:

(lldb) e let $vc = unsafeBitCast(0x7fd0b3e22bc0, GooglyPuff.PhotoCollectionViewController.self)
(lldb) po $vc.navigationItem.prompt = "WOOT!"

Then resume execution of the app. You’ll see the following:









With this method, you can make updates to the UI, inquire about the properties of a class, and even execute methods — all while not having to restart the app to get into that special workflow state. Pretty neat.


Where to Go From Here?

You can download
the final project here.

I hate to bang on this subject again, but you really should check out the How
to Use Instruments tutorial. You’ll definitely need this if you plan on doing any optimization of your apps. Be aware that Instruments is good for profiling relative execution: comparing which areas of code takes longer in relation to other areas. If you’re
trying to figure out the actual execution time of a method, you might need to come up with a more home-brewed solution.

Also check out How
to Use NSOperations and NSOperationQueue Tutorial in Swift, a concurrency technology that is built on top of GCD. In general, it’s best practice to use GCD if you are using simple fire-and-forget tasks. NSOperations offers better control, an implementation
for handling maximum concurrent operations, and a more object-oriented paradigm at the cost of speed.

Remember, unless you have a specific reason to go lower, always try and stick with a higher level API. Only venture into the dark arts of Apple if you want to learn more or to do something really, really “interesting”. :]

Good luck and have fun! Post any questions or feedback in the discussion below!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: