- 實現(xiàn):
1、WKWebView加載過內(nèi)容需要做本地存儲。
2、WKWebView加載的url本地有緩存時,在無網(wǎng)狀態(tài)下也能加載出來。
3、WKWebView加載的url本地有緩存,但網(wǎng)頁內(nèi)容更改時,需要重新加載url(不取本地緩存,加載完成后更新本地緩存)。
4、可設(shè)置緩存時間和緩存最大容量。(參考“SD_WebImage”封裝的內(nèi)部緩存及清理緩存方式)。
5、可清除緩存。
話不多說,直接上代碼(封裝類):
#import <WebKit/WebKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface LCWebViewCache : NSObject
/// 從池中獲取一個WKWebView
+ (WKWebView *)getWKWebViewFromPool;
/// 緩存路徑
+ (NSString *)getCacheDirectory;
/// 清除緩存
/// @param completionBlock 完成回調(diào)
+ (void)cleanDiskWithCompletionBlock:(void(^)(void))completionBlock;
@end
NS_ASSUME_NONNULL_END
#import "LCWebViewCache.h"
#import <CommonCrypto/CommonDigest.h>
#import "ReactiveObjC.h"
#import "NSDictionary+Lotus.h"
static NSTimeInterval const kLCWebViewCacheMaxCacheAge = -604800; //過期時間: 一周
static NSUInteger const kLCWebViewCacheMaxCacheSize = 524288000; //緩存大小: 500M
static NSString * const kLCWebViewCacheDirectory = @"LCWebViewCache"; //緩存文件夾
//MARK: - WKWebView (LCWebViewCache)
@implementation WKWebView (LCWebViewCache)
+ (BOOL)handlesURLScheme:(NSString *)urlScheme {
return NO;
}
@end
//MARK: - LCWebViewCacheReourceItem
@interface LCWebViewCacheReourceItem : NSObject
@property (nonatomic,strong) NSURLResponse *response;
@property (nonatomic,strong) NSData *data;
@property (nonatomic,strong) NSError *error;
@end
@implementation LCWebViewCacheReourceItem
@end
//MARK: - LCWebViewURLHandler
@interface LCWebViewURLHandler : NSObject <WKURLSchemeHandler>
@property (nonatomic, strong) NSMutableDictionary *taskVaildDic;
@property (nonatomic, strong) dispatch_queue_t serialQueue;
@property (nonatomic, strong) NSOperationQueue *operationQueue;
@property (nonatomic, strong) NSURLSession *session;
@property (nonatomic, copy) NSString *rootCachePath;
@end
@implementation LCWebViewURLHandler
- (instancetype)init {
if (self = [super init]) {
self.taskVaildDic = [NSMutableDictionary dictionary];
self.serialQueue = dispatch_queue_create("lcweb_serial_queue", NULL);
self.operationQueue = [[NSOperationQueue alloc] init];
self.operationQueue.maxConcurrentOperationCount = 10;
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
self.session = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:self.operationQueue];
self.rootCachePath = [LCWebViewCache getCacheDirectory];
}
return self;
}
- (void)dealloc {
[self.session invalidateAndCancel];
self.session = nil;
}
- (NSString *)md5:(NSString *)string {
const char* ptr = [string UTF8String];
unsigned char md5Buffer[CC_MD5_DIGEST_LENGTH];
// CC_MD5 之前
CC_SHA1(ptr, (int)strlen(ptr), md5Buffer);
NSMutableString* output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
[output appendFormat:@"%02x",md5Buffer[i]];
}
return output;
}
- (NSData *)dataForRequestId:(NSString *)requestId {
//load from disk
NSString *cacheFilePath = [self filePathWithType:1 sessionID:requestId];
return [NSData dataWithContentsOfFile:cacheFilePath];
}
- (NSDictionary *)responseHeadersWithRequestID:(NSString *)requestId {
//load from disk
NSString *responsePath = [self filePathWithType:0 sessionID:requestId];
return [NSDictionary dictionaryWithContentsOfFile:responsePath];
}
- (void)finishRequestForRequest:(NSURLRequest *)request
response:(NSURLResponse *)response
result:(NSData *)result {
//load from cache
NSString *responseId = [self md5:request.URL.absoluteString]; //存請求的url
NSHTTPURLResponse *httpRes = (NSHTTPURLResponse *)response;
NSDictionary *responseHeaders = httpRes.allHeaderFields;
if (responseHeaders) {
NSString *responsePath = [self filePathWithType:0 sessionID:responseId];
[responseHeaders writeToFile:responsePath atomically:YES];
}
if (result) {
NSString *dataPath = [self filePathWithType:1 sessionID:responseId];
[result writeToFile:dataPath atomically:YES];
}
}
/// 緩存請求
/// @param type 0:responseHeaders, 1:responseData
/// @param sessionID 唯一id
- (NSString *)filePathWithType:(NSInteger)type sessionID:(NSString *)sessionID {
NSString *cacheFileName = [sessionID stringByAppendingPathExtension:[@(type) stringValue]];
return [self.rootCachePath stringByAppendingPathComponent:cacheFileName];
}
- (LCWebViewCacheReourceItem *)loadResource:(NSURLRequest *)request {
//load from cache
NSString *requestId = [self md5:request.URL.absoluteString];
NSDictionary *responseHeaders = [self responseHeadersWithRequestID:requestId];
if (responseHeaders) {
LCWebViewCacheReourceItem *item = [[LCWebViewCacheReourceItem alloc] init];
NSHTTPURLResponse *resp = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:responseHeaders];
item.response = resp;
item.data = [self dataForRequestId:requestId];
return item;
} else {
return nil;
}
}
- (NSString *)getRequestCookieHeaderForURL:(NSURL *)URL {
NSArray *cookieArray = [self searchAppropriateCookies:URL];
if (cookieArray != nil && cookieArray.count > 0) {
NSDictionary *cookieDic = [NSHTTPCookie requestHeaderFieldsWithCookies:cookieArray];
if ([cookieDic objectForKey:@"Cookie"]) {
return cookieDic[@"Cookie"];
}
}
return nil;
}
- (NSArray *)searchAppropriateCookies:(NSURL *)URL {
NSMutableArray *cookieArray = [NSMutableArray array];
NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in [cookieStorage cookies]) {
if ([URL.host containsString:cookie.domain]) {
[cookieArray addObject:cookie];
}
}
return cookieArray;
}
//MARK: WKURLSchemeHandler
- (void)webView:(WKWebView *)webView startURLSchemeTask:(nonnull id<WKURLSchemeTask>)urlSchemeTask {
dispatch_sync(self.serialQueue, ^{
[self.taskVaildDic setValue:@(YES) forKey:urlSchemeTask.description];
});
//獲取Cookie
NSURLRequest *request = [urlSchemeTask request];
NSMutableURLRequest *mutaRequest = [request mutableCopy];
[mutaRequest setValue:[self getRequestCookieHeaderForURL:request.URL] forHTTPHeaderField:@"Cookie"];
request = [mutaRequest copy];
//判斷是否緩存
BOOL shouldCache = YES;
if (request.HTTPMethod && ![request.HTTPMethod.uppercaseString isEqualToString:@"GET"]) {
shouldCache = NO;
}
NSString *hasAjax = [request valueForHTTPHeaderField:@"X-Requested-With"];
if (hasAjax != nil) {
shouldCache = NO;
}
//獲取緩存item
LCWebViewCacheReourceItem *item = [self loadResource:request];
NSDictionary *responseHeaders = [(NSHTTPURLResponse *)item.response allHeaderFields];
NSString *contentType = responseHeaders[@"Content-Type"];
if ([contentType isEqualToString:@"video/mp4"]) {
shouldCache = NO;
}
// 獲取網(wǎng)頁內(nèi)容有更改時關(guān)鍵字段
NSDictionary *cachedHeaders = [[NSUserDefaults standardUserDefaults] objectForKey:mutaRequest.URL.absoluteString];
//設(shè)置request headers (帶上上次的請求頭下面兩參數(shù)一種就可以,也可以兩個都帶上)
if (cachedHeaders) {
NSString *etag = [cachedHeaders objectForKey:@"Etag"];
if (etag) {
[mutaRequest setValue:etag forHTTPHeaderField:@"If-None-Match"];
}
NSString *lastModified = [cachedHeaders objectForKey:@"Last-Modified"];
if (lastModified) {
[mutaRequest setValue:lastModified forHTTPHeaderField:@"If-Modified-Since"];
}
}
// 請求是否需要刷新網(wǎng)頁
@weakify(self);
NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
NSLog(@"httpResponse == %@", httpResponse);
// 根據(jù)statusCode設(shè)置緩存策略
if ((httpResponse.statusCode == 304 || httpResponse.statusCode == 0) && item && shouldCache) {
[urlSchemeTask didReceiveResponse:item.response];
if (item.data) {
[urlSchemeTask didReceiveData:item.data];
}
[urlSchemeTask didFinish];
[mutaRequest setCachePolicy:NSURLRequestReturnCacheDataElseLoad];
} else {
@strongify(self);
if (![self.taskVaildDic boolValueForKey:urlSchemeTask.description default:NO] || !urlSchemeTask){
return;
}
[urlSchemeTask didReceiveResponse:response];
[urlSchemeTask didReceiveData:data];
if (error) {
[urlSchemeTask didFailWithError:error];
} else {
[urlSchemeTask didFinish];
if (shouldCache) {
[self finishRequestForRequest:request response:response result:data];
}
}
[mutaRequest setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData];
// 保存當(dāng)前的NSHTTPURLResponse
[[NSUserDefaults standardUserDefaults] setObject:httpResponse.allHeaderFields forKey:mutaRequest.URL.absoluteString];
}
}];
[dataTask resume];
}
- (void)webView:(WKWebView *)webView stopURLSchemeTask:(nonnull id<WKURLSchemeTask>)urlSchemeTask {
dispatch_sync(self.serialQueue, ^{
[self.taskVaildDic setValue:@(NO) forKey:urlSchemeTask.description];
});
}
@end
//MARK: - LCWebViewCache
@interface LCWebViewCache ()
@property (nonatomic, assign) NSUInteger initialViewsMaxCount;
@property (nonatomic, strong) NSMutableArray <WKWebView *>*preloadedViews;
@end
@implementation LCWebViewCache
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
static LCWebViewCache *instance = nil;
dispatch_once(&onceToken,^{
instance = [[super allocWithZone:NULL] init];
});
return instance;
}
+ (id)allocWithZone:(struct _NSZone *)zone{
return [self sharedInstance];
}
- (instancetype)init {
if (self = [super init]) {
self.initialViewsMaxCount = 10;
self.preloadedViews = [NSMutableArray arrayWithCapacity:self.initialViewsMaxCount];
[self prepareWithCount:self.initialViewsMaxCount];
}
return self;
}
+ (void)initialize {
[self cleanDiskWithCompletionBlock:^{
NSLog(@"cleanDisk");
}];
}
//MARK: Public
/// 從池中獲取一個WKWebView
+ (WKWebView *)getWKWebViewFromPool {
return [[self sharedInstance] getWKWebViewFromPool];
}
/// 緩存路徑
+ (NSString *)getCacheDirectory {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *path = [paths.firstObject stringByAppendingPathComponent:kLCWebViewCacheDirectory];
BOOL isDir = YES;
if (![[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]) {
NSError *error = nil;
[[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&error];
if (error) {
return nil;
}
}
return path;
}
/// 清除緩存
/// @param completionBlock 完成回調(diào)
+ (void)cleanDiskWithCompletionBlock:(void(^)(void))completionBlock {
NSString *diskCachePath = [self getCacheDirectory];
NSFileManager *fileManager = [NSFileManager defaultManager];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 這兩個變量主要是為了下面生成NSDirectoryEnumerator準(zhǔn)備的
// 一個是記錄遍歷的文件目錄,一個是記錄遍歷需要預(yù)先獲取文件的哪些屬性
NSURL *diskCacheURL = [NSURL fileURLWithPath:diskCachePath isDirectory:YES];
NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
// 遞歸地遍歷diskCachePath這個文件夾中的所有目錄,此處不是直接使用diskCachePath,而是使用其生成的NSURL
// 此處使用includingPropertiesForKeys:resourceKeys,這樣每個file的resourceKeys對應(yīng)的屬性也會在遍歷時預(yù)先獲取到
// NSDirectoryEnumerationSkipsHiddenFiles表示不遍歷隱藏文件
NSDirectoryEnumerator *fileEnumerator = [fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
// 獲取文件的過期時間,SDWebImage中默認(rèn)是一個星期
// 不過這里雖然稱*expirationDate為過期時間,但是實質(zhì)上并不是這樣。
// 其實是這樣的,比如在2015/12/12/00:00:00最后一次修改文件,對應(yīng)的過期時間應(yīng)該是
// 2015/12/19/00:00:00,不過現(xiàn)在時間是2015/12/27/00:00:00,我先將當(dāng)前時間減去1個星期,得到
// 2015/12/20/00:00:00,這個時間才是我們函數(shù)中的expirationDate。
// 用這個expirationDate和最后一次修改時間modificationDate比較看誰更晚就行。
NSTimeInterval maxCacheAge = kLCWebViewCacheMaxCacheAge;
NSUInteger maxCacheSize = kLCWebViewCacheMaxCacheSize;
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:maxCacheAge];
// 用來存儲對應(yīng)文件的一些屬性,比如文件所需磁盤空間
NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
// 記錄當(dāng)前已經(jīng)使用的磁盤緩存大小
NSUInteger currentCacheSize = 0;
// 在緩存的目錄開始遍歷文件. 此次遍歷有兩個目的:
// 1. 移除過期的文件
// 2. 同時存儲每個文件的屬性(比如該file是否是文件夾、該file所需磁盤大小,修改時間)
NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
// 當(dāng)前掃描的是目錄,就跳過
if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// 移除過期文件
// 這里判斷過期的方式:對比文件的最后一次修改日期和expirationDate誰更晚,如果expirationDate更晚,就認(rèn)為該文件已經(jīng)過期,具體解釋見上面
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
// 計算當(dāng)前已經(jīng)使用的cache大小,
// 并將對應(yīng)file的屬性存到cacheFiles中
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
[cacheFiles setObject:resourceValues forKey:fileURL];
}
for (NSURL *fileURL in urlsToDelete) {
// 根據(jù)需要移除文件的url來移除對應(yīng)file
[fileManager removeItemAtURL:fileURL error:nil];
}
// 如果我們當(dāng)前cache的大小已經(jīng)超過了允許配置的緩存大小,那就刪除已經(jīng)緩存的文件。
// 刪除策略就是,首先刪除修改時間更早的緩存文件
if (maxCacheSize > 0 && currentCacheSize > maxCacheSize) {
// 直接將當(dāng)前cache大小降到允許最大的cache大小的一般
const NSUInteger desiredCacheSize = maxCacheSize / 2;
// 根據(jù)文件修改時間來給所有緩存文件排序,按照修改時間越早越在前的規(guī)則排序
NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
}];
// 每次刪除file后,就計算此時的cache的大小
// 如果此時的cache大小已經(jīng)降到期望的大小了,就停止刪除文件了
for (NSURL *fileURL in sortedFiles) {
if ([fileManager removeItemAtURL:fileURL error:nil]) {
// 獲取該文件對應(yīng)的屬性
NSDictionary *resourceValues = cacheFiles[fileURL];
// 根據(jù)resourceValues獲取該文件所需磁盤空間大小
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
// 計算當(dāng)前cache大小
currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];
if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
// 如果有completionBlock,就在主線程中調(diào)用
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
}
//MARK: Private
/// 預(yù)初始化若干WKWebView
/// @param count 個數(shù)
- (void)prepareWithCount:(NSUInteger)count {
// Actually does nothing, only initialization must be called.
while (self.preloadedViews.count < MIN(count,self.initialViewsMaxCount)) {
id preloadedView = [self createPreloadedView];
if (preloadedView) {
[self.preloadedViews addObject:preloadedView];
} else {
break;
}
}
}
/// 從池中獲取一個WKWebView
- (WKWebView *)getWKWebViewFromPool {
if (!self.preloadedViews.count) {
return [self createPreloadedView];
} else {
id preloadedView = self.preloadedViews.firstObject;
[self.preloadedViews removeObject:preloadedView];
return preloadedView;
}
}
/// 創(chuàng)建一個WKWebView
- (WKWebView *)createPreloadedView {
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
configuration.preferences.javaScriptEnabled = YES;
configuration.suppressesIncrementalRendering = YES; // 是否支持記憶讀取
[configuration.preferences setValue:@YES forKey:@"allowFileAccessFromFileURLs"];//支持跨域
[configuration setURLSchemeHandler:[LCWebViewURLHandler new] forURLScheme:@"https"];
[configuration setURLSchemeHandler:[LCWebViewURLHandler new] forURLScheme:@"http"];
#ifndef POD_CONFIGURATION_RELEASE
[self setupVConsoleEnabled:configuration]; //顯示vConsole
#endif
WKWebView *wkWebView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration];
wkWebView.allowsBackForwardNavigationGestures = YES; // 是否允許手勢左滑返回上一級, 類似導(dǎo)航控制的左滑返回
wkWebView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
return wkWebView;
}
/// 用于進(jìn)行JavaScript注入
/// @param configuration 配置
- (void)setupVConsoleEnabled:(WKWebViewConfiguration *)configuration {
NSString *path = [[NSBundle mainBundle] pathForResource:@"LCWebModule" ofType:@"bundle"];
NSString *vConsolePath = [path stringByAppendingPathComponent:@"vconsole.min.js"];
NSString *vConsoleStr = [NSString stringWithContentsOfFile:vConsolePath encoding:NSUTF8StringEncoding error:nil];
NSString *jsErrorPath = [path stringByAppendingPathComponent:@"jserror.js"];
NSString *jsErrorStr = [NSString stringWithContentsOfFile:jsErrorPath encoding:NSUTF8StringEncoding error:nil];
if (vConsoleStr && jsErrorStr) {
NSString *jsStr = [vConsoleStr stringByAppendingString:jsErrorStr];
WKUserScript *wkUScript = [[WKUserScript alloc] initWithSource:jsStr injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
[configuration.userContentController addUserScript:wkUScript];
}
}
@end
使用方式如下:
#import "ViewController.h"
#import <WebKit/WKWebView.h>
#import "LCWebViewCache.h"
@interface ViewController ()<WKNavigationDelegate,WKUIDelegate>
@property (nonatomic,strong) WKWebView *webView;
@property (nonatomic,copy) NSString *url;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.url = @"http://www.itdecent.cn/u/3715407414e6";
self.webView.frame = self.view.bounds;
[self.view addSubview:self.webView];
[self startLoadRequest];
}
- (void)startLoadRequest {
self.url = [self.url stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
if ([self.url hasPrefix:@"http://"] || [self.url hasPrefix:@"https://"] || [self.url containsString:@"www"]) {
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:[self.url stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]]cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60];
[self.webView loadRequest:request];
} else {
[self.webView loadHTMLString:self.url baseURL:nil];
}
}
#pragma mark - Get
- (WKWebView *)webView {
if (!_webView) {
_webView = [LCWebViewCache getWKWebViewFromPool];
_webView.navigationDelegate = self;
_webView.UIDelegate = self;
}
return _webView;
}
@end