[转] 为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离线浏览,也就体现不出这种方法的意义了,于是只好拿百度来垫背。
#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]) { // 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
#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
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]; }}
- (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;}
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]; } //...}
NSString *absoluteString = url.absoluteString;NSLog(@"%@", absoluteString);NSCachedURLResponse *cachedResponse = [cachedResponses objectForKey:absoluteString];if (cachedResponse) { NSLog(@"cached: %@", absoluteString); return cachedResponse;}
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];}
NSMutableURLRequest *newRequest = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:request.timeoutInterval];newRequest.allHTTPHeaderFields = request.allHTTPHeaderFields;newRequest.HTTPShouldHandleCookies = request.HTTPShouldHandleCookies;
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;}
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:@"%02x", 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];
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;