轉(zhuǎn)自
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
智能手機的流行讓移動運營商們大賺了一筆,然而消費者們卻不得不面對可怕的數(shù)據(jù)流量賬單。因為在線看部電影可能要上千塊通訊費,比起電影院什么的簡直太坑爹了。
所 以為了減少流量開銷,離線瀏覽也就成了很關(guān)鍵的功能,而UIWebView這個讓人又愛又恨的玩意弱爆了,居然只在Mac OS X上提供webView:resource:willSendRequest:redirectResponse:fromDataSource:這個方 法,于是只好自己動手實現(xiàn)了。 原理就是SDK里絕大部分的網(wǎng)絡請求都會訪問[NSURLCache sharedURLCache]這個對象,它的cachedResponseForRequest:方法會返回一個 NSCachedURLResponse對象。如果這個NSCachedURLResponse對象不為nil,且沒有過期,那么就使用這個緩存的響應, 否則就發(fā)起一個不訪問緩存的請求。 要注意的是NSCachedURLResponse對象不能被提前釋放,除非UIWebView去調(diào)用 NSURLCache的removeCachedResponseForRequest:方法,原因貌似是UIWebView并不retain這個響應。 而這個問題又很頭疼,因為UIWebView有內(nèi)存泄露的嫌疑,即使它被釋放了,也很可能不去調(diào)用上述方法,于是內(nèi)存就一直占用著了。 順便說下NSURLRequest對象,它有個cachePolicy屬性,只要其值為NSURLRequestReloadIgnoringLocalCacheData的話,就不會訪問緩存??上驳氖沁@種情況貌似只有在緩存里沒取到,或是強制刷新時才可能出現(xiàn)。 實 際上NSURLCache本身就有磁盤緩存功能,然而在iOS上,NSCachedURLResponse卻被限制為不能緩存到磁盤 (NSURLCacheStorageAllowed被視為NSURLCacheStorageAllowedInMemoryOnly)。 不過既然知道了原理,那么只要自己實現(xiàn)一個NSURLCache的子類,然后改寫cachedResponseForRequest:方法,讓它從硬盤讀取緩存即可。 于是就開工吧。這次的demo邏輯比較復雜,因此我就按步驟來說明了。 先定義視圖和控制器。 它的邏輯是打開應用時就嘗試訪問緩存文件,如果發(fā)現(xiàn)存在,則顯示緩存完畢;否則就嘗試下載整個網(wǎng)頁的資源;在下載完成后,也顯示緩存完畢。 不過下載所有資源需要解析HTML,甚至是JavaScript和CSS。為了簡化我就直接用一個不顯示的UIWebView載入這個頁面,讓它自動去發(fā)起所有請求。 當然,緩存完了還需要觸發(fā)事件來顯示網(wǎng)頁。于是再提供一個按鈕,點擊時顯示緩存的網(wǎng)頁,再次點擊就關(guān)閉。 順帶一提,我本來想用Google為例的,可惜它自己實現(xiàn)了HTML 5離線瀏覽,也就體現(xiàn)不出這種方法的意義了,于是只好拿百度來墊背。
#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 = @
"請接通網(wǎng)絡再運行本應用" ;
}- (
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點。 實現(xiàn)了UIWebViewDelegate,因為需要知道緩存完畢或下載失敗這個事件。 另外,正如前面所說的,UIWebView可能不會通知釋放緩存。所以在收到內(nèi)存警告時,如果UIWebView對象已被釋放,那么就可以安全地清空緩存了(或許還要考慮多線程的影響)。 接下來就是重點了:實現(xiàn)URLCache類。 它需要2個屬性:一個是用于保存NSCachedURLResponse的cachedResponses,另一個是用于保存響應信息的responsesInfo(包括MIME類型和文件名)。 另外還需要實現(xiàn)一個saveInfo方法,用于將responsesInfo保存到磁盤。不過大多數(shù)應用應該使用數(shù)據(jù)庫來保存,這里我只是為了簡化而已。
#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 寫完這些沒技術(shù)含量的代碼后,就來實現(xiàn)saveInfo方法吧。 這 里有一個要點需要說下,iTunes會備份所有的應用資料,除非放在Library/Caches或tmp文件夾下。由于緩存并不是什么很重要的用戶資 料,沒必要增加用戶的備份時間和空間,所以我們應該把緩存放到這2個文件夾里。而后者會在退出應用或重啟系統(tǒng)時清空,這顯然不是我們想要的效果,于是最佳 選擇是前者。
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:。不過我估計后者會做更多的檢查工作,所以采用了前者。 在實現(xiàn)saveInfo后,初始化方法就也可以實現(xiàn)了。它主要就是載入保存的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 ;
}
接下來就可以實現(xiàn)cachedResponseForRequest:方法了。 我們得先判斷是不是GET方法,因為其他方法不應該被緩存。還得判斷是不是網(wǎng)絡請求,例如http、https和ftp,因為連data協(xié)議等本地請求都會跑到這個方法里來…
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的。 接著判斷是不是已經(jīng)在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 ];
}
這里的難點在于構(gòu)造NSURLResponse和NSCachedURLResponse,不過對照下文檔看看也就清楚了。如前文所說,我們還得把cachedResponse保存到cachedResponses里,避免它被提前釋放。 接下來就說明緩存不存在了,需要我們自己發(fā)起一個請求??珊薜氖荖SURLResponse不能更改屬性,所以還需要手動新建一個NSMutableURLRequest對象:
NSMutableURLRequest *newRequest = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:request.timeoutInterval];
newRequest.allHTTPHeaderFields = request.allHTTPHeaderFields;
newRequest.HTTPShouldHandleCookies = request.HTTPShouldHandleCookies;
實際上NSMutableURLRequest還有一些其他的屬性,不過并不太重要,所以我就只復制了這2個。 然后就可以用它來發(fā)起請求了。由于UIWebView就是在子線程調(diào)用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,并構(gòu)造一個NSCachedURLResponse。 然而這里還有個陷阱,因為直接使用response對象會無效。我稍微研究了一下,發(fā)現(xiàn)它其實是個NSHTTPURLResponse對象,可能是它的allHeaderFields屬性影響了緩存策略,導致不能重用。 不過這難不倒我們,直接像前面那樣構(gòu)造一個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,現(xiàn)在終于大功告成了,打開WIFI然后啟動這個程序,過一會就會提示緩存完畢了。然后關(guān)掉WIFI,嘗試打開網(wǎng)頁,你會發(fā)現(xiàn)網(wǎng)頁能正常載入了。 而查看log,也能發(fā)現(xiàn)這確實是從我們的緩存中取出來的。 還不放心的話可以退出程序,這樣內(nèi)存緩存肯定就釋放了。然后再次進入并打開網(wǎng)頁,你會發(fā)現(xiàn)一切仍然正常~ ? ?
轉(zhuǎn)載于:https://www.cnblogs.com/jiangshiyong/archive/2012/12/26/2834122.html
總結(jié)
以上是生活随笔 為你收集整理的UIWebView实现离线浏览 的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔 推薦給好友。