为UIWebView实现离线浏览

智能手机的流行让移动运营商们大赚了一笔,然而消费者们却不得不面对可怕的数据流量账单。因为在线看部电影可能要上千块通讯费,比起电影院什么的简直太坑爹了。 
所以为了减少流量开销,离线浏览也就成了很关键的功能,而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离线浏览,也就体现不出这种方法的意义了,于是只好拿百度来垫背。 
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 = {{00}, {320380}};  
  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 = {{130400}, {6030}};  
  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 = {{60200}, {20030}};  
  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 = {{00}, {320380}};  
  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个属性:一个是用于保存NSCachedURLResponse的cachedResponses,另一个是用于保存响应信息的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/Caches或tmp文件夹下。由于缓存并不是什么很重要的用户资料,没必要增加用户的备份时间和空间,所以我们应该把缓存放到这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方法,因为其他方法不应该被缓存。还得判断是不是网络请求,例如http、https和ftp,因为连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. }  

这里的难点在于构造NSURLResponse和NSCachedURLResponse,不过对照下文档看看也就清楚了。如前文所说,我们还得把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. }  

如果下载没出错的话,我们就能拿到data和response了,于是就能将其保存到磁盘了。保存的文件名必须是合法且独一无二的,所以我就用到了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:@"%02x", 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,也能发现这确实是从我们的缓存中取出来的。 
还不放心的话可以退出程序,这样内存缓存肯定就释放了。然后再次进入并打开网页,你会发现一切仍然正常~ 
WebZip 7.03 绿色汉化特别版   WebZIP 是著名的离线浏览器软件,在它的帮助下你能够完整下载网站的内容,或者你也可以选择自行设置下载的层数、文件类型、网页与媒体文件的定位以及网址过滤器,以便按己所需地获取网站内容。你下载到本地硬盘中的网站内容将仍保持原本的 HTML 格式,其文件名与目录结构都不会变化,这样可以准确地提供网站的镜像。现在使用 WebZIP 中新的 FAR 插件工具,你可以把下载的内容制作成 HTML-帮助文件(.chm)。你也可以把抓取的网站内容压缩为 ZIP 文件。   用 WebZIP 进行离线浏览还可以节省大量的时间,因为从一个链接转到另一个链接的速度要比在线时快得多。此外,WebZIP 最大可以同时下载 16 个网页或图片,并支持断点续传与使用代理服务器,所以你能够在较短的时间内获得大量的信息。总之,WebZIP 是用于发布、参考与离线使用网站素材的优秀工具。 ------------------------------------------------------------------------ Offline Explorer Enterprise 5.8 绿色汉化特别版   相当方便使用的离线浏览工具,可排定抓取时间、设定Proxy,也可选择抓取的项目及大小,可自设下载的存放位置、及存放的空间限制。它内置浏览程序、可直接浏览或是使用自己喜欢的浏览器来浏览、且更可直接以全浏览窗切换来作网上浏览,另它对于抓取的网站更有MAP的提供、可更清楚整个网站的连结及目录结构   注意要一次性下载完 不能不关闭软件就退出关机 这样几百兆文件就白下了 ------------------------------------------------------------------------ TeleportUltra 1.61 绿色汉化特别版 TeleportUltra所能做的,不仅仅是离线浏览某个网页(让你离线快速浏览某个网页的内容当然是它的一项重要功能),它可以从Internet的任何地方抓回你想要的任何文件,它可以在你指定的时间自动登录到你指定的网站下载你指定的内容,你还可以用它来创建某个网站的完整的镜象,作为创建你自己的网站的参考。 如果你也和我一样,曾想把整个网页捉回慢慢欣赏,如果你也曾像我一样费尽千辛万苦,只为了重复捉取同一网站的档案而做一些机械性的动作TeleportUltra简直是我们的救星!它可迅速、确实地将整个网站复制在你的硬碟中,为您节省大笔的连线费用与时间。 TeleportUltra是著名的离线浏览软件TeleportPro版本的增强版! 更新记录: 1.新增了一项功能,使得该软件的Ultra,VLX,Exec,Exec/VLX版可以打开比较小的项目 2.新增了可以在UNC卷上运行项目的功能 3.在Exec和Exec/VLX版本中新增了API命令 4.更新了所有版本的文档 5.改进了脚本,可以处理更多的脚本命令 6.改进了规则引擎 7.修正了会取回一些不需要的URL的bug 8.重新设置了Ultra版的试用期 ------------------------------------------------------------------------ Webdup 0.93 安装汉化特别版   Webdup能够把您想要浏览的信息(如网页和图片等) 预先下载下来,保存在本地硬盘,使您可以从本地进行离线浏览,这样不仅可以大大减少上网时间,降低上网费用,还可以加快浏览速度;并且将来无须上网就可以很方便地查阅这些信息。不仅如此, Webdup更提供了备份历次下载记录和比较完善的管理功能,使您能够方便地分类保存和管理有价值的下载信息。 功能简介 1、支持HTTP和FTP下载,并支持HTTP和Socks5代理服务器; 2、支持多线程下载;    3、支持断点续传;    4、可按URL和文件后缀名设置过滤,只下载所需文件;    5、自动识别下载过的文件是否更新过,减少重复下载;    6、可设置定时下载和定时停止;    7、支持自动拨号和自动挂断;    8、可导出和导入项目文件,方便用户交换网上资源;    9、提供与浏览器(IE)的整合,方便快速地创建项目;    10、支持项目和类别的拖拽操作,方便用户分类管理项目;    11、能自动识别操作系统的语言,按需显示中文或英文。 ------------------------------------------------------------------------ WebCHM 2.07 绿色汉化特别版 WebCHM(原WebSeizer测试版)是中国最强大的多线程离线浏览软件,专业的下载大型网站的工具。主要特色是可以下载超大型的网站(容量>10GB,文件数目>100万的网站)。对小型网站的支持也很有力,是目前唯一能下载java链接的离线浏览器软件,而且支持需要登陆的网站,以前很难下载的论坛、BBS等,大多也可用WebCHM下载。WebCHM另一个重要特色是内置了CHM压缩引擎,可以灵活方便地将下载的网页制作成CHM压缩文档,当下载大型网站时,可以边下载边压缩到CHM文档,既方便浏览,又大大节省了空间,也方便了将来复制删除。WebCHM的下载方式也很灵活,可以制定下载的层次、范围(可选择本目录、域名、连续域名等),可以过滤掉不需要下载的链接,也可以指定下载后的文件是直接保存到硬盘还是压缩到某个CHM包。所以既可以将整个网站拉到硬盘慢慢看,也可以方便地下载某一篇感兴趣的文章。下载了大型的书库后,也可以将其中喜欢的某个目录的文章打包制作成可以在PDA上阅读的CHM格式的电子书,或者可将小型任务的文件全部压制到CHM电子书中。WebCHM还支持断点续传、在线升级、分类管理等功能,操作方便,体贴用户。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值