使用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.










免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM