UIWebView 本地缓存

由于需要用到UIWebView本地缓存功能.在网上找了一些demo

cocoaChina上一篇 是试用ASIHttpRequest.这个是比较好的.

http://www.cocoachina.com/bbs/read.php?tid=69287

 

还有一篇就是上一篇中提到的一个网站内容,试用NSURLCache

http://re-reference.iteye.com/blog/1391408

 

使用ASIHttpRequest本地缓存的代码:

3.ASIHTTPRequestASIDownloadCache   ASIWebPageRequest

   首先我得说,这确实是个很好的框架,使用起来确实很方便,但是对于缓存这个问题,好像也跟第二点提到的效果差不多,加载速度没有明显的提升,离线模式下也无法加载。这是实现的代码:

 

-(void)loadURL:(NSURL*)url

{

    ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];

    //ASIWebPageRequest *request= [ASIWebPageRequest requestWithURL:url];

    [request setDelegate:self];

    //[request setUrlReplacementMode:ASIReplaceExternalResourcesWithData];

    [request setDidFailSelector:@selector(webPageFetchFailed:)];

    [request setDidFinishSelector:@selector(webPageFetchSucceeded:)];

     //设置缓存

    [request setDownloadCache:[ASIDownloadCache sharedCache]];

    //[request setCacheStoragePolicy:ASICachePermanentlyCacheStoragePolicy];

    [request setCachePolicy:ASIAskServerIfModifiedWhenStaleCachePolicy|ASIFallbackToCacheIfLoadFailsCachePolicy];

    [request setDownloadDestinationPath:[[ASIDownloadCache sharedCache]pathToStoreCachedResponseDataForRequest:request]];

     [request startAsynchronous];

}

 

 

- (void)webPageFetchFailed:(ASIHTTPRequest *)theRequest

{

    // Obviously you should handle the error properly...

    NSLog(@"%@",[theRequest error]);

    NSString *path = [[NSBundle mainBundle] pathForResource:@"error1.html" ofType:nil inDirectory:@"WebResources/Error"];

    NSURL  *url=[NSURL fileURLWithPath:path];  

    [viewer loadRequest:[NSURLRequest requestWithURL:url]];

}

 

- (void)webPageFetchSucceeded:(ASIHTTPRequest *)theRequest

{

    NSString *response = [NSString stringWithContentsOfFile:

                          [theRequest downloadDestinationPath] encoding:[theRequest responseEncoding] error:nil];

    // Note we're setting the baseURL to the url of the page we downloaded. This is important!

    [viewer loadHTMLString:response baseURL:[theRequest url]];

    //[viewer loadHTMLString:response baseURL:nil];

}

 

 

使用USURLCache缓存本地文件

智能手机的流行让移动运营商们大赚了一笔,然而消费者们却不得不面对可怕的数据流量账单。因为在线看部电影可能要上千块通讯费,比起电影院什么的简直太坑爹了。 

所以为了减少流量开销,离线浏览也就成了很关键的功能,而UIWebView这个让人又爱又恨的玩意弱爆了,居然只在Mac OS X上提供webView:resource:willSendRequest:redirectResponse:fromDataSource:这个方法,于是只好自己动手实现了。 

 

原理就是SDK里绝大部分的网络请求都会访问[NSURLCache sharedURLCache]这个对象,它的cachedResponseForRequest:方法会返回一个NSCachedURLResponse对象。如果这个NSCachedURLResponse对象不为nil,且没有过期,那么就使用这个缓存的响应,否则就发起一个不访问缓存的请求。 

要注意的是NSCachedURLResponse对象不能被提前释放,除非UIWebView去调用NSURLCacheremoveCachedResponseForRequest:方法,原因貌似是UIWebView并不retain这个响应。而这个问题又很头疼,因为UIWebView有内存泄露的嫌疑,即使它被释放了,也很可能不去调用上述方法,于是内存就一直占用着了。 

 

顺便说下NSURLRequest对象,它有个cachePolicy属性,只要其值为NSURLRequestReloadIgnoringLocalCacheData的话,就不会访问缓存。可喜的是这种情况貌似只有在缓存里没取到,或是强制刷新时才可能出现。 

实际上NSURLCache本身就有磁盘缓存功能,然而在iOS上,NSCachedURLResponse却被限制为不能缓存到磁盘(NSURLCacheStorageAllowed被视为NSURLCacheStorageAllowedInMemoryOnly)。 

不过既然知道了原理,那么只要自己实现一个NSURLCache的子类,然后改写cachedResponseForRequest:方法,让它从硬盘读取缓存即可。 

 

于是就开工吧。这次的demo逻辑比较复杂,因此我就按步骤来说明了。 

 

先定义视图和控制器。 

它的逻辑是打开应用时就尝试访问缓存文件,如果发现存在,则显示缓存完毕;否则就尝试下载整个网页的资源;在下载完成后,也显示缓存完毕。 

不过下载所有资源需要解析HTML,甚至是JavaScriptCSS。为了简化我就直接用一个不显示的UIWebView载入这个页面,让它自动去发起所有请求。 

当然,缓存完了还需要触发事件来显示网页。于是再提供一个按钮,点击时显示缓存的网页,再次点击就关闭。 

顺带一提,我本来想用Google为例的,可惜它自己实现了HTML 5离线浏览,也就体现不出这种方法的意义了,于是只好拿百度来垫背。 

Objective-c代码  

  1. #import <UIKit/UIKit.h>  
  2.   
  3. @interface WebViewController : UIViewController <UIWebViewDelegate> {  
  4.     UIWebView *web;  
  5.     UILabel *label;  
  6. }  
  7.   
  8. @property (nonatomic, retain) UIWebView *web;  
  9. @property (nonatomic, retain) UILabel *label;  
  10.   
  11. - (IBAction)click;  
  12.   
  13. @end  
  14.   
  15.   
  16. #import "WebViewController.h"  
  17. #import "URLCache.h"  
  18.   
  19. @implementation WebViewController  
  20.   
  21. @synthesize web, label;  
  22.   
  23. - (IBAction)click {  
  24.     if (web) {  
  25.         [web removeFromSuperview];  
  26.         self.web = nil;  
  27.     } else {  
  28.         CGRect frame = {{0, 0}, {320, 380}};  
  29.         UIWebView *webview = [[UIWebView alloc] initWithFrame:frame];  
  30.         webview.scalesPageToFit = YES;  
  31.         self.web = webview;  
  32.           
  33.         NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com/"]];  
  34.         [webview loadRequest:request];  
  35.         [self.view addSubview:webview];  
  36.         [webview release];  
  37.     }  
  38. }  
  39.   
  40. - (void)addButton {  
  41.     CGRect frame = {{130, 400}, {60, 30}};  
  42.     UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];  
  43.     button.frame = frame;  
  44.     [button addTarget:self action:@selector(click) forControlEvents:UIControlEventTouchUpInside];  
  45.     [button setTitle:@"我点" forState:UIControlStateNormal];    
  46.     [self.view addSubview:button];  
  47. }  
  48.   
  49. - (void)viewDidLoad {  
  50.     [super viewDidLoad];  
  51.   
  52.     URLCache *sharedCache = [[URLCache alloc] initWithMemoryCapacity:1024 * 1024 diskCapacity:0 diskPath:nil];  
  53.     [NSURLCache setSharedURLCache:sharedCache];  
  54.       
  55.     CGRect frame = {{60, 200}, {200, 30}};  
  56.     UILabel *textLabel = [[UILabel alloc] initWithFrame:frame];  
  57.     textLabel.textAlignment = UITextAlignmentCenter;  
  58.     [self.view addSubview:textLabel];  
  59.     self.label = textLabel;  
  60.       
  61.     if (![sharedCache.responsesInfo count]) { // not cached  
  62.         textLabel.text = @"缓存中…";  
  63.           
  64.         CGRect frame = {{0, 0}, {320, 380}};  
  65.         UIWebView *webview = [[UIWebView alloc] initWithFrame:frame];  
  66.         webview.delegate = self;  
  67.         self.web = webview;  
  68.           
  69.         NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com/"]];  
  70.         [webview loadRequest:request];  
  71.         [webview release];  
  72.     } else {  
  73.         textLabel.text = @"已从硬盘读取缓存";  
  74.         [self addButton];  
  75.     }  
  76.       
  77.     [sharedCache release];  
  78. }  
  79.   
  80. - (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {  
  81.     self.web = nil;  
  82.     label.text = @"请接通网络再运行本应用";  
  83. }  
  84.   
  85. - (void)webViewDidFinishLoad:(UIWebView *)webView {  
  86.     self.web = nil;  
  87.     label.text = @"缓存完毕";  
  88.     [self addButton];  
  89.       
  90.     URLCache *sharedCache = (URLCache *)[NSURLCache sharedURLCache];  
  91.     [sharedCache saveInfo];  
  92. }  
  93.   
  94. - (void)didReceiveMemoryWarning {  
  95.     [super didReceiveMemoryWarning];  
  96.       
  97.     if (!web) {  
  98.         URLCache *sharedCache = (URLCache *)[NSURLCache sharedURLCache];  
  99.         [sharedCache removeAllCachedResponses];  
  100.     }  
  101. }  
  102.   
  103. - (void)viewDidUnload {  
  104.     self.web = nil;  
  105.     self.label = nil;  
  106. }  
  107.   
  108.   
  109. - (void)dealloc {  
  110.     [super dealloc];  
  111.     [web release];  
  112.     [label release];  
  113. }  
  114.   
  115. @end  

 

 

大部分的代码没什么要说的,随便挑2点。 

实现了UIWebViewDelegate,因为需要知道缓存完毕或下载失败这个事件。 

另外,正如前面所说的,UIWebView可能不会通知释放缓存。所以在收到内存警告时,如果UIWebView对象已被释放,那么就可以安全地清空缓存了(或许还要考虑多线程的影响)。 

 

接下来就是重点了:实现URLCache类。 

它需要2个属性:一个是用于保存NSCachedURLResponsecachedResponses,另一个是用于保存响应信息的responsesInfo(包括MIME类型和文件名)。 

另外还需要实现一个saveInfo方法,用于将responsesInfo保存到磁盘。不过大多数应用应该使用数据库来保存,这里我只是为了简化而已。 

Objective-c代码  

  1. #import <Foundation/Foundation.h>  
  2.   
  3. @interface URLCache : NSURLCache {  
  4.     NSMutableDictionary *cachedResponses;  
  5.     NSMutableDictionary *responsesInfo;  
  6. }  
  7.   
  8. @property (nonatomic, retain) NSMutableDictionary *cachedResponses;  
  9. @property (nonatomic, retain) NSMutableDictionary *responsesInfo;  
  10.   
  11. - (void)saveInfo;  
  12.   
  13. @end  
  14.   
  15.   
  16. #import "URLCache.h"  
  17. @implementation URLCache  
  18. @synthesize cachedResponses, responsesInfo;  
  19.   
  20. - (void)removeCachedResponseForRequest:(NSURLRequest *)request {  
  21.     NSLog(@"removeCachedResponseForRequest:%@", request.URL.absoluteString);  
  22.     [cachedResponses removeObjectForKey:request.URL.absoluteString];  
  23.     [super removeCachedResponseForRequest:request];  
  24. }  
  25.   
  26. - (void)removeAllCachedResponses {  
  27.     NSLog(@"removeAllObjects");  
  28.     [cachedResponses removeAllObjects];  
  29.     [super removeAllCachedResponses];  
  30. }  
  31.   
  32. - (void)dealloc {  
  33.     [cachedResponses release];  
  34.     [responsesInfo release];  
  35. }  
  36.   
  37. @end  

 

 

写完这些没技术含量的代码后,就来实现saveInfo方法吧。 

这里有一个要点需要说下,iTunes会备份所有的应用资料,除非放在Library/Cachestmp文件夹下。由于缓存并不是什么很重要的用户资料,没必要增加用户的备份时间和空间,所以我们应该把缓存放到这2个文件夹里。而后者会在退出应用或重启系统时清空,这显然不是我们想要的效果,于是最佳选择是前者。 

Objective-c代码  

  1. static NSString *cacheDirectory;  
  2.   
  3. + (void)initialize {  
  4.     NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);  
  5.     cacheDirectory = [[paths objectAtIndex:0] retain];  
  6. }  
  7.   
  8. - (void)saveInfo {  
  9.     if ([responsesInfo count]) {  
  10.         NSString *path = [cacheDirectory stringByAppendingString:@"responsesInfo.plist"];  
  11.         [responsesInfo writeToFile:path atomically: YES];  
  12.     }     
  13. }  

 

这里我用了stringByAppendingString:方法,更保险的是使用stringByAppendingPathComponent:。不过我估计后者会做更多的检查工作,所以采用了前者。 

 

在实现saveInfo后,初始化方法就也可以实现了。它主要就是载入保存的plist文件,如果不存在则新建一个空的NSMutableDictionary对象。 

Objective-c代码  

  1. - (id)initWithMemoryCapacity:(NSUInteger)memoryCapacity diskCapacity:(NSUInteger)diskCapacity diskPath:(NSString *)path {  
  2.     if (self = [super initWithMemoryCapacity:memoryCapacity diskCapacity:diskCapacity diskPath:path]) {  
  3.         cachedResponses = [[NSMutableDictionary alloc] init];  
  4.         NSString *path = [cacheDirectory stringByAppendingString:@"responsesInfo.plist"];  
  5.         NSFileManager *fileManager = [[NSFileManager alloc] init];  
  6.         if ([fileManager fileExistsAtPath:path]) {  
  7.             responsesInfo = [[NSMutableDictionary alloc] initWithContentsOfFile:path];  
  8.         } else {  
  9.             responsesInfo = [[NSMutableDictionary alloc] init];  
  10.         }  
  11.         [fileManager release];  
  12.     }  
  13.     return self;  
  14. }  

 

接下来就可以实现cachedResponseForRequest:方法了。 

我们得先判断是不是GET方法,因为其他方法不应该被缓存。还得判断是不是网络请求,例如httphttpsftp,因为连data协议等本地请求都会跑到这个方法里来… 

Objective-c代码  

  1. static NSSet *supportSchemes;  
  2.   
  3. + (void)initialize {  
  4.     NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);  
  5.     cacheDirectory = [[paths objectAtIndex:0] retain];  
  6.     supportSchemes = [[NSSet setWithObjects:@"http", @"https", @"ftp", nil] retain];  
  7. }  
  8.   
  9. - (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request {  
  10.     if ([request.HTTPMethod compare:@"GET"] != NSOrderedSame) {  
  11.         return [super cachedResponseForRequest:request];  
  12.     }  
  13.   
  14.     NSURL *url = request.URL;  
  15.     if (![supportSchemes containsObject:url.scheme]) {  
  16.         return [super cachedResponseForRequest:request];  
  17.     }  
  18.     //...  
  19. }  

 

因为没必要处理它们,所以直接交给父类的处理方法了,它会自行决定是否返回nil的。 

 

接着判断是不是已经在cachedResponses里了,这样的话直接拿出来即可: 

Objective-c代码  

  1. NSString *absoluteString = url.absoluteString;  
  2. NSLog(@"%@", absoluteString);  
  3. NSCachedURLResponse *cachedResponse = [cachedResponses objectForKey:absoluteString];  
  4. if (cachedResponse) {  
  5.     NSLog(@"cached: %@", absoluteString);  
  6.     return cachedResponse;  
  7. }  

 

再查查responsesInfo里有没有,如果有的话,说明可以从磁盘获取: 

Objective-c代码  

  1. NSDictionary *responseInfo = [responsesInfo objectForKey:absoluteString];  
  2. if (responseInfo) {  
  3.     NSString *path = [cacheDirectory stringByAppendingString:[responseInfo objectForKey:@"filename"]];  
  4.     NSFileManager *fileManager = [[NSFileManager alloc] init];  
  5.     if ([fileManager fileExistsAtPath:path]) {  
  6.         [fileManager release];  
  7.           
  8.         NSData *data = [NSData dataWithContentsOfFile:path];  
  9.         NSURLResponse *response = [[NSURLResponse alloc] initWithURL:request.URL MIMEType:[responseInfo objectForKey:@"MIMEType"] expectedContentLength:data.length textEncodingName:nil];  
  10.         cachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:data];  
  11.         [response release];  
  12.           
  13.         [cachedResponses setObject:cachedResponse forKey:absoluteString];  
  14.         [cachedResponse release];  
  15.         NSLog(@"cached: %@", absoluteString);  
  16.         return cachedResponse;  
  17.     }  
  18.     [fileManager release];  
  19. }  

 

这里的难点在于构造NSURLResponseNSCachedURLResponse,不过对照下文档看看也就清楚了。如前文所说,我们还得把cachedResponse保存到cachedResponses里,避免它被提前释放。 

 

接下来就说明缓存不存在了,需要我们自己发起一个请求。可恨的是NSURLResponse不能更改属性,所以还需要手动新建一个NSMutableURLRequest对象: 

Objective-c代码  

  1. NSMutableURLRequest *newRequest = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:request.timeoutInterval];  
  2. newRequest.allHTTPHeaderFields = request.allHTTPHeaderFields;  
  3. newRequest.HTTPShouldHandleCookies = request.HTTPShouldHandleCookies;  

 

实际上NSMutableURLRequest还有一些其他的属性,不过并不太重要,所以我就只复制了这2个。 

 

然后就可以用它来发起请求了。由于UIWebView就是在子线程调用cachedResponseForRequest:的,不用担心阻塞的问题,所以无需使用异步请求: 

Objective-c代码  

  1. NSError *error = nil;  
  2. NSURLResponse *response = nil;  
  3. NSData *data = [NSURLConnection sendSynchronousRequest:newRequest returningResponse:&response error:&error];  
  4. if (error) {  
  5.     NSLog(@"%@", error);  
  6.     NSLog(@"not cached: %@", absoluteString);  
  7.     return nil;  
  8. }  

 

如果下载没出错的话,我们就能拿到dataresponse了,于是就能将其保存到磁盘了。保存的文件名必须是合法且独一无二的,所以我就用到了sha1算法。 

Objective-c代码  

  1. uint8_t digest[CC_SHA1_DIGEST_LENGTH];  
  2.     CC_SHA1(data.bytes, data.length, digest);  
  3.     NSMutableString* output = [NSMutableString stringWithCapacity:CC_SHA1_DIGEST_LENGTH * 2];  
  4.     for(int i = 0; i < CC_SHA1_DIGEST_LENGTH; i++)  
  5.         [output appendFormat:@"x", digest[i]];  
  6.       
  7. NSString *filename = output;//sha1([absoluteString UTF8String]);   
  8. NSString *path = [cacheDirectory stringByAppendingString:filename];  
  9. NSFileManager *fileManager = [[NSFileManager alloc] init];  
  10. [fileManager createFileAtPath:path contents:data attributes:nil];  
  11. [fileManager release];  

 

接下来还得将文件信息保存到responsesInfo,并构造一个NSCachedURLResponse 

然而这里还有个陷阱,因为直接使用response对象会无效。我稍微研究了一下,发现它其实是个NSHTTPURLResponse对象,可能是它的allHeaderFields属性影响了缓存策略,导致不能重用。 

不过这难不倒我们,直接像前面那样构造一个NSURLResponse对象就行了,这样就没有allHeaderFields属性了:

Objective-c代码  

  1. NSURLResponse *newResponse = [[NSURLResponse alloc] initWithURL:response.URL MIMEType:response.MIMEType expectedContentLength:data.length textEncodingName:nil];  
  2. responseInfo = [NSDictionary dictionaryWithObjectsAndKeys:filename, @"filename", newResponse.MIMEType, @"MIMEType", nil];  
  3. [responsesInfo setObject:responseInfo forKey:absoluteString];  
  4. NSLog(@"saved: %@", absoluteString);  
  5.   
  6. cachedResponse = [[NSCachedURLResponse alloc] initWithResponse:newResponse data:data];  
  7. [newResponse release];  
  8. [cachedResponses setObject:cachedResponse forKey:absoluteString];  
  9. [cachedResponse release];  
  10. return cachedResponse;  

 

OK,现在终于大功告成了,打开WIFI然后启动这个程序,过一会就会提示缓存完毕了。然后关掉WIFI,尝试打开网页,你会发现网页能正常载入了。 

而查看log,也能发现这确实是从我们的缓存中取出来的。 

还不放心的话可以退出程序,这样内存缓存肯定就释放了。然后再次进入并打开网页,你会发现一切仍然正常~

posted @ 2013-04-24 10:46  hhhker  阅读(829)  评论(0编辑  收藏  举报