Transcript
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
[ Silence ]
>> Welcome to this
afternoon's, this year's edition
of "What's New in
Foundation Networking."
Foundation Networking
is, of course,
the layer of networking
that's available
to application developers
on Mac OS X and iOS.
It's our goal to provide
you guys with the protocol
and networking support
that you need
to make great applications
on our platform.
And, of course, we use
these APIs ourselves.
Here's a standard picture
of what frameworks look
like on our system.
Your application sits on top,
and nowadays you're also having
extensions that are sitting
on top of all these frameworks.
Extensions are particularly
interesting
for background networking
because when your extension
terminates, you might still need
to have work done, and you
can use background networking
through NSURLSession
to get that work done.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
through NSURLSession
to get that work done.
Looking at the different
levels that actually make
up Foundation Networking,
that we start
with the Core OS BSD
networking layer.
This is the lowest level that
sockets its bind, its addresses.
It's kind of like
building a car from a kit.
Back when Mac OS started,
we took the foundation layer
of things from NeXTSTEP,
and we said, "Well,
we need something
that's a C API."
And CoreFoundation
came out of that.
And CFNetwork came out of that.
As time has gone
on, we find more
and more developers want
a higher level approach.
Foundation and Foundation
Networking provides
NSURLSession, replacing
NSURLConnection, NSStream,
and NSNetServices as
Cocoa-style APIs for developers
to use on Mac OS X and iOS.
These APIs serve
different purposes.
NSStream is an API that is used
to synch bytes into the system
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
NSStream is an API that is used
to synch bytes into the system
in various places
or to receive bytes
out when you maybe
don't have an idea
of how many bytes there are.
You could have a file
on disk and you know
that it's a certain size.
So you create an
NSInputStream from a file.
Or you could receive an
NSInputStream from an API.
You don't know how big it is,
so you schedule the stream
and then you start
reading bytes out of it.
NSNetServices is an
API that allows you
to do peer-to-peer servers
and clients on your machine,
on your device, talking
to other devices.
It's a great way
of publishing work
that you want other apps to do.
NSURLSession provides sort
of a message-based delivery.
You're talking about putting
a request on the network
and receiving a response
and some data code.
In this talk, we're going
to talk about those APIs:
NSStream NSNetServices
and NSURLSession.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
NSStream NSNetServices
and NSURLSession.
We're going to review
NSURLSession,
which is what we
introduced last year.
So there will be a
bit of review here.
We have some new
protocol support,
and if you saw Craig's
talk yesterday,
there's on one slide there was a
little mention of a new protocol
that we'll talk about.
And lastly we're going to
spend some time talking
about the best way
to use NSURLSession
and background networking.
Let's get the new APIs
out of the way first.
NSNetServices has
a single, new API.
But it's not really new because
it was available last year
in iOS 7.
This year includes
peer-to-peer is available
on Mac OS X Yosemite on
hardware that supports it
which is recent, 2012
or later Mac hardware.
That's really the only
API change available,
but it is now available
for you to use.
NSStream gets two new APIs.
Now NSStream is-it's Toll-Free
Bridged with CFStream,
which means that there's a lower
level API that this works with.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
which means that there's a lower
level API that this works with.
But at some point
we lost the ability
of just creating a stream
to a host on the internet.
So adding back a new
API gets streams to host
with name specifying a port.
Then the host could
be a dotted IP,
IPv6 or regular internet
host name.
The result is an inputStream
and an outputStream
that you then open,
schedule, and read and write
from as those streams get opened
or if you pass on
to another layer.
So, for instance, if you had an
audio streaming app that wanted
to read bytes off of a network
resource that was providing you
with audio data, you could use
this API to get an NSInputStream
from that and pass
that NSInputStream off
to the audio player.
Well, if you had bytes that you
were generating that you wanted
to send into that audio
player, or if you wanted
to filter those bytes,
you might need
to implement your own NSStream.
You could do that by
subclassing NSStream
or the preferred
way we'd want you
to do it is creating what
we call a "bound pair".
That creates an input and
output stream and a buffer.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
That creates an input and
output stream and a buffer.
So you'll write bytes
into the output stream,
and the audio unit or whatever
it is will consume the bytes
out of the input stream.
So that's just a
couple of new APIs
in NSNetServices and NSStream.
I'm going to talk
about NSURLSession.
And a lot of this is
review from last year.
NSURLSession is sort
of a big machine
that has a lot of moving parts.
And in this picture it
kind of looks like a boat.
It's an object that you
create with a configuration
and it references
storage objects.
The storage objects are
needed to supply information
about how to perform a load.
Well, if it's a boat,
the captain of the
boat is your delegate
that you bind into the system.
The purpose of this boat as it's
running along is to take data
in a request and to produce
responses and data objects out.
So it's like carrying
passengers and a duty-free shop
out the other side where
you get whatever it is
that you're getting.
All right.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So the concepts of
an NSURLSession.
The session object itself.
The API is called
an NSURLSession.
The main factory class is
called an NSURLSession.
An NSURLSessionClass is created
with an NSURLSession
configuration object.
The configuration object is
a dictionary of properties
that dictate things like the
how many connections we can make
to a server.
What kind of SSL
we're going to use.
What storage objects
we're going to use.
The session creates
task objects.
These represent the
sort of transient state
of a resource as
it's being loaded.
A task object is created
from a request or a URL.
And its initial state
is suspended.
You resume it; it does its
work within the session,
conferring with your
delegate as needed,
and in the end the
task becomes finished.
And you're done;
you've got your data.
Okay. The delegate itself
is something you supply.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Okay. The delegate itself
is something you supply.
The delegate is has
to conform to a bunch
of different protocols.
There are protocols
for session, for task
and for the various
task subclasses.
There are storage objects that
exist in the system by default.
There is a Keychain-based
credential store.
There is a cookie
store-a cookie jar.
Cookies are sort of a degenerate
form of authentication.
They serve the same purpose.
They identify you to a
website, and allow that website
to communicate with you in
what it thinks is a secure way.
Protocols are objects that you
can register with a session
that override the
default behavior
or provide additional behavior.
So a URL looks like "HTTP:",
your protocol could be "MOOF:"
and you could deal with MOOF
protocols however you want,
including rewriting them
to become HTTP protocols.
Lastly, there's the URL cache
object, which is what we're able
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Lastly, there's the URL cache
object, which is what we're able
to consult in order to avoid
going out on the network at all.
In NSURLSession, the other
configuration object,
you can specify your
own subclasses for all
of these storage objects.
The configuration object
itself, as I mentioned,
it basically is a dictionary
although it is comprised
of a bunch of property
attributes.
When you create one, when you
get a configuration object,
you modify it however you want.
And then you're going
to create a session
with that configuration.
Once you've created a
session with a configuration,
you can't modify it again.
These are some of the
attributes that you can change
within a configuration: the
TLS levels, whether you want
to support the latest TLS
level, restrict it to that,
or to allow the previous
one, whether or not
to allow a request to go over
the network (cell network).
Network service type is,
well, "I'm doing video data.
I want a higher priority
if that's available."
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
I won't try and list all
the configuration options,
because there are quite a few
and they're all in
the header file.
To get a configuration object,
you can ask for the default
session configuration.
This is a factory method
that returns an object
that captures the global
state of the framework.
If you look at the
Foundation Framework,
there are some classes like
NSData that have a class method,
NSData dataWithContentsOfURL.
If you were to pass
an HTTP URL into that,
it would use all
the global defaults
of the Foundation
Framework in order to go
out on the network
and get that data.
Never call that API because
it blocks and it will end
up hanging whatever thread
you're executing it on.
The point is that that
configuration is what the
default session configuration
represents.
If you get a default session
configuration and modify it,
you're only modifying
that configuration.
You're not modifying the
global configuration.
There's a factory method for an
ephemeral session configuration.
This is set up so that none
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
This is set up so that none
of the storage objects
actually persist data to this.
You would use it in private
browsing, for instance.
The third factory method is used
to create a background session.
You specify an identifier
which is a string that,
when your app is re-launched,
you use the same string in order
to get re-associated with that
session in the background.
Then you'll start receiving
events for that session.
There's a factory
method for a session
that gives you a shared session.
The shared session is, well,
going back to that NSData
dataWithContentsOfURL example,
that would be using the
shared session to do its work.
It's representing the
global environment.
It's a very handy thing
to use when you just want
to get a resource
off the network.
Once you have a configuration
object that's set up though,
you might want to create
a session all your own
that uses only your
own resources
and maybe only your
own storage classes.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and maybe only your
own storage classes.
You don't have to specify
a delegate though, because,
as we're going to talk
about in a few minutes,
there's some asynchronous
convenience routines
that don't require you
to implement a delegate
with your NSURLSessions.
But if you do specify a
delegate, which you need to do
for background transfers,
then this is the API you use:
sessionWithConfiguration passing
of a delegate and
a delegateQueue.
And a delegateQueue can
be a concurrent queue.
So, task objects themselves.
The session creates
task objects.
There's a base class task object
that has a couple of methods:
-cancel, -suspend, and -resume.
When a task object is
created, it is suspended.
You have to send it the
resume message in order
for it to start working.
There are two subclasses,
which are really only available
for the purposes of providing
some sort of semantic glue.
Currently, there's a
delegate for data tasks,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Currently, there's a
delegate for data tasks,
but there's no delegate specific
for download-for upload tasks.
But if there were in the future,
we would have the ability
to differentiate
between the types.
There's also a download task.
And all this adds on top
of the normal task object is
cancelByProducingResumeData.
When a download is
cancelled if you use this API,
we're able to capture the
state of the download and,
at a later time, you can
create a download task using
that resumeData and avoid having
to download all those bytes
again if the server supports it
and if the conditions warrant
it and blah, blah, blah.
There are all sorts of reasons
why that might not work.
But if you want it to
have a chance of working,
this is how you'd
get that resumeData.
To create task objects from
a session, dataTaskWithURL:
You just give it an
arbitrary URL-HTTP, MOOF,
whatever is going to be
supported by the protocols.
dataWithRequest, which takes
an NSURLRequest, which is sort
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
dataWithRequest, which takes
an NSURLRequest, which is sort
of the older way of specifying
binding a URL with a method
or however your protocol is
going to interpret that request.
Upload tasks are similar,
except that we don't allow you
to create an upload
task from a URL.
You have to create an
upload task from a request
where you've specified the
method like post or put.
We give you the option of
performing an upload from a file
or a data; either
of these are great.
But we also give you the option
of creating an upload task
with a stream-we say
"streamed request".
That means we're going to ask
your delegate for a stream
when we're ready
to write the bytes
of your upload to the network.
The reason we have to do that is
because sometimes we will
open a connection to a server
and we'll start writing bytes.
And the server will come
back and off-challenge us.
And we'll have to talk
with your delegate.
But at that point we've
already sent bytes along.
And if we're doing an
upload from a stream,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
And if we're doing an
upload from a stream,
we're going to have to
throw that stream away
and ask your delegate
for a new one.
If we did the upload from a
file or from a data object,
we know how to restart
that stream
without having to
ask you for it.
If you're trying to do uploads
for background transfers,
you have to do it from a file
because we want to capture
that file so we can upload
it in the background.
Download tasks are similar.
You can download from a URL.
I want to copy: I want a URL
that goes from, you know,
my big file .tgz or from an
NSURLRequest or create it
from a resumeData block.
And that gives you
back a downloadTask
which you then resume.
The lifecycle of a data
task starts out suspended,
referencing the request that
we're going to deal with.
You tell it, "Okay, resume."
The state goes to running.
And then the delegate
starts getting called.
didReceiveResponse occurs.
It handed you the response, and
the response is also available
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
It handed you the response, and
the response is also available
as a property on
the task object.
As data comes in, will call
didReceiveData delegate message
0 or more times.
If the task is in a
session that was enabled
that has enabled caching,
we're going to call
your willCacheResponse.
This gives you the
opportunity to say,
"It's for a particular
resource," "I don't want
to cache this," or you want
to modify the cache response,
but we're going to call that
at this point before we call
didCompleteWithError
and the state
of the task goes to finish.
One thing to note,
didCompleteWithError
as the final thing that gets
sent to your delegate on behalf
of the task, the error
is going to be nil
if the server gave
us a valid response.
A valid response
would be 200 OK.
But a valid response is
also 404 FileNotFound.
Because the body that came back
might be interesting to you.
You might want to
display that body.
The error parameter
here then is strictly
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
The error parameter
here then is strictly
for transmission errors, like,
the host cannot be resolved,
or a timeout occurred,
or you cancelled the task
while it was in flight.
When you look at the error here,
it's going to contain, well,
the reason why the transfer
failed, and you should assume
that the response and the data
that you got back are invalid.
A download task works
very similarly.
You create a download task.
In this case, you reference some
file that you're going to get,
and it starts doing its work
as soon as you say resume-yeah,
you send it the resume message.
So it goes to running
and it will periodically
call didWriteData.
It will tell you, "Yes,
I'm making progress
on this download as it's going."
Note that this is also
what's going to happen
for a background download.
If this task were created
for a background download
and your app is running,
you're going
to receive didWriteData
callbacks.
Finally, we've written
all the bytes
of this file to the file system.
We're going to invoke the
didFinishDownloadingToURL
message.
And we're going to give you
a file URL in your container.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
And we're going to give you
a file URL in your container.
You have to move that to
a more permanent location.
Move the file to a
more permanent location
in your container because, when
you return from this delegate,
we're going to delete the file.
Then we will call
didCompleteWithError:nil,
because we successfully got
the file all the way to disk.
Creating NSURLSessionTasks
can be done just-you know,
throw a request in,
get a task out.
But there's some convenience
routines that are really handy,
especially when you
have a session
that doesn't use a
delegate or, I should say,
particularly you can only
use these with a session
that doesn't have a delegate.
So you say
dataTaskWithURL:completion
and you give it a
completionHandler.
The task object that comes
back has to be resumed.
But it doesn't bother calling
any of the delegate messages
that might be in the
session, or it might not.
And when the work is done it
invokes your completionHandler.
This is true for, you know,
with an upload via file or data,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
This is true for, you know,
with an upload via file or data,
or a download to disk.
In that case,
the completionHandler
references the file.
And, like the delegate for
didFinish LoadingToFile,
you have to move that file away.
This is what this looks like
(the convenience routines).
Very simply you just create
a URL that you're going
to request off the network.
In this case I'm going to create
a configuration object using the
ephemeralSessionConfiguration
prototype object.
I don't want to store anything
and I don't want anybody
to know about this resource.
I'm going to create
a private session.
And then I'm going
to create a task
with that private
session and with that URL.
So I tell the session,
dataTaskWithURL, "Here's my URL.
Here's my completionHandler."
The signature for the
completionHandler is data,
response and error.
Error will be nil if data
and response are valid.
I resume the task, and then I go
back to running in the run loop.
If you were to just exit right
here, nothing would happen
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
because we're not
doing anything.
We have to run the run loop
or dispatchMain in order
for this completionHandler
to get called.
It gets called and
we're able to deal
with the data that came back.
The delegate that you create a
session with then, is comprised
of a number of different
protocols.
The session level
delegate is sort
of meta information
about the session.
When a session is created,
it might become invalid.
You can send an "invalidate"
message,
in which case the session
invalidation delegate message
will be called.
But there's a more interesting
delegate message sent
to a session delegate, which is
for connection-level
authentication.
If you connected to a website
that has SSL encryption,
we're going to call your
session-level authentication
challengeHandler to ask
whether or not you want
to continue connecting
to a site.
We're going to put multiple
requests on the same socket.
We're not going to make a new
socket and off-challenge you
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
We're not going to make a new
socket and off-challenge you
for each request as they go.
We're going to do it once.
On the basis of each
task, on each request,
there's an
NSURLSessionTaskDelegate.
You can see that these
things are sort of building
on top of one another.
The most important thing
that this delegate responds
to is didFinishLoadingWithError.
And again, if the error is nil,
that means that the
response was valid
and you should deal
with the data.
If the error is not nil, then
something happened that kept us
from being able to
transmit your request.
There is also request
authentication handling here.
If the server says, for
a particular request,
"I don't know how
to deal with you.
I need some credentials
for this request."
It has nothing to do
with any other request,
just this request.
Then that challenge request will
come through this task delegate.
Data delegate then
extends the task delegate.
It's got a couple new
methods that are interesting.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
It's got a couple new
methods that are interesting.
First, didReceiveResponse.
Suppose you're downloading
a file off the network
or downloading data off
the network, and you look
at the content type when
you get the response back.
The content type says
that this is a disk image.
Well, you probably don't want
to read the disk
image all to memory.
The completion routine for this
delegate message allows you
to specify that this data task
should become a download task.
And that's exactly what happens.
We create a download task.
We replace your data task and we
start writing the file to disk.
When the file is completely
written, we treat this task
as if it were a data
task and we notify you
of the file's location.
The bytes that are read off the
network then are sent to you
as didReceiveData callback,
zero or more didReceiveData
callbacks.
One thing to note: you don't
have to copy those datas.
You can keep track of them.
You can reference them.
But you don't need to copy them.
Each of those datas
may, under the scenes,
be discontiguous data.
So you should use the NSData
discontiguous data application
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So you should use the NSData
discontiguous data application
APIs instead of requesting
the byte pointer
to the start of the data.
And that's probably
true with a lot of APIs
that are producing data today.
Behind the scenes, we're trying
to stitch things together
to keep things discontiguous
in memory,
which is a big performance win.
Finally the download delegate
extends the task delegate.
It doesn't receive the bytes,
it receives notification
of the bytes.
In the end, it receives a
delegate message that says,
"Here is the file
location for the data
that we just transferred."
One quick thing that I want to
talk about is the delegate queue
that you created
the session with.
We treated it in iOS 7
and Mac OS X Mavericks
as a serial queue.
Every time a task had
something to do, it would call
into your delegate and
wait for that to return.
And go on to the next task
that had some work to do.
The problem with this
approach is that,
if any one of your delegates
stalled for some reason,
like is happening here, then we
have to wait for that delegate
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
like is happening here, then we
have to wait for that delegate
to return before the
next task can come along.
In Mac OS X Yosemite and
iOS 8, we're now going
to treat your queue
as concurrent
if it is a concurrent queue,
as wide as you have specified.
So all of these tasks
are able to call
in to your delegate at once.
No matter what order
they finish, you know,
delegates may finish
at different times.
We're always going to be able to
enqueue work for the next task,
and that's a good thing.
There is a little bit of
new API that I need to talk
about in NSURLSession.
This revolves around
storage objects
like the NSHTTPCookieStorage
and NSURLcredentialStorage
and the NSURLCache as
well as NSURLProtocol.
In each of these cases, we
found that it was insufficient
for the accessors for
these storage objects
to receive just the URL.
People who were subclassing
these objects wanted access
to the task that was
asking the question.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
to the task that was
asking the question.
So there's a new category
NSURLSessionTaskAdditions.
If you look in the headers or on
the NSURLLibrary, you're going
to see this category show up,
and it provides asynchronous
gets and, presumably,
asynchronous sets, although
that's an implementation detail.
We'll take NSHTTPCookieStorage
for an example.
So two new methods of
gets: storeCookies:forTask.
Here's an array of
cookies, and I'm doing it
because this task cares.
You look-the implementation
will look in the task,
look at the current
request, get the URL out of
that and do that work.
getCookiesForTask
works the same way.
I would like the
cookies for this task.
But instead of synchronously
responding,
here is a completionHandler.
Once you've got those cookies,
invoke the completionHandler,
and you don't have to block me.
These APIs are available
on NSHTTPCookieStorage,
NSURLCredential and NSURLCache.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
As I mentioned earlier, during
the keynote there was a slide
which had one little bit
of interesting stuff in it.
And I'd like to ask Scott
to come on up and tell us
about the new protocol support.
>> Thank you, Steve.
Hello, everyone.
I'm Scott, and today
I'll be talking
about new protocol support
in Foundation Networking.
We've been working hard
to improve the performance
of our HTTP implementation
for our developers-you guys.
And a popular request at last
year's WWDC was to add support
for the SPDY protocol.
Today we're pleased to announce
that the SPDY protocol is
now supported by NSURLSession
on OS X Yosemite and iOS 8.
Not only does this mean that
it's available in Safari
for your day-to-day browsing,
but it's also available
for direct use in your apps.
Even if you don't use
NSURLSession directly
in your applications, keep in
mind that SPDY's also leveraged
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
in your applications, keep in
mind that SPDY's also leveraged
through other Apple
frameworks, like UIWebView,
which you might use
in your apps.
For those of you who don't
know what SPDY is already,
it's a protocol that is
designed to make the web faster.
Essentially, what it does is
it changes the on-wire format
of HTTP/1.1.
But the semantics of
HTTP/1.1 stay the same.
So you know about request
types like get, post, put.
Those are the same.
Response codes, like 200
OK, that's still the same.
Caches, cookies,
credentials-those are
also all the same.
SPDY is also serving as the base
for the HTTP/2.0
draft specification.
I'm actually going to come back
to that point a little
bit later on.
But it's interesting to see
where technology and the sort
of development specifications
is heading
as a result of the SPDY work.
Now the way SPDY works at a
very sort of high level is
that it allows for the exchange
of multiple HTTP
messages-both requests
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
of multiple HTTP
messages-both requests
and responses-simultaneously
and potentially out of order,
all over a single
TCP connection.
So if you'd like to use
SPDY in your applications,
here is what you need to know.
As I mentioned, it's available
on both OS X Yosemite and iOS 8.
And we support three versions
of the SPDY protocol:
2, 3, and 3.1.
And what we've decided to do is
make it supported transparently
by NSURLSession.
So this goes along the lines
of a philosophy we have.
Which is that we will always
pick the best protocol we should
use when communicating
with your server.
Currently, that means we're
going to decide if it's going
to be HTTP/1.1 or SPDY
and the version of SPDY
that we're going to use.
But this could also
hold true in the future
for some other protocol.
What this means for you is that
no source changes are needed.
It will just work.
If we look at a source code
example here, what you do first,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
If we look at a source code
example here, what you do first,
of course, is create your URL
for whatever resource
you'd be interested
in fetching from your server.
And then you'd construct
a session task.
In this case we're using
a data task-in this case,
also with the shared session-and
then having a completionHandler
with the data you'd
receive, the response
and a transmission
error if there was one.
Of course, you then have to
resume the task to start it.
What you'll notice about this
code sample is there's nothing
different here because
SPDY is going to be used.
There isn't some code
that I added, changed,
or otherwise removed
to support SPDY.
In fact, this example
should look very similar
to what Steve showed you not
more than a couple minutes ago,
and that's intentional.
I want to dive into
some of SPDY's benefits
so you can understand how it
will actually impact your apps.
The way SPDY works
is it has a single,
long-lived TCP connection
between the client
and the server.
This actually helps to mitigate
latency penalties often times
that are seen when you ask
for additional resources,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
that are seen when you ask
for additional resources,
because traditionally
we may have had
to open an additional socket
and TCP connection
to get the data back.
But now we have the one
connection that's open
for a longer period of time,
which helps to get
rid of that latency.
It also means that since we're
only opening one connection
to your server, that any given
app instance now should be using
fewer resources on your server,
which is a big win for you.
SPDY supports the
concept of multiplexing,
which is where a request and
response can be interleaved.
Potentially multiple requests
and responses can be interleaved
over a single connection.
This gets rid of what's known
as Head-of-Line Blocking.
This is where, while
you're in the process
of receiving the
response for one object,
that you can't receive
responses for other objects
on the same connection.
For example, you might
be in the process
of downloading a large image,
and that actually then gets
in the way of your ability
to download say a smaller
and more important
Javascript file or CSS file.
Finally, SPDY supports
the notion of priorities.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Finally, SPDY supports
the notion of priorities.
Now the order in which you
issue requests no longer has
to dictate the order in which
responses are actually received
by your application.
I want to sort of show you
a diagram of how this works.
Keep in mind that this is going
to be sort of an ideal network
where there's no latency and
perfect bandwidth utilization.
We're going to first
start by looking
at how Head-of-Line
Blocking is present
in HTTP/1.1 without pipelining.
Imagine you issue three requests
for three resources: an image,
a style sheet, and
some XML data.
The first thing that will
happen of course is we'll send
out the GET request
for the image.
And then you'll wait a
while as all the bytes
in the 200 OK response come back
for that image back to your app.
Meanwhile, notice
nothing has happened
for the style sheet
or the XML data.
Then we can send out the
request for the style sheet,
receive its response, and then
do the same for the XML data.
So you can see here
that during the time
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So you can see here
that during the time
when you are receiving the
response bytes for the image,
we can't make any
forward progress
with the style sheet
or the XML data.
Now pipelining makes the
situation a little bit better,
but it doesn't get rid of
Head-of-Line Blocking still.
So let's see what
that looks like.
Now, requests can get sent
out one after another.
But responses are still blocked
by the previous response.
And Head-of-Line Blocking
is still an issue even
if you are using pipelining.
So multiplexing changes
that with SPDY.
Let's look at the same
three resources now,
but this time with multiplexing.
We'll start by sending
the request for the image.
That should look no
different than before.
Like before, with pipelining we
can actually send the request
for the stylesheet immediately
after the request for the image.
And of course we can
start making progress
by receiving bytes
for the image.
But here's where
things get different.
Because the stylesheet is
higher priority than the image,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Because the stylesheet is
higher priority than the image,
the server will start
sending back response bytes
for the stylesheet instead of
response bytes for the image.
And then once the request
for the XML data has gone
out because it is the
highest priority object,
we receive its response bytes
in lieu of the image bytes
or the stylesheet bytes.
You'll also notice in this
diagram that the amount
of data we're receiving for
the XML data is a larger chunk
than that of the style
sheet or the image.
That was intentional.
Often times, with
SPDY implementations,
the responses are
weighted by priority.
So if we let this sort
progressing continue,
what we end up seeing is that
we can receive the entire
stylesheet and XML
data before the image,
even though those
resources were asked
for after we asked
for the image.
So you might be wondering
why should I adopt SPDY?
And the plain and
simple reason is
that it can give a
better user experience.
That's what we're all
after I would think.
There's sort of two
reasons this is the case.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
There's sort of two
reasons this is the case.
The first is that, as I
mentioned, it reduces latency
by having a single,
long-lived connection open
between the client and server.
What this means is that over
the lifetime of you app, as more
and more requests are
issued, we don't have
to continually open
new connections.
Which helps to get
rid of that latency.
It means that your app can have
much more interactive behavior
even when using a
cellular connection.
In our own performance
measurements,
we found that SPDY could be up
to 25% faster than HTTP/1.1.
And I'll come back to this
a little bit later on.
There's also though some more
subtle points I want to raise
about performance in
SPDY and its benefits.
Because it's only opening
a single TCP connection,
it also only has to do
a single SSL handshake.
And that means that your
app will have less CPU use,
and over time better
battery life on the device.
It also means that when your
app becomes the next big app
on the app store, and a big hit,
that you may not have to roll
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
on the app store, and a big hit,
that you may not have to roll
out as much server
site infrastructure
to support the increased
number of clients,
because each client has
fewer connections going back
to your server, right?
It's one instead of many.
I already told you that
if you'd like to make use
of SPDY you don't need any
client-side code changes.
But there are a few
other points with respect
to SPDY adoption I do
want to talk about.
First, we are planning
to add API
for setting priorities
on session tasks.
You'll see that in
a future seed;
it's not currently available.
We'd also welcome any other
feedback from you during either
of the networking lab sessions,
or through a bug report
if you believe there's other
APIs that would be useful
that you don't currently see.
While you don't need to have
any client-side code changes
to support SPDY, keep in
mind SPDY does require
server-side support.
This all happens when the client
is negotiating with the server,
using the TLS handshake.
The reason this is important
is that all of the URLs
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
The reason this is important
is that all of the URLs
for requests that you issue
from your application need
to have HTTPS and not HTTP.
I'll just point out that
there's a fair amount
of existing web server software
and content delivery networks
that already support SPDY.
So you may already have SPDY
support and have to do nothing
at all to turn it
on, on your server.
Finally, many of you might
have an NSURLProtocol subclass,
or maybe perhaps multiple
in your application.
And I do want to note that our
SPDY implementation is not going
to interfere with your protocol
subclasses regardless of whether
or not they're adding
support for SPDY
or some other protocol entirely.
I mentioned that we
found that SPDY could be
up to 25% faster than HTTP/1.1.
I want to go into a little
more detail about sort
of the performance
and expectations.
Naturally, parallel
TCP connections
in some cases can be faster
than SPDY single connection.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
in some cases can be faster
than SPDY single connection.
What we've found is that
whether or not SPDY is going
to be faster is dependent
on both network conditions
and your workload.
In general, our guidance
is that if you are going
to issue many requests,
particularly requests
for small size objects,
you're going
to see a performance
gain with SPDY.
But if you're writing an app
that's downloading a handful
of files, perhaps though
also downloading large files,
such as a movie,
SPDY is not going
to be advantageous for you.
So keep in mind that you control
whether or not SPDY's going
to be used based on whether
or not it's turned
on on your server.
I also want to note that the
SPDY specification indicates
that HTTP headers can be
compressed to boost performance.
But it turns out this
is actually susceptible
to the crime vulnerability,
which you can read about online.
So in our implementation,
as is in many common
SPDY implementations,
we've actually disabled
this for user privacy.
So you're not going to
see a performance boost
from compressed headers.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
from compressed headers.
Finally, I want to point
out SPDY is not an
IETF-recognized standard.
But we see it as a protocol that
is paving the way for HTTP/2.0.
In particular it gives you,
developers, the opportunity
to get a jump start on
using a protocol that relies
on a single, long-lived
connection with multiplexing,
which is the same direction
that HTTP/2.0 is heading.
I want to wrap up and talk
about some best practices
for working with SPDY.
Keep in mind, these are not
necessarily hard and fast rules
that you always have to follow,
but these are good suggestions
just to keep in the back
of your mind as you
work with SPDY.
The first is, it's best if you
can issue your requests as soon
as you want the resources.
In other words, what I mean is
you might have previously held
back and actually manually
scheduled requests to try
to avoid Head-of-Line Blocking.
But because Head-of-Line
Blocking is no longer
of concern, we ask that you
enqueue all your requests right
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
of concern, we ask that you
enqueue all your requests right
away, and let multiplexing
do what it's meant to do.
Also, you might have
spread your content
across multiple host names,
called hostname sharding.
So if some resources might have
been, like, on css.apple.com,
some would have been on
images.apple.com and so on.
For HTTP/1.1 this made sense.
It would cause us to open
multiple TCP connections
which would boost performance.
But since SPDY relies on a
single, long-live connection
which gives us optimal
connection reuse,
it's actually best if you
unshard and consolidate all
of your content to a single
hostname and single port,
so we can have that one,
long-lived connection
to give you the best
experience possible.
I'd like to turn
things over to Dan.
He's going to be talking
about Background
Networking and Extensions.
>> Good afternoon, everyone.
I'm Dan. And as Scott said,
I'm going to be talking
about background networking.
So Steve mentioned before
that you can enable
background networking
in your applications
using NSURLSession
if you use the background
session configuration
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
if you use the background
session configuration
with identifier factory method.
I'd like to give you a brief
overview of what I'm going
to be talking about today.
First, I'm going to
go over why you'd want
to use background sessions
and background networking
in your applications.
Tying in with this, I'll talk
about using background sessions
in app extensions,
which is a new feature
in iOS 8 and OS X Yosemite.
I'll then talk about
discretionary networking,
which is a feature of background
networking that allows us
to schedule tasks
when it's appropriate,
given current power conditions.
Finally, I'll talk about using
background sessions properly
in your applications, going
over a couple use cases,
and talking about some common
pitfalls and best practices.
So why do you want
to use background sessions
in your applications?
Well, the main benefit,
particularly
on iOS is for multitasking.
In a background session,
file-based uploads
and downloads can
continue out of process,
even while your app
isn't running.
This means that your
app can crash even.
It can be terminated.
Or, on iOS, it can be suspended
and those file-based uploads
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Or, on iOS, it can be suspended
and those file-based uploads
and downloads will still
continue, and will actually wake
up your app on iOS in the
background to handle things
like authentication
challenges and the completion
of all the tasks
in your sessions.
Another benefit is that
in a background session,
we monitor the network and
power environment for you.
This means that we cover things
like network reachability
and connectivity for
you, so you don't have
to use the reachability
APIs at all.
We won't attempt to establish
a connection until we know
that the server is reachable.
And similarly, if the user is
performing a download and steps
out of Wi-Fi, normally
that task would then fail
with a transmission error.
But, in a background session,
we'll actually recover
from that automatically
and retry it and resume
where we left off if the
download is resumable.
And you won't hear
about that error.
For discretionary tasks, we also
perform some battery monitoring
so that we don't perform a
task if the user's really low
on battery and not charging.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
We'll also do bandwidth
monitoring.
What this means is
that if the user's
on a really flaky Wi-Fi network
and isn't making really
sufficient throughput,
than we'll stop and
just retry automatically
when network conditions
are better.
I'd like to talk about
using background sessions
in app extensions, which
as I said are a new feature
of iOS 8 and OS X Yosemite.
Now extensions are very
short-lived processes.
Generally they're only going
to be running while they're
actually active on screen,
and that's going to be a very
short period of time usually.
In-process networking
really isn't sufficient
in an app extension if you
need to do any moderate-
to large-sized upload
or download.
But if you use a
background session,
then that task will
actually be performed
by our background daemon.
So your app extension can exit
or be suspended and, on iOS,
when that task completes or
authentication is required,
will actually launch the app
that your extension's shipped
with in order to
handle those events.
So essentially, your
app extensions
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So essentially, your
app extensions
and your apps can share
background sessions.
This leads us to a
couple constraints
when using background
sessions in app extensions.
The first is that in order
to use a background session
in an app extension, you need
to use a shared data container.
Now the reason for this
is that by default an app
and any extensions
that it ships with are
in different data containers
and won't have access
to the same sets of files.
But in Xcode you can create
an application group using the
Capabilities tab.
And if you create an
application group,
you can tell us the
identifier of that app group
of that group container,
and will download
into that container.
I'll show you how to
do that momentarily.
Another caveat is that only
one process can be connected
to the background
session at a time.
This means that if your app
is running in the background,
and then another app launches
and brings up your extension,
it won't be able to use that
same background session.
Now a background session
is essentially defined
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Now a background session
is essentially defined
by the identifier that you use
when you create your background
session configuration object.
What we recommend here is
that you use a different
background session identifier
for your app and for each
extension that you ship with.
The only time that you should
take advantage of the fact
that your app and your
extensions can share background
sessions is when
we launch your app
to handle events
for that session.
Now I mention that you need
to use a shared data container
when using background
sessions and app extensions.
You can specify that using the
shared container identifier
property on
NSURLSessionConfiguration.
What you do is you just create
a configuration object using the
backgroundSessionConfiguration
WithIdentifier factory method
that Steve showed you before.
And you set the
sharedContainerIdentifier
property to the identifier of
your shared group container.
Then you can create
an NSURLSession
from that configuration object
and create tasks
in that session.
Now I'd like to switch
gears and talk
about discretionary
networking, which is something
that some background tasks can
use in order to perform tasks
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
that some background tasks can
use in order to perform tasks
at power-optimal times.
What this means is, that we will
take into account things like,
whether or not we're on a Wi-Fi
network or using cellular data.
And we'll also take into account
the current battery state,
whether or not we're charging,
how much-what percentage
the battery is charged.
We also take into account things
like how often your
app is launched.
If it's something that a user
launches really frequently,
we'll be more likely to
treat a task more urgently,
since the user's
more likely to notice
when those resources
are downloaded.
One improvement that
we've made in iOS 8
for discretionary transfers,
is that tasks are treated
with more urgency
as time goes on.
In iOS 7, discretionary
transfers were limited
to Wi-Fi only.
Now, while we may limit
discretionary transfers
to run while there is Wi-Fi
and plugged in at first,
these constraints will
relax as time goes on.
As we approach the resource
timeout that's specified
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
As we approach the resource
timeout that's specified
on your configuration
object, we'll be more likely
to relax these conditions
and allow transfers
over battery and cellular data.
There are a couple
different ways
that you can use
discretionary networking
in a background session.
One way is by explicitly opting
in using the discretionary
property which is just a boolean
in an NSURLSessionConfiguration.
This was made available on iOS 7
and is now available
on OS X Yosemite.
You might want to do
this for any tasks
that aren't really
user-initiated.
So let's say you have, you know,
an app that lets users watch
episodes of a TV series.
And they're watching an episode
that they've downloaded already,
and maybe you want to pre-fetch
the next one so it's ready
for them when they're finished
watching the current episode.
Now, that's not something the
user explicitly requested.
So that's something
that you might want
to treat as discretionary.
Similarly, if you have an app
and the user is modifying
a document, you might want
to upload that document
to your app servers
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
to upload that document
to your app servers
so that it's accessible from
the users other devices.
And this might also be
something that's discretionary.
On iOS there are also times
when we will treat tasks
as discretionary automatically.
This happens when your app
is running in the background.
In iOS 7, we introduced a
few new multitasking APIs.
Most notably background
fetch updates,
and handling silent
push notifications
where your app gets a limited
amount of time, on the order
of 30 to 60 seconds, to run in
the background to make updates.
When used in-this is really
great when used in conjunction
with background uploads
and downloads.
But because the user doesn't
know that your app is running
at this time, any
downloads or uploads
that you enqueue will be treated
as discretionary automatically
because this work can't
be user-initiated,
because the user doesn't
know that you're running.
One improvement that we've
made here in iOS 8 is that any
of these tasks that
we automatically treat
as discretionary will
become non-discretionary
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
as discretionary will
become non-discretionary
if the user launches the app
and brings it to the foreground.
I've mentioned a couple times
that on iOS we will launch
your apps in the background
to handle events
like authentication
and the completion
of all your tasks.
I want to talk a little bit
about how you handle
those events.
In iOS 7, we introduced a new
method on UIApplicationDelegate,
called application:
handleEventsForBackground
URLSession:completionHandler.
When this is called, you'll be
provided with the identifier
of the session that
needs your attention.
And at this point you should
reconnect to that session
by creating a background
configuration object
with that identifier,
and then an NSURLSession
from that configuration object.
At this point you'll immediately
begin receiving the delegate
messages that you missed.
Maybe authentication challenges
or didCompleteWithError
callbacks.
And after you're finished
handling these events,
you'll want to call
the completionHandler
that was provided to your
UIApplicationDelegate.
This completionHandler allows
us to take a snapshot of your UI
to show up in the app switcher,
so that it's up-to-date
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
to show up in the app switcher,
so that it's up-to-date
when the user double-clicks
the home button.
It also allows us
to suspend your app.
Now, when you're
launched to handle events
for background sessions, you'll
again be given a limited amount
of time to run
in the background-around
30 to 60 seconds.
If you don't call your
completion handler
within this time, your
app will be terminated,
meaning it won't have
an up-to-date snapshot
and it will be slower to launch
the next time the user wants
to launch your app.
So it's important that you
call this completionHandler
so your app gets suspended
and has an up-to-date UI.
Once you're finished receiving
all the pending events
from your NSURLSession,
we'll deliver the URLSessionDid
FinishEventsForBackground
URLSession message to
your NSURLSessionDelegate.
And this is an indication
of when it's a good time
to call your completionHandler
past your UIApplicationDelegate.
I'd also like to talk
about using data tasks
in background sessions.
Steve mentioned earlier
that data tasks,
instead of downloading to a file
or on disk will just deliver
didReceiveData callbacks
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
or on disk will just deliver
didReceiveData callbacks
in memory.
Now data tasks were unavailable
in background sessions
in iOS 7 and OS X Mavericks.
But they're now available
in iOS 8 and OS X Yosemite
with one restriction, which is
that we will only
perform a data task
in the background session while
your app is actually running.
If your app gets suspended or is
terminated, then there is no one
to deliver that data to,
so it makes no sense for us
to continue performing it.
However, you can
convert it to a download
when you receive the response.
Steve eluded to this
earlier when talking
about the didReceiveResponse
callback.
That delegate message
provides a completionHandler.
And you can pass
NSURLSessionResponse
BecomeDownload to
that completionHandler
and then we'll start
streaming the bytes to a file
on disk rather than memory.
Once you do that, this
becomes a download
that can be continued even after
your app is suspended or exits.
Now I'd like to talk about
a couple common pitfalls
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Now I'd like to talk about
a couple common pitfalls
that we've seen from
apps in the past
when using background sessions.
One common pattern that we've
seen that we'd like developers
to avoid is a pattern where
they create one task at a time.
So you might, for instance,
be downloading a large video
from your server that's split
up into many different smaller
segments, each a separate file.
If you download all of
these and if you, let's say,
create a download task for the
first task for the first asset
and then create a
second download task
for the next asset once
the first one completes,
this is a really bad
pattern, particularly because,
if the user suspends the app
by going to the home screen
at any point, than you'll
actually need to be re-launched
in the background once the
current download finishes before
you can enqueue your next one.
This means because it's
running in the background
that it will automatically
be treated as discretionary,
which means we won't guarantee
that it will start right away.
In particular, even
if conditions are great-we're
100% charged and connected
to a power source
and we're connected
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
to a power source
and we're connected
to a great Wi-Fi network,
we still make no guarantees
that we'll start right away.
In particular, the
system will take measures
to prevent your app from
being launched too frequently.
So you can't rely on launches
for background sessions
for any sort of regular
launch events.
Tying in with this, it's
much better if instead
of downloading lots of
small assets like this,
if you can zip these
up on your server
into one large zipped asset.
That's much more efficient
for a background download.
And finally, blocking
while waiting for transfers
to complete is just
a really bad idea
when doing any kind
of networking.
But it's particularly bad
in a background session
where we recover from
network failures automatically
and don't tell you
about them and, again,
for discretionary transfers
where we're not guaranteed
to start right away.
I'd also like to go over
a couple best practices
for using background sessions
and also NSURLSession
in general.
One common mistake that we
think some people have made
in the past is that they've
assumed that when running
in the background
to handle things
like a background fetch update
or a silent push notification,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
like a background fetch update
or a silent push notification,
that they've been required
to use background sessions.
Now, using background uploads
or downloads with these forms
of multitasking works
really well,
but in particular
for large downloads.
When you're running for
a background fetch update
or a silent push notification,
as I said, you'll have about 30
to 60 seconds to run
in the background.
Now if you have any
small networking tasks
that could finish within this
time, it's perfectly okay
to do them in an in-process
or default NSURLSession.
If you have something like a
Twitter client that's going
to be downloading a few tweets
for a background fetch update,
that's totally doable
in process.
On the other hand, if you
want to download, you know,
the next episode of a TV series,
that's something that's fairly
large and probably not going
to finish within the
60 seconds you have
to run-that should be done
in a background session.
It's also important that you
support resumable downloads
on your servers.
As I mentioned in a background
session, we retry automatically
after network failures
if the user goes off
of Wi-Fi, for instance.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
of Wi-Fi, for instance.
So if you support resumable
downloads, we don't have
to re-download bytes
we've already received.
But if you don't, than we have
to start from the beginning.
Now supporting resumable
downloads is generally just
as simple as supporting Range
GET requests on your servers.
And most servers should
support this configuration
out of the box.
Finally, it's very important
that you handle launch
events properly,
as I talked about before.
This means when you get launch
to handle a background session,
you should reconnect to
that background session
and handle any messages.
This is particularly
important with authentication.
Because if you don't respond
to an authentication challenge,
then that task will
timeout and fail.
And that's not what
the user wants.
Finally, be sure to call the
completion handler that's past
your UIApplicationDelegate.
As I said before, this allows
your UI to be up-to-date
in the app switcher and makes
it so your app is suspended
after it's completed
instead of being terminated.
Before I wrap up, I'd just
like to give a brief summary
of what we discussed today.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
of what we discussed today.
First we talked about new APIs
and NSStream and NSNetService.
We gave a review of using
NSURLSession from what we talked
about in last year's
WWDC session.
We talked about SPDY and
new protocol support.
And went over some new
features and best use cases
in background networking.
In case you don't get the
chance to ask us any questions
in person this week
while you're here,
Paul Danbold is our
Technologies Evangelist.
And he's a great person
to ask for any questions
that you might have that
you don't get a chance
to ask us while you're here.
We also have plenty of
documentation available
at developer.apple.com,
including for NSURLSession.
The Apple developer forums
are another great place
to ask questions that you don't
get to ask while you're here.
I'd also like to point you
at a couple related sessions
that are going on this week.
In particular you
might be interested
in the Extensions sessions.
There are two of them.
You can learn more about
creating app extensions,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
You can learn more about
creating app extensions,
and we'll also talk a little bit
about using background
sessions and app extensions.
With that, I'd like to
thank you all for coming.
And we look forward to seeing
the amazing apps you create
with iOS 8 and OS X
Yosemite [applause].