NSURLProtocol is like a magic key to the URL. It lets you redefine how Apple’s URL Loading System operates, by defining custom URL schemes and redefining the behavior of existing URL schemes.
Does that sound magical? It should. Because, if you look for it, I’ve got a sneaky feeliing you’ll find URLs — much like love — are all around us. What does UIWebView use? URLs. What’s used for video streaming with MPMoviePlayer? URLs. How do you send someone to your app on iTunes, initiate FaceTime or Skype, launch another app on the system, or even embed an image in an HTML file? With a URL. Have a peek at NSFileManager and notice what many of its file-manipulation methods require and return — URLs.
NSURLProtocol
is awesome because it lets your app speak the language of love…um, URLs. It can also be used to make deep changes to how they are processed. You can add new networking protocols that existing URL-based components and libraries can automatically use. Or, you can modify how existing protocols work. For instance, you can log all network requests, modify outgoing and incoming information streams or service some requests differently and transparently, e.g., from a cache.
By default, iOS URL Loading System supports the following schemes:
- ftp:// for File Transfer Protocol
- http:// for Hypertext Transfer Protocol
- https:// for Hypertext Transfer Protocol with encryption
- file:// for local file URLs
- data:// for data URLs
In this NSURLProtocol tutorial, you’ll learn how to define a protocol handler that modifies URL schemes. It will add a rough and ready transparent caching layer, by storing retrieved resources in Core Data. By enabling it, an ordinary UIWebView can then take on the role of a browser by caching downloaded pages for offline viewing at a later time.
Before you dive in head first, you’ll need a basic understanding of networking concepts and familiarity with how NSURLConnection
works. If you are not currently familiar with NSURLConnection
then I suggest reading this tutorial and/or this document by Apple.
However, nothing about NSURLProtocol
is specific to NSURLConnection
. You can implement a custom protocol that uses:
- Lower level networking primitives
- Just the filesystem
- Or pure computation.
Custom protocols can configure the behavior of NSURLConnection
, and also of the newNSURLSession
-based networking facilities. Cool!
So are you ready to learn what you can do with NSURLProtocol
? Good, go pour yourself a cuppa something and settle in for a meaty, mind-broadening discussion and step-by step exercise. The first thing to discuss in what it is and how it works.
What Is NSURLProtocol?
A set of classes known as the URL Loading System, handles URL requests. You need to know them to find out how iOS handles your app’s requests to load a URL-based resource.
At the heart of the URL Loading System is the NSURL
class. For network requests, this class tells what host your app is trying to reach and path to the resource at that host. In addition the NSURLRequest
object adds information like HTTP headers, the body of your message, etc.. The loading system provides a few different classes you can use to process the request, the most common being NSURLConnection
and NSURLSession
.
When you receive a response it comes back in two parts: metadata and data. The metadata is encapsulated in a NSURLResponse
object. It will tell you the MIME type, the text encoding (when applicable), the expected amount of data of your response and the URL that is the source of the information. The data arrives as NSData
objects.
Behind the scenes, when the loading system downloads information using a NSURLRequest
, it will create an instance of a NSURLProtocol
subclass, which represents your custom URL protocol. NSURLProtocol
is an abstract class that exists only to be extended in this way. You should never instantiate an NSURLProtocol
object directly.
Note: Remember that Objective-C doesn’t actually have abstract classes as a first class citizen. It’s only by definition and documentation that a class is marked as abstract.
Given the name of NSURLProtocol
you could be forgiven for thinking that it’s an Objective-C protocol. Strangely, it isn’t. It’s a class. But it is used in a way that is very similar to a protocol as it’s defining a set of methods that must be implemented by something that conforms to NSURLProtocol
. A protocol was probably not flexible enough for Apple, so they chose to use an abstract base class.
When a subclass of NSURLProtocol
handles a request, it’s the subclass’ job to create theNSURLResponse
objects to encapsulate the response. Once you have registered your ownNSURLProtocol
, the loading system will search for the first one equipped to handle a specific NSURLRequest
.
When To Use NSURLProtocol?
How can you use NSURLProtocol
to make your app cooler, faster, stronger and jaw-droppingly awesome? Here are a few examples:
Provide Custom Responses For Your Network Requests:
It doesn’t matter if you’re making a request using a UIWebView
, NSURLConnection
or even using a third-party library (like AFNetworking, MKNetworkKit, your own, etc, as these are all built on top of NSURLConnection
). You can provide a custom response, both for metadata and for data. You might use this if you wanted to stub out the response of a request for testing purposes, for example.
Skip Network Activity and Provide Local Data:
Sometimes you may think it’s unnecessary to fire a network request to provide the app whatever data it needs. NSURLProtocol can set your app up to find data on local storage or in a local database.
Redirect Your Network Requests:
Have you ever wished you could redirect requests to a proxy server — without trusting the user to follow specific iOS setup directions? Well, you can! NSURLProtocol gives you what you want — control over requests. You can set up your app to intercept and redirect them to another server or proxy, or wherever you want to. Talk about control!!
Change the User-agent of Your Requests:
Before firing any network request, you can decide to change its metadata or data. For instance, you may want to change the user-agent. This could be useful if your server changes content based on the user-agent. An example of this would be differences between the content returned for mobile versus desktop, or the client’s language.
Use Your Own Networking Protocol:
You may have your own networking protocol (for instance, something built on top of UDP). You can implement it and, in your application, you still can can keep using any networking library you prefer.
Needless to say, the possibilities are many. It would be impractical (but not impossible) to list all the possibilities you have with NSURLProtocol
in this tutorial. You can do anything you need with a given NSURLRequest
before it’s fired by changing the designated NSURLResponse. Better yet, just create your own NSURLResponse
. You’re the developer, after all.
While NSURLProtocol
is powerful, remember that it’s not a networking library. It’s a tool you can use in addition to the library you already use. In short, you can take advantage ofNSURLProtocol
‘s benefits while you use your own library.
Right, now on to some code…
Getting Started
Now it’s time to get your hands dirty! Next, you’ll develop a simple project so you have somewhere to get to know NSURLProtocol
. After completing this exercise you’ll know how to customize how an app loads URL data.
You’ll build an elementary mobile web browser, such as one that you might add to your next app. It will have a basic user interface that lets the user enter and go to a URL. The twist is that your browser will cache successfully retrieved results. This way the user can load pages he’s already visited in the twinkle of an eye, because the page won’t load from a network request, but from the app’s local cache.
You already know that fast page loads = happy users, so this is a good example of howNSURLProtocol
can improve your app’s performance.
These are the steps you’re going to go through:
- Use a UIWebView for displaying the websites
- Use Core Data, for caching the results.
If you’re not familiar with Core Data, you can take a look into our tutorial. However, the code in this tutorial should be enough to understand the possibilities of NSURLProtocol
. Using Core Data is just a simple way to implement the local cache, so it’s not essential to learn something useful here.
Starter project overview
You can download the starter project here. As soon as the download is finished, unzip it and open the project file. It will look like this:
When you open the project, there are two main files. The first one is the Main.storyboardfile. It has the UIViewController set up the way you need for implementation. Notice the UITextField (for URL input), UIButton (for firing the web requests) and UIWebView.
Open BrowserViewController.m. Here you’ll see the basic behavior set up for the UI components. This UIViewController implements the UITextViewDelegate, so you can fire the request when the user taps the return key. The IBAction for the button is pre-set to behave the same way as the return key. Last, the sendRequest method just takes the text from the textfield, creates a NSURLRequest object and sends calls the loadRequest: method from UIWebView to load it.
Once you’re familiarized with the app, build and run! When the app opens, enter “http://raywenderlich.com” and press the “Go” button. The UIWebView will load the response and display the results in the app. Pretty simple for a starting point. Now it’s time for you to stretch those finger muscles. Up next….coding!
Intercepting network requests
Now it’s time to start intercepting all NSURLRequest’s fired by the app. For that, you’ll need to create your own NSURLProtocol
implementation.
Click File\New\File…. Select Objective-C class and hit the Next button. In the Class field, enter MyURLProtocol and in Subclass of field, enter NSURLProtocol. Finally press Nextand then Create when the dialog appears.
Open MyURLProtocol.m and add this method to the class implementation:
|
Every time the URL Loading System receives a request to load a URL, it searches for a registered protocol handler to handle the request. Each handler tells the system whether it can handle a given request via its +canInitWithRequest:
method.
The parameter to this method is the request that the protocol is being asked if it can handle. If the method returns YES
, then the loading system will rely on this NSURLProtocol
subclass to handle the request, and ignore all other handlers.
If none of the custom registered handlers can handle the request, then the URL Loading System will handle it by itself, using the system’s default behavior.
If you want to implement a new protocol, like foo://, then this is where you should check to see if the request’s URL scheme was foo. But in the example above, you’re simply returningNO
, which tells you your app cannot handle the request. Just hold on a minute, we’ll start handling them soon!
Now, register this protocol with the loading system. Open AppDelegate.m. Then add an import for MyURLProtocol.h at the top and insert the following code within the -application:didFinishLaunchingWithOptions:
method:
|
Now that you’ve registered the class with the URL Loading System, it will have the opportunity to handle every request delivered to the URL Loading system. This includes code which calls the loading system directly, as well as many system components that rely on the URL loading framework, such as UIWebView.
Now, build and run. Insert “http://raywenderlich.com” as the website, click on “Go” and check the Xcode console. Now, for every request the app needs to perform, the URL Loading System asks your class if it can handle it.
In the console you should see something like this:
|
For now, you’re just logging the string representation of the request’s URL and returning NO
, which means your custom class cannot handle the request. But if you look into the logs, you’ll see all the requests made from the UIWebView. It includes the main website (.html) and all the assets, such as JPEGs and CSS files. Every time the UIWebView needs to fire a request, it’s logged to the console before it’s actually fired. The count should show you a mountain of requests — likely over five hundred — because of all the assets on the Ray Wenderlich page.
Custom URL Loading
“I love it when pages take forever to load” said no user, ever. So now you need to make sure your app can actually handle the requests. As soon as you return YES
in your+canInitWithRequest:
method, it’s entirely your class’s responsibility to handle everything about that request. This means you need to get the requested data and provide it back to the URL Loading System.
How do you get the data?
If you’re implementing a new application networking protocol from scratch (e.g. adding afoo:// protocol), then here is where you embrace the harsh joys of application network protocol implementation. But since your goal is just to insert a custom caching layer, you can just get the data by using a NSURLConnection.
Effectively you’re just going to intercept the request and then pass it back off to the standard URL Loading System through using NSURLConneciton.
Data is returned from your custom NSURLProtocol
subclass through a NSURLProtocolClient
. Every NSURLProtocol
object has access to it’s “client”, an instance of NSURLProtocolClient
. (Well, actually NSURLProtocolClient
is a protocol. So it’s an instance of something that conforms to NSURLProtocolClient
).
Through the client, you communicate to the URL Loading System to pass back state changes, responses and data.
Open MyURLProtocol.m. Add the following class continuation category at the top of the file:
|
Next, find +canInitWithRequest:
. Change the return to YES
, like this:
|
Now add four more methods:
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { return request; } + (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b { return [super requestIsCacheEquivalent:a toRequest:b]; } - (void)startLoading { self.connection = [NSURLConnection connectionWithRequest:self.request delegate:self]; } - (void)stopLoading { [self.connection cancel]; self.connection = nil; } |
+canonicalRequestForRequest:
is an abstract method from NSURLProtocol
. Your class must implement it. It’s up to your application to define what a “canonical request” means, but at a minimum it should return the same canonical request for the same input request. So if two semantically equal (i.e. not necessarily ==) are input to this method, the output requests should also be semantically equal.
To meet this bare minimum, just return the request itself. Usually, this is a reliable go-to solution, because you usually don’t want to change the request. After all, you trust the developer, right?! An example of something you might do here is to change the request by adding a header and return the new request.
+requestIsCacheEquivalent:toRequest:
. is where you could take the time to define when two distinct requests of a custom URL scheme (i.e foo:// are equal, in terms of cache-ability. If two requests are equal, then they should use the same cached data. This concerns URL Loading System’s own, built-in caching system, which you’re ignoring for this tutorial. So for this exercise, just rely on the default superclass implementation.
-startLoading
and -stopLoading
are what the loading system uses to tell your NSURLProtocol
to start and stop handling a request. The start method is called when a protocol should start loading data. The stop method exists so that URL loading can be cancelled. This is handled in the above example by cancelling the current connection and getting rid of it.
Woo-hoo! You’ve implemented the interface required of a valid NSURLProtocol
instance. Checkout out the official documentation describing what methods an valid NSURLProtocol
subclass can implement, if you want to read more.
But your coding isn’t done yet! You still need to do the actual work of processing the request, which you do by handling the delegate callbacks from the NSURLConnection you created.
Open MyURLProtocol.m. Add the following methods:
|
These are all NSURLConnection delegate methods. They are called when the NSURLConnection you’re using to load the data has a response, when it has data, when it finishes loading and when it fails. In each of these cases, you’re going to need to hand this information off to the client.
So to recap, your MyURLProtocol handler creates its own NSURLConnection and asks that connection to process the request. In the NSURLConnection delegate callbacks methods above, the protocol handler is relaying messages from the connection back to the URL Loading System. These messages talk about loading progress, completion, and errors.
Look and you’ll see the close family resemblance in message signatures for the NSURLConnectionDelegate and the NSURLProtocolClient
— they are both APIs for asynchronous data loading. Also notice how MyURLProtocol uses its client
property to send messages back to the URL Loading system.
Build and run. When the app opens, enter the same URL and hit Go.
Uh-oh! Your browser isn’t loading anything anymore! If you look at the Debug Navigator while it’s running, you’ll see memory usage is out of control. The console log should show a racing scroll of innumerable requests for the same URL. What could be wrong?
In the console you should see lines being logged forever and ever like this:
|
Squashing the Infinite Loop with Tags
Think again about the URL Loading System and protocol registration, and you might have a notion about why this is happening. When the UIWebView wants to load the URL, the URL Loading System asks MyURLProtocol if it can handle that specific request. Your class saysYES
, it can handle it.
So the URL Loading System will create an instance of your protocol and call startLoading
. Your implementation then creates and fires its NSURLConnection. But this also calls the URL Loading System. Guess what? Since you’re always returning YES
in the +canInitWithRequest:
method, it creates another MyURLProtocol instance.
This new instance will lead to a creation of one more, and then one more and then an ifinite number of instances. That’s why you app doesn’t load anything! It just keeps allocating more memory, and shows only one URL in the console. The poor browser is stuck in an infinite loop! Your users could be frustrated to the point of inflicting damage on their devices.
Review what you’ve done and then move on to how you can fix it. Obviously you can’t just always return YES
in the +canInitWithRequest:
method. You need to have some sort of control to tell the URL Loading System to handle that request only once. The solution is in the NSURLProtocol
interface. Look for the class method called +setProperty:forKey:inRequest:
that allows you to add custom properties to a given URL request. This way, you can ‘tag’ it by attaching a property to it, and the browser will know if it’s already seen it before.
So here’s how you break the browser out of infinite instance insanity. Open MyURLProtocol.m. Then change the -startLoading
and the +canInitWithRequest:
methods as follows:
|
Now the -startLoading
method sets a NSNumber instance (<code@yes< code="">) for a given key (@"MyURLProtocolHandledKey"
) and for a given request. It means the next time it calls+canInitWithRequest:
for a given NSURLRequest instance, the protocol can ask if this same property is set.
If it is set, and it’s set to YES, then it means that you don’t need to handle that request anymore. The URL Loading System will load the data from the web. Since your MyURLProtocol instance is the delegate for that request, it will receive the callbacks from NSURLConnectionDelegate.
Build and run. When you try it now, the app will successfully display web pages in your web view. Sweet victory! You might be wondering why you did all of this just to get the app to behave just like it was when you started. Well, because you need to prepare for the fun part!
The console should now look something like this:
|
Now you have all the control of the URL data of your app and you can do whatever you want with it. It’s time to start caching your app’s URL data.
Implementing the Local Cache
Remember the basic requirement for this app: for a given request, it should load the data from the web once and cache it. If the same request is fired again in the future, the cached response will be provided to the app without reloading it from the web.
Now, you can take advantage of Core Data (already included in this app). Open NSURLProtocolExample.xcdatamodeld. Select the Event entity, then click on it again so that it lets you rename it. Call it CachedURLResponse.
Next, click on the + button under Attributes to add a new attribute and name it data with Type set to Binary Data. Do the same thing again to create the properties encoding(String), mimeType (String) and url(String). Rename timeStamp to timestamp. At the end, your entity should look like this:
Now you’re going to create your NSManagedObject subclass for this entity. SelectFile\New\File…. On the left side of the dialog, select Core Data\NSManagedObject. Click on Next, leave the checkbox for NSURLProtocolExample selected and hit Next. In the following screen, select the checkbox next to CachedURLResponse and click Next. Finally, click Create.
Now you have a model to encapsulate your web data responses and their metadata!
It’s time to save the responses your app receives from the web, and retrieve them whenever it has matching cached data. Open MyURLProtocol.h and add two properties like so:
|
The response
property will keep the reference to the metadata you’ll need when saving the response from a server. The mutableData
property will be used to hold the data that the connection receives in the -connection:didReceiveData:
delegate method. Whenever the connection finishes, you can cache the response (data and metadata).
Let’s add that now.
Open MyURLProtocol.m. Change the NSURLConnection delegate methods to the following implementations:
|
Instead of directly handing off to the client, the response and data are stored by your custom protocol class now.
You’ll notice a call to an unimplemented method, saveCachedResponse. Let’s go ahead and implement that.
Still in MyURLProtocol.m, add imports for AppDelegate.h and CachedURLResponse.h. Then add the following method:
|
Here is what that does:
- Obtain the Core Data
NSManagedObjectContext
from theAppDelegate
instance. - Create an instance of
CachedURLResponse
and set its properties based on the references to theNSURLResponse
andNSMutableData
that you kept. - Save the Core Data managed object context.
Build and run. Nothing changes in the app’s behavior, but remember that now successfully retrieved responses from the web server save to your app’s local database.
Retrieving the Cached Response
Finally, now it’s time to retrieve cached responses and send them to the NSURLProtocol
‘s client. Open MyURLProtocol.m. Then add the following method:
|
Here’s what it does:
- Grab the Core Data managed object context, just like in
saveCachedResponse
. - Create an
NSFetchRequest
saying that we want to find entities called CachedURLResponse. This is the entity in the managed object model that we want to retrieve. - The predicate for the fetch request needs to obtain the
CachedURLRepsonse
object that relates to the URL that we’re trying to load. This code sets that up. - Finally, the fetch request is executed.
- If there are any results, then the first result is returned.
Now it’s time to look back at the -startLoading
implementation. It needs to check for a cached response for the URL before actually loading it from the web. Find the current implementation and replace it withe the following:
|
Here’s what that does:
- First, we need to find out if there’s a cached response for the current request.
- If there is then we pull all the relevant data out of the cached object.
- An
NSURLResponse
object is created with the data we have saved. - Finally, for the cached case, the client is told of the response and data. Then it immediately is told the loading finished, because it has! No longer do we need to wait for the network to download the data. It’s already been served through the cache! The reasonNSURLCacheStorageNotAllowed is passed to the client in the response call, is that we don’t want the client to do any caching of its own. We’re handling the caching, thanks!
- If there was no cached response, then we need to load the data as normal.
Build and run your project again. Browse a couple of web sites and then stop using it (stop the project in Xcode). Now, retrieve cached results. Turn the device’s Wi-Fi off (or, if using the iOS simulator, turn your computer’s Wi-Fi off) and run it again. Try to load any website you just loaded. It should load the pages from the cached data. Woo hoo! Rejoice! You did it!!!
You should see lots of entries in the console that look like this:
|
That’s the log saying that the response is coming from your cache!
And that’s that. Now your app successfully caches retrieved data and metadata from web page requests. Your users will enjoy faster page loads and superior performance! :]
Where To Go From Here
Here is where you can download the final code for this tutorial.
This example covered a simple usage of NSURLProtocol
, but don’t mistake it as a complete solution for caching. There is a lot more to implementing a production-quality caching browser. In fact, the loading system has built-in caching configurations, which are worth getting to know. The goal of this tutorial is simply to show you the possibilities. BecauseNSURLProtocol
has access to the data going in and out of so many components, it’s very powerful! There are almost no limits to what you can do implementing the -startLoading
method.
While IETF’s RFC 3986 may modestly define URLs as a “…compact sequence of characters that identifies an abstract or physical resource…” the truth is that the URL is its own mini language. It’s the domain-specific language (DSL) for naming and locating things. It’s probably the most pervasive domain-specific language in the world, considering that URLs have crawled out of the screen and are now broadcast in radio and TV advertisements, printed in magazines and splashed on shop signs all over the world.
NSURLProtocol
is a language you can use in a myriad of ways. When Twitter wanted to implement the SPDY protocol on iOS, an optimized successor to HTTP 1.1, they did it withNSURLProtocol
. What you use it for, is up to you. NSURLProtocol
gives you power and flexibility at the same time requires a simple implementation to accomplish your goals.
Please, feel free to leave any questions or suggestions about this tutorial in our forum discussion. It’s right below!
原文地址:http://www.raywenderlich.com/59982/nsurlprotocol-tutorial