Fork me on GitHub

使用NSURLCache让本地数据来代替远程UIWebView请求

原文作者:Matt Gallagher

原文链接:http://cocoawithlove.com/2010/09/substituting-local-data-for-remote.html

翻译:http://disanji.net/2010/11/24/substituting-local-data-for-remote-html/

cachedResponseForRequestCocoaCocoaWithLoveiOSMatt GallagherNSURLCacheUIWebViewUIWebViewDelegate

在研究苹果官方Demo:XMLPerformance 的时候,对NSURLCache的功能和使用并不是很了解,在网上找了一篇比较有代表性的文章好好地拜读了下!

正文:

在这篇文章中,我将讲述如何在iOS中的UIWebView中加载一个网页,使用修正的NSURLCache来用本地网页资源复本来代替基于远程网页的数据复本。

介绍

正常情况下当你需要写一个具备网络连接的iOS程序,你会想要一个本地的iOS接口能够接收网络上的所有数据。

然而,在项目中总是有一些限制你可以实现的东西,而且有时候你可能想要为用户显示一个规整的页面。

如果你打算采用这种方式,你最好确信网络接口尽可能流畅。你可以采取的措施之一是将图片的本地复本和其他非更新的资源包含到程序中。

为了在一个远程加载的网页中使用本地资源,或者需要远程页面以某种方式参考本地资源(例如通过URL主题),或者需要用本地地址来代替远程地址。

在这个文章中,我将讲述当网页包含远程资源时如何用本地资源来替代。

NSURLCache

在Mac上,你可以在WebViewDelegate上使用一系列不同的方式来实现,包括实现webView:resource:willSendRequest:redirectResponse:fromDataSource来使得NSURLRequest代替另一个。不幸的是,iOS中的UIWebViewDelegate并不如此好用因此我们需要以另外的方式来实现。

幸运的是,还有一点你可以利用:就是NSURLCache在几乎每个请求下都会被调用。

正常情况下,只有很少的数据存储在NSURLCache中,特别是在更旧的iOS设备上,这个存储区很小。即使你利用setMemoryCapacity:函数来增加这个缓存的大小,它相对于Mac上的NSURLCache来说还是太小了以至于不能存储资源。

当然在这个例子中那不是问题,因为我们将会子类化NSURLCache并且实现自定义的版本,该版本将保证可以存储我们所需的资源而且不需要pre-caching(在程序运行之前所有的资源都要保证准备在存储去内)。

cachedResponseForRequest:

唯一一个我们需要重写的函数是cachedResponseForRequest:,这能够允许我们在它发送前查看每一个请求而且如果我们需要的话返回本地数据。

在这个代码中,我会使用词典来将远程URL映射为在本地程序相关库中的资源的文件名。如果一个请求是指向特定的URL,那么将返回本地文件内容。

下面给出了这个词典。

- (NSDictionary *)substitutionPaths
{
return
[NSDictionary dictionaryWithObjectsAndKeys:
@"fakeGlobalNavBG.png",
@"http://images.apple.com/global/nav/images/globalnavbg.png",
nil];
}

只要针对URL:http://image.apple.com/global/nav/images/globalnavbg.png请求发出,那么下面的cachedResponseForRequest:可以利用资源文件夹中的fakeGlobalNavBG.png文件来代替。

- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request
{
// Get the path for the request
NSString *pathString = [[request URL] absoluteString];

// 判断我们是否为这个路径提供了替代资源
NSString *substitutionFileName = [[self substitutionPaths] objectForKey:pathString];
if (!substitutionFileName)
{
// 没有替代资源,返回默认值
return [super cachedResponseForRequest:request];
}

// 如果我们已经创建了一个缓存实例,那么返回它
NSCachedURLResponse *cachedResponse = [cachedResponses objectForKey:pathString];
if (cachedResponse)
{
return cachedResponse;
}

// 获得替代文件的路径
NSString *substitutionFilePath =
[[NSBundle mainBundle]
pathForResource:[substitutionFileName stringByDeletingPathExtension]
ofType:[substitutionFileName pathExtension]];
NSAssert(substitutionFilePath,
@"File %@ in substitutionPaths didn't exist", substitutionFileName);

// 加载替代数据
NSData *data = [NSData dataWithContentsOfFile:substitutionFilePath];

// 创建可缓存的响应
NSURLResponse *response =
[[[NSURLResponse alloc]
initWithURL:[request URL]
MIMEType:[self mimeTypeForPath:pathString]
expectedContentLength:[data length]
textEncodingName:nil]
autorelease];
cachedResponse =
[[[NSCachedURLResponse alloc] initWithResponse:response data:data] autorelease];

// 为后续响应,把它加入我们的响应词典中
if (!cachedResponses)
{
cachedResponses = [[NSMutableDictionary alloc] init];
}
[cachedResponses setObject:cachedResponse forKey:pathString];

return cachedResponse;
}

设置我们的缓存区作为共享缓存

一个UIWebView试图使用当前的+[NSURLCache sharedURLCache]。为了调用我的代码,你需要创建一个NSURLCache的子类并且调用+[NSURLCache setSharedURLCache:]。

这里需要注意:一旦你设置新的网络缓存,你可能打算保持它工作直到你的程序退出。

当UIWebView向你的NSURLCache请求资源时,它假设NSURLCache具备NSCachedURLResponse。如果当UIWebView正在使用它的时候你释放了NSCachedURLResponse,有可能你的程序会崩溃。

不幸的是,迫使WebKit释放它的参考(references)—在某些例子里它何时释放是不确定的。只有WebKit去调用removeCachedResponseForRequest:的时候它才通知你可以丢弃那些资源。

这意味着你必须保证程序中只有一个NSURLCache,在application:didFinishLaunchingWithOptions方法中进行设置并且不要移去它。

一个限制

显然地,如果你设置了要用来存储本地数据的缓存区,只有一个查看缓存区的请求才是使其生效。

这意味这如果URL请求是requestWithURL:cachePolicy:timeoutInterval:,缓存策略是NSURLRequestReloadIgnoringCacheData,那么这个请求将忽略本地替代。

默认情况下,NSURLRequests的缓存策略是NSURLRequestUseProtocolCachePolicy。这个HTTP的缓存策略是相当复杂的而且我从来没有见过一个正常的NSURLRequest忽视缓存,这些规则可能会在某些情况下产生它忽视缓存的情况。如果这些情况发生的话,你的程序应该保持正常工作。

本地替代缓存示例程序

LocalSubstitutionCache.zip

下面是程序截图

利用我们的NSURLCache子类调用了后,顶部灰色链接栏上的灰色链接按钮被在本地资源文件中的蓝色图像所代替。

结论

这个工作的意图是允许UIWebView响应更灵敏而且更像本地用户界面。

事实上,UIWebView决不会具有本地用户界面那样的集成度和灵敏的响应。但是

使得本地存储尽可能多的资源有助于尽可能少的带给用户不好的体验。

原文作者:Matt Gallagher

原文链接:http://cocoawithlove.com/2010/09/substituting-local-data-for-remote.html

Substituting local data for remote UIWebView requests

In this post, I’ll show you how you can load a webpage in a UIWebView in iOS while using a modified NSURLCache to substitute local copies of resources within the webpage for the remote copies referred to by the actual page.

Introduction

Normally if you’re writing an iOS app with network connectivity, you’ll want to put a native iOS interface on all data received over the network.

However, there are always scheduling and other constraints on a project that limit what you can implement and sometimes you may simply choose to show a regular, webpage to the user.

If you choose to take this approach, it is best to make sure the web interface feels as smooth as possible. One of the steps you can take to ensure this is to include local copies of all image and other non-updating resources within the application itself.

To use a local resource in an iOS webpage loaded from a remote location, either the remote page must refer to the local resource in some way (e.g. through a custom URL scheme) or you must swap a local location in place of a remote locations.

In this post, I’ll look at how we can substitute a local resource when the webpage contains references to remote resources.

NSURLCache

On the Mac, you could use a range of different approaches in the WebViewDelegate to do this, including implementing webView:resource:willSendRequest:redirectResponse:fromDataSource: to substitute one NSURLRequest for another. Unfortunately, the UIWebViewDelegate in iOS is not nearly as capable so we need to do this another way.

Fortunately, there is one point you can hook into that is invoked for (almost) every request: the NSURLCache.

Normally, very little is actually cached in the NSURLCache, particularly on older iOS devices where the cache size is downright miniscule. Even if you use the setMemoryCapacity: method to increase the size of the cache, it seems significantly less likely to store resources than the NSURLCache on the Mac.

Of course that doesn’t matter in this case, since we’re going to subclass NSURLCache and implement our own version that will be guaranteed to hold all the resources we need and won’t need pre-caching (all the resources will be there before the program is started).

cachedResponseForRequest:

The only important method we need to override is cachedResponseForRequest:. This will allow us to examine every request before it is sent and return local data if we prefer.

For this code, I’ll use a dictionary that maps remote URLs to local file names in the Resources folder of the application bundle. If any request is made for the specified URLs, the contents of the local file will be returned instead.

So given the following dictionary containing a single path for substitution

- (NSDictionary *)substitutionPaths
{
return
[NSDictionary dictionaryWithObjectsAndKeys:
@"fakeGlobalNavBG.png",
@"http://images.apple.com/global/nav/images/globalnavbg.png",
nil];
}

The following cachedResponseForRequest: implementation will substitute the contents of the fakeGlobalNavBG.png file in the Resources folder any time the URL http://images.apple.com/global/nav/images/globalnavbg.png is requested

- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request
{
// Get the path for the request
NSString *pathString = [[request URL] absoluteString];

// See if we have a substitution file for this path
NSString *substitutionFileName = [[self substitutionPaths] objectForKey:pathString];
if (!substitutionFileName)
{
// No substitution file, return the default cache response
return [super cachedResponseForRequest:request];
}

// If we've already created a cache entry for this path, then return it.
NSCachedURLResponse *cachedResponse = [cachedResponses objectForKey:pathString];
if (cachedResponse)
{
return cachedResponse;
}

// Get the path to the substitution file
NSString *substitutionFilePath =
[[NSBundle mainBundle]
pathForResource:[substitutionFileName stringByDeletingPathExtension]
ofType:[substitutionFileName pathExtension]];
NSAssert(substitutionFilePath,
@"File %@ in substitutionPaths didn't exist", substitutionFileName);

// Load the data
NSData *data = [NSData dataWithContentsOfFile:substitutionFilePath];

// Create the cacheable response
NSURLResponse *response =
[[[NSURLResponse alloc]
initWithURL:[request URL]
MIMEType:[self mimeTypeForPath:pathString]
expectedContentLength:[data length]
textEncodingName:nil]
autorelease];
cachedResponse =
[[[NSCachedURLResponse alloc] initWithResponse:response data:data] autorelease];

// Add it to our cache dictionary for subsequent responses
if (!cachedResponses)
{
cachedResponses = [[NSMutableDictionary alloc] init];
}
[cachedResponses setObject:cachedResponse forKey:pathString];

return cachedResponse;
}

Setting our cache as the shared cache

AUIWebView will try to use the current +[NSURLCache sharedURLCache]. To get our code called, you’ll need to create an instance of our NSURLCache subclass and invoke +[NSURLCache setSharedURLCache:].

A big warning here: once you set a new web cache, you probably want to leave it set until your program exits.

When the UIWebView requests resources from your NSURLCache, it assumes that the NSURLCache retains the NSCachedURLResponse. If you release the NSCachedURLResponse while any UIWebView is using it, it will probably crash your app.

Unfortunately, it is pretty hard to force WebKit to let go of its references — it can hold onto them indefinitely in some cases. Until WebKit itself chooses to invoke removeCachedResponseForRequest: to tell you that you can throw away the resource you must hold onto it.

What this means is that you should only have one NSURLCache in your program. Set it in your application:didFinishLaunchingWithOptions: method and never remove it.

A limitation…

Obviously, if you’re overriding the cache to substitute local data, it will only work if the request actually looks at the cache.

This means that if the URL is requested with requestWithURL:cachePolicy:timeoutInterval: with a cache policy of NSURLRequestReloadIgnoringCacheData, the the request will bypass this local substitution.

By default, NSURLRequests have a cache policy of NSURLRequestUseProtocolCachePolicy. The HTTP cache policy is pretty complicated and while I’ve never actually seen a normal NSURLRequest actually bypass the cache, the number of rules involved create a situation where it seems like it may be possible in some situations. Your app should not misbehave if this were to happen for some reason.

The LocalSubstitutionCache sample app

You can download the LocalSubstitutionCache.zip (66kb) sample project

Here’s a small screenshot of today’s http://www.apple.com running in a UIWebView

After invoking +[NSURLCache setSharedURLCache:] with our NSURLCache subclass, the gray links bar across the top are replaced with a blue graphic stored in the app’s bundle

Conclusion

The purpose of this work is to allow UIWebViews to feel more responsive and a bit more like native user-interfaces.

In reality, a UIWebView will never feel as responsive or integrated as a native user-interface but sometimes making one screen of your app a remote webpage is a big enough saving in developer resources that you’re prepared to make the sacrifice in user quality. Making sure as many resources as possible are stored locally will help make any negative impact on user quality as minor as possible.









posted on 2012-03-07 16:00  pengyingh  阅读(9424)  评论(1编辑  收藏  举报

导航