轉自
http://www.keakon.net/2011/08/14/%E4%B8%BAUIWebView%E5%AE%9E%E7%8E%B0%E7%A6%BB%E7%BA%BF%E6%B5%8F%E8%A7%88
智能手機的流行讓移動運營商們大賺了一筆,然而消費者們卻不得不面對可怕的數據流量賬單。因為在線看部電影可能要上千塊通訊費,比起電影院什么的簡直太坑爹了。
所 以為了減少流量開銷,離線瀏覽也就成了很關鍵的功能,而UIWebView這個讓人又愛又恨的玩意弱爆了,居然只在Mac OS X上提供webView:resource:willSendRequest:redirectResponse:fromDataSource:這個方 法,于是只好自己動手實現了。
原理就是SDK里絕大部分的網絡請求都會訪問[NSURLCache sharedURLCache]這個對象,它的cachedResponseForRequest:方法會返回一個 NSCachedURLResponse對象。如果這個NSCachedURLResponse對象不為nil,且沒有過期,那么就使用這個緩存的響應, 否則就發起一個不訪問緩存的請求。
要注意的是NSCachedURLResponse對象不能被提前釋放,除非UIWebView去調用 NSURLCache的removeCachedResponseForRequest:方法,原因貌似是UIWebView并不retain這個響應。 而這個問題又很頭疼,因為UIWebView有內存泄露的嫌疑,即使它被釋放了,也很可能不去調用上述方法,于是內存就一直占用著了。
順便說下NSURLRequest對象,它有個cachePolicy屬性,只要其值為NSURLRequestReloadIgnoringLocalCacheData的話,就不會訪問緩存。可喜的是這種情況貌似只有在緩存里沒取到,或是強制刷新時才可能出現。
實 際上NSURLCache本身就有磁盤緩存功能,然而在iOS上,NSCachedURLResponse卻被限制為不能緩存到磁盤 (NSURLCacheStorageAllowed被視為NSURLCacheStorageAllowedInMemoryOnly)。
不過既然知道了原理,那么只要自己實現一個NSURLCache的子類,然后改寫cachedResponseForRequest:方法,讓它從硬盤讀取緩存即可。
于是就開工吧。這次的demo邏輯比較復雜,因此我就按步驟來說明了。
先定義視圖和控制器。
它的邏輯是打開應用時就嘗試訪問緩存文件,如果發現存在,則顯示緩存完畢;否則就嘗試下載整個網頁的資源;在下載完成后,也顯示緩存完畢。
不過下載所有資源需要解析HTML,甚至是JavaScript和CSS。為了簡化我就直接用一個不顯示的UIWebView載入這個頁面,讓它自動去發起所有請求。
當然,緩存完了還需要觸發事件來顯示網頁。于是再提供一個按鈕,點擊時顯示緩存的網頁,再次點擊就關閉。
順帶一提,我本來想用Google為例的,可惜它自己實現了HTML 5離線瀏覽,也就體現不出這種方法的意義了,于是只好拿百度來墊背。
#import <UIKit/UIKit.h>@
interface WebViewController : UIViewController <UIWebViewDelegate> {UIWebView *web;
UILabel *label;
}@
property (
nonatomic,
retain)
UIWebView *web;
@
property (
nonatomic,
retain)
UILabel *label;- (
IBAction)click;@
end#import "WebViewController.h"
#import "URLCache.h"@
implementation WebViewController@
synthesize web, label;- (
IBAction)click {
if (web) {[web removeFromSuperview];
self.web =
nil;}
else {
CGRect frame = {{
0,
0}, {
320,
380}};
UIWebView *webview = [[
UIWebView alloc] initWithFrame:frame];webview.scalesPageToFit =
YES;
self.web = webview;NSURLRequest *request = [NSURLRequest requestWithURL:[
NSURL URLWithString:@
"http://www.baidu.com/"]];[webview loadRequest:request];[
self.view addSubview:webview];[webview
release];}
}- (
void)addButton {
CGRect frame = {{
130,
400}, {
60,
30}};
UIButton *button = [
UIButton buttonWithType:UIButtonTypeRoundedRect];button.frame = frame;[button addTarget:
self action:@selector(click) forControlEvents:UIControlEventTouchUpInside];[button setTitle:@
"我點" forState:UIControlStateNormal]; [
self.view addSubview:button];
}- (
void)viewDidLoad {[
super viewDidLoad];URLCache *sharedCache = [[URLCache alloc] initWithMemoryCapacity:
1024 *
1024 diskCapacity:
0 diskPath:
nil];[NSURLCache setSharedURLCache:sharedCache];
CGRect frame = {{
60,
200}, {
200,
30}};
UILabel *textLabel = [[
UILabel alloc] initWithFrame:frame];textLabel.textAlignment = UITextAlignmentCenter;[
self.view addSubview:textLabel];
self.label = textLabel;
if (![sharedCache.responsesInfo count]) { textLabel.text = @
"緩存中…";
CGRect frame = {{
0,
0}, {
320,
380}};
UIWebView *webview = [[
UIWebView alloc] initWithFrame:frame];webview.delegate =
self;
self.web = webview;NSURLRequest *request = [NSURLRequest requestWithURL:[
NSURL URLWithString:@
"http://www.baidu.com/"]];[webview loadRequest:request];[webview
release];}
else {textLabel.text = @
"已從硬盤讀取緩存";[
self addButton];}[sharedCache
release];
}- (
void)webView:(
UIWebView *)webView didFailLoadWithError:(NSError *)error {
self.web =
nil;label.text = @
"請接通網絡再運行本應用";
}- (
void)webViewDidFinishLoad:(
UIWebView *)webView {
self.web =
nil;label.text = @
"緩存完畢";[
self addButton];URLCache *sharedCache = (URLCache *)[NSURLCache sharedURLCache];[sharedCache saveInfo];
}- (
void)didReceiveMemoryWarning {[
super didReceiveMemoryWarning];
if (!web) {URLCache *sharedCache = (URLCache *)[NSURLCache sharedURLCache];[sharedCache removeAllCachedResponses];}
}- (
void)viewDidUnload {
self.web =
nil;
self.label =
nil;
}- (
void)dealloc {[
super dealloc];[web
release];[label
release];
}@
end 大部分的代碼沒什么要說的,隨便挑2點。
實現了UIWebViewDelegate,因為需要知道緩存完畢或下載失敗這個事件。
另外,正如前面所說的,UIWebView可能不會通知釋放緩存。所以在收到內存警告時,如果UIWebView對象已被釋放,那么就可以安全地清空緩存了(或許還要考慮多線程的影響)。
接下來就是重點了:實現URLCache類。
它需要2個屬性:一個是用于保存NSCachedURLResponse的cachedResponses,另一個是用于保存響應信息的responsesInfo(包括MIME類型和文件名)。
另外還需要實現一個saveInfo方法,用于將responsesInfo保存到磁盤。不過大多數應用應該使用數據庫來保存,這里我只是為了簡化而已。
#import <Foundation/Foundation.h>@
interface URLCache : NSURLCache {NSMutableDictionary *cachedResponses;
NSMutableDictionary *responsesInfo;
}@
property (
nonatomic,
retain)
NSMutableDictionary *cachedResponses;
@
property (
nonatomic,
retain)
NSMutableDictionary *responsesInfo;- (
void)saveInfo;@
end#import "URLCache.h"@
implementation URLCache@
synthesize cachedResponses, responsesInfo;- (
void)removeCachedResponseForRequest:(NSURLRequest *)request {
NSLog(@
"removeCachedResponseForRequest:%@", request.URL.absoluteString);[cachedResponses removeObjectForKey:request.URL.absoluteString];[
super removeCachedResponseForRequest:request];
}- (
void)removeAllCachedResponses {
NSLog(@
"removeAllObjects");[cachedResponses removeAllObjects];[
super removeAllCachedResponses];
}- (
void)dealloc {[cachedResponses
release];[responsesInfo
release];
}@
end
寫完這些沒技術含量的代碼后,就來實現saveInfo方法吧。
這 里有一個要點需要說下,iTunes會備份所有的應用資料,除非放在Library/Caches或tmp文件夾下。由于緩存并不是什么很重要的用戶資 料,沒必要增加用戶的備份時間和空間,所以我們應該把緩存放到這2個文件夾里。而后者會在退出應用或重啟系統時清空,這顯然不是我們想要的效果,于是最佳 選擇是前者。
static NSString *cacheDirectory;+ (
void)initialize {NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask,
YES);cacheDirectory = [[paths objectAtIndex:
0]
retain];
}- (
void)saveInfo {
if ([responsesInfo count]) {
NSString *path = [cacheDirectory stringByAppendingString:@
"responsesInfo.plist"];[responsesInfo writeToFile:path atomically:
YES];}
}
這里我用了stringByAppendingString:方法,更保險的是使用stringByAppendingPathComponent:。不過我估計后者會做更多的檢查工作,所以采用了前者。
在實現saveInfo后,初始化方法就也可以實現了。它主要就是載入保存的plist文件,如果不存在則新建一個空的NSMutableDictionary對象。
- (
id)initWithMemoryCapacity:(NSUInteger)memoryCapacity diskCapacity:(NSUInteger)diskCapacity diskPath:(
NSString *)path {
if (
self = [
super initWithMemoryCapacity:memoryCapacity diskCapacity:diskCapacity diskPath:path]) {cachedResponses = [[
NSMutableDictionary alloc] init];
NSString *path = [cacheDirectory stringByAppendingString:@
"responsesInfo.plist"];NSFileManager *fileManager = [[NSFileManager alloc] init];
if ([fileManager fileExistsAtPath:path]) {responsesInfo = [[
NSMutableDictionary alloc] initWithContentsOfFile:path];}
else {responsesInfo = [[
NSMutableDictionary alloc] init];}[fileManager
release];}
return self;
}
接下來就可以實現cachedResponseForRequest:方法了。
我們得先判斷是不是GET方法,因為其他方法不應該被緩存。還得判斷是不是網絡請求,例如http、https和ftp,因為連data協議等本地請求都會跑到這個方法里來…
static NSSet *supportSchemes;+ (
void)initialize {NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask,
YES);cacheDirectory = [[paths objectAtIndex:
0]
retain];supportSchemes = [[NSSet setWithObjects:@
"http", @
"https", @
"ftp",
nil]
retain];
}- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request {
if ([request.HTTPMethod compare:@
"GET"] != NSOrderedSame) {
return [
super cachedResponseForRequest:request];}
NSURL *url = request.URL;
if (![supportSchemes containsObject:url.scheme]) {
return [
super cachedResponseForRequest:request];}
}
因為沒必要處理它們,所以直接交給父類的處理方法了,它會自行決定是否返回nil的。
接著判斷是不是已經在cachedResponses里了,這樣的話直接拿出來即可:
NSString *absoluteString = url.absoluteString;
NSLog(@
"%@", absoluteString);
NSCachedURLResponse *cachedResponse = [cachedResponses objectForKey:absoluteString];
if (cachedResponse) {
NSLog(@
"cached: %@", absoluteString);
return cachedResponse;
}
再查查responsesInfo里有沒有,如果有的話,說明可以從磁盤獲取:
NSDictionary *responseInfo = [responsesInfo objectForKey:absoluteString];
if (responseInfo) {
NSString *path = [cacheDirectory stringByAppendingString:[responseInfo objectForKey:@
"filename"]];NSFileManager *fileManager = [[NSFileManager alloc] init];
if ([fileManager fileExistsAtPath:path]) {[fileManager
release];NSData *data = [NSData dataWithContentsOfFile:path];NSURLResponse *response = [[NSURLResponse alloc] initWithURL:request.URL MIMEType:[responseInfo objectForKey:@
"MIMEType"] expectedContentLength:data.length textEncodingName:
nil];cachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:data];[response
release];[cachedResponses setObject:cachedResponse forKey:absoluteString];[cachedResponse
release];
NSLog(@
"cached: %@", absoluteString);
return cachedResponse;}[fileManager
release];
}
這里的難點在于構造NSURLResponse和NSCachedURLResponse,不過對照下文檔看看也就清楚了。如前文所說,我們還得把cachedResponse保存到cachedResponses里,避免它被提前釋放。
接下來就說明緩存不存在了,需要我們自己發起一個請求。可恨的是NSURLResponse不能更改屬性,所以還需要手動新建一個NSMutableURLRequest對象:
NSMutableURLRequest *newRequest = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:request.timeoutInterval];
newRequest.allHTTPHeaderFields = request.allHTTPHeaderFields;
newRequest.HTTPShouldHandleCookies = request.HTTPShouldHandleCookies;
實際上NSMutableURLRequest還有一些其他的屬性,不過并不太重要,所以我就只復制了這2個。
然后就可以用它來發起請求了。由于UIWebView就是在子線程調用cachedResponseForRequest:的,不用擔心阻塞的問題,所以無需使用異步請求:
NSError *error =
nil;
NSURLResponse *response =
nil;
NSData *data = [NSURLConnection sendSynchronousRequest:newRequest returningResponse:&response error:&error];
if (error) {
NSLog(@
"%@", error);
NSLog(@
"not cached: %@", absoluteString);
return nil;
}
如果下載沒出錯的話,我們就能拿到data和response了,于是就能將其保存到磁盤了。保存的文件名必須是合法且獨一無二的,所以我就用到了sha1算法。
NSString *filename = sha1([absoluteString UTF8String]);
NSString *path = [cacheDirectory stringByAppendingString:filename];
NSFileManager *fileManager = [[NSFileManager alloc] init];
[fileManager createFileAtPath:path contents:data attributes:
nil];
[fileManager
release];
接下來還得將文件信息保存到responsesInfo,并構造一個NSCachedURLResponse。
然而這里還有個陷阱,因為直接使用response對象會無效。我稍微研究了一下,發現它其實是個NSHTTPURLResponse對象,可能是它的allHeaderFields屬性影響了緩存策略,導致不能重用。
不過這難不倒我們,直接像前面那樣構造一個NSURLResponse對象就行了,這樣就沒有allHeaderFields屬性了:
NSURLResponse *newResponse = [[NSURLResponse alloc] initWithURL:response.URL MIMEType:response.MIMEType expectedContentLength:data.length textEncodingName:
nil];
responseInfo = [
NSDictionary dictionaryWithObjectsAndKeys:filename, @
"filename", newResponse.MIMEType, @
"MIMEType",
nil];
[responsesInfo setObject:responseInfo forKey:absoluteString];
NSLog(@
"saved: %@", absoluteString);cachedResponse = [[NSCachedURLResponse alloc] initWithResponse:newResponse data:data];
[newResponse
release];
[cachedResponses setObject:cachedResponse forKey:absoluteString];
[cachedResponse
release];
return cachedResponse;
OK,現在終于大功告成了,打開WIFI然后啟動這個程序,過一會就會提示緩存完畢了。然后關掉WIFI,嘗試打開網頁,你會發現網頁能正常載入了。
而查看log,也能發現這確實是從我們的緩存中取出來的。
還不放心的話可以退出程序,這樣內存緩存肯定就釋放了。然后再次進入并打開網頁,你會發現一切仍然正常~ ? ?
轉載于:https://www.cnblogs.com/jiangshiyong/archive/2012/12/26/2834122.html
總結
以上是生活随笔為你收集整理的UIWebView实现离线浏览的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。