由于需要用到UIWebView本地缓存功能.在网上找了一些demo
cocoaChina上一篇 是试用ASIHttpRequest.这个是比较好的.
http://www.cocoachina.com/bbs/read.php?tid=69287
还有一篇就是上一篇中提到的一个网站内容,试用NSURLCache
http://re-reference.iteye.com/blog/1391408
使用ASIHttpRequest本地缓存的代码:
3.ASIHTTPRequest,ASIDownloadCache
-(void)loadURL:(NSURL*)url
{
}
- (void)webPageFetchFailed:(ASIHTTPRequest *)theRequest
{
}
- (void)webPageFetchSucceeded:(ASIHTTPRequest *)theRequest
{
}
使用USURLCache缓存本地文件
智能手机的流行让移动运营商们大赚了一笔,然而消费者们却不得不面对可怕的数据流量账单。因为在线看部电影可能要上千块通讯费,比起电影院什么的简直太坑爹了。
所以为了减少流量开销,离线浏览也就成了很关键的功能,而UIWebView这个让人又爱又恨的玩意弱爆了,居然只在Mac OS X上提供webView:resource:willSendRequest:redirectResponse:fromDataSource:这个方法,于是只好自己动手实现了。
原理就是SDK里绝大部分的网络请求都会访问[NSURLCache sharedURLCache]这个对象,它的cachedResponseForRequest
要注意的是NSCachedURLResponse对象不能被提前释放,除非UIWebView去调用NSURLCache的removeCachedResponseForR
顺便说下NSURLRequest对象,它有个cachePolicy属性,只要其值为NSURLRequestReloadIgnori
实际上NSURLCache本身就有磁盘缓存功能,然而在iOS上,NSCachedURLResponse却被限制为不能缓存到磁盘(NSURLCacheStorageAllowed
不过既然知道了原理,那么只要自己实现一个NSURLCache的子类,然后改写cachedResponseForRequest
于是就开工吧。这次的demo逻辑比较复杂,因此我就按步骤来说明了。
先定义视图和控制器。
它的逻辑是打开应用时就尝试访问缓存文件,如果发现存在,则显示缓存完毕;否则就尝试下载整个网页的资源;在下载完成后,也显示缓存完毕。
不过下载所有资源需要解析HTML,甚至是JavaScript和CSS。为了简化我就直接用一个不显示的UIWebView载入这个页面,让它自动去发起所有请求。
当然,缓存完了还需要触发事件来显示网页。于是再提供一个按钮,点击时显示缓存的网页,再次点击就关闭。
顺带一提,我本来想用Google为例的,可惜它自己实现了HTML 5离线浏览,也就体现不出这种方法的意义了,于是只好拿百度来垫背。
Objective-c代码
- #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:UIControlEventTouchUpIns ide]; -
[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]) { // not cached -
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保存到磁盘。不过大多数应用应该使用数据库来保存,这里我只是为了简化而已。
Objective-c代码
- #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)removeCachedResponseForR equest:(NSURLRequest *)request { -
NSLog(@"removeCachedResponseForR equest:%@", request.URL.absoluteString); -
[cachedResponses removeObjectForKey:request.URL.absoluteString]; -
[super removeCachedResponseForR equest:request]; - }
-
- -
(void)removeAllCachedResponses { -
NSLog(@"removeAllObjects"); -
[cachedResponses removeAllObjects]; -
[super removeAllCachedResponses ]; - }
-
- -
(void)dealloc { -
[cachedResponses release]; -
[responsesInfo release]; - }
-
- @end
写完这些没技术含量的代码后,就来实现saveInfo方法吧。
这里有一个要点需要说下,iTunes会备份所有的应用资料,除非放在Library/Caches或tmp文件夹下。由于缓存并不是什么很重要的用户资料,没必要增加用户的备份时间和空间,所以我们应该把缓存放到这2个文件夹里。而后者会在退出应用或重启系统时清空,这显然不是我们想要的效果,于是最佳选择是前者。
Objective-c代码
- static
NSString *cacheDirectory; -
- +
(void)initialize { -
NSArray *paths = NSSearchPathForDirectori esInDomains(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:方法,更保险的是使用stringByAppendingPathCom
在实现saveInfo后,初始化方法就也可以实现了。它主要就是载入保存的plist文件,如果不存在则新建一个空的NSMutableDictionary对象。
Objective-c代码
- -
(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协议等本地请求都会跑到这个方法里来…
Objective-c代码
- static
NSSet *supportSchemes; -
- +
(void)initialize { -
NSArray *paths = NSSearchPathForDirectori esInDomains(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里了,这样的话直接拿出来即可:
Objective-c代码
- NSString
*absoluteString = url.absoluteString; - NSLog(@"%@",
absoluteString); - NSCachedURLResponse
*cachedResponse = [cachedResponses objectForKey:absoluteString]; - if
(cachedResponse) { -
NSLog(@"cached: %@", absoluteString); -
return cachedResponse; - }
再查查responsesInfo里有没有,如果有的话,说明可以从磁盘获取:
Objective-c代码
- 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对象:
Objective-c代码
- NSMutableURLRequest
*newRequest = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnori ngLocalCacheData timeoutInterval:request.timeoutInterval]; - newRequest.allHTTPHeaderFields
= request.allHTTPHeaderFields; - newRequest.HTTPShouldHandleCookies
= request.HTTPShouldHandleCookies;
实际上NSMutableURLRequest还有一些其他的属性,不过并不太重要,所以我就只复制了这2个。
然后就可以用它来发起请求了。由于UIWebView就是在子线程调用cachedResponseForRequest
Objective-c代码
- 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算法。
Objective-c代码
- uint8_t
digest[CC_SHA1_DIGEST_LENGTH]; -
CC_SHA1(data.bytes, data.length, digest); -
NSMutableString* output = [NSMutableString stringWithCapacity:CC_SHA1_DIGEST_LENGTH * 2]; -
for(int i = 0; i < CC_SHA1_DIGEST_LENGTH; i++) -
[output appendFormat:@"x", digest[i]]; -
- NSString
*filename = output;//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属性了:
Objective-c代码
- NSURLResponse
*newResponse = [[NSURLResponse alloc] initWithURL:response.URL MIMEType:response.MIMEType expectedContentLength:data.length textEncodingName:nil]; - responseInfo
= [NSDictionary dictionaryWithObjectsAnd Keys: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,也能发现这确实是从我们的缓存中取出来的。
还不放心的话可以退出程序,这样内存缓存肯定就释放了。然后再次进入并打开网页,你会发现一切仍然正常~
摘自:http://www.keakon.net/2011/08/14/为UIWebView实现离线浏览