可以先下載Demo看看效果,Github地址:< GitHub - ming1016/STMURLCache: iOS預加載Web頁面方案 >
可以預加載多個網址,然後在離線狀態去顯示那幾個網址,看看是不是都完全緩存下來了。
在需要開啟預加載的地方創建
self.sCache = [STMURLCache create:^(STMURLCacheMk *mk) { mk.whiteListsHost(whiteLists).whiteUserAgent(@"starming"); }];
這裡是所有可設置項目,默認設置可以查看 model 的 get 方法
- (STMURLCacheMk *(^)(NSUInteger)) memoryCapacity; //內存容量 - (STMURLCacheMk *(^)(NSUInteger)) diskCapacity; //本地存儲容量 - (STMURLCacheMk *(^)(NSUInteger)) cacheTime; //緩存時間 - (STMURLCacheMk *(^)(NSString *)) subDirectory; //子目錄 - (STMURLCacheMk *(^)(BOOL)) isDownloadMode; //是否啟動下載模式 - (STMURLCacheMk *(^)(NSArray *)) whiteListsHost; //域名白名單 - (STMURLCacheMk *(^)(NSString *)) whiteUserAgent; //WebView的user-agent白名單 - (STMURLCacheMk *(^)(NSString *)) addHostWhiteList; //添加一個域名白名單 - (STMURLCacheMk *(^)(NSString *)) addRequestUrlWhiteList; //添加請求白名單 //NSURLProtocol相關設置 - (STMURLCacheMk *(^)(BOOL)) isUsingURLProtocol; //是否使用NSURLProtocol,默認使用NSURLCache
也可以隨時更新這些設置項
[self.sCache update:^(STMURLCacheMk *mk) { mk.isDownloadMode(YES); }];
預加載名單可以按照整個 web 頁面請求進行預加載
[self.sCache preLoadByWebViewWithUrls:@[@"http://www.v2ex.com",@"http://www.github.com"];
如果需要按照單個資源列表進行預加載可以使用 preLoadByRequestWithUrls 這個方法。
對於只希望緩存特定域名或者地址的可以通過白名單進行設置,可以在創建時進行設置或者更新時設置。
NSString *whiteListStr = @"www.starming.com|www.github.com|www.v2ex.com|www.baidu.com"; NSMutableArray *whiteLists = [NSMutableArray arrayWithArray:[whiteListStr componentsSeparatedByString:@"|"]]; self.sCache = [STMURLCache create:^(STMURLCacheMk *mk) { mk.whiteListsHost(whiteLists).whiteUserAgent(@"starming"); }];
這裡的 whiteUserAgent 的設置會設置 webview 的 UserAgent,這樣能夠讓webview以外的網絡請求被過濾掉。
創建 STMURLCache 後設置 NSURLCache 的 URLCache ,在 cachedResponseForRequest 方法中獲取 NSURLRequest 判斷白名單,檢驗是否有與之對應的 Cache ,有就使用本地數據返回 NSCachedURLResponse ,沒有就通過網絡獲取數據數據緩存。 STMURLCache 對象釋放時將 NSURLCache 設置為不緩存,表示這次預加載完成不需要再緩存。當緩存空間超出設置大小會將其清空。
使用 NSURLProtocol 這種原理基本類似。
創建域名列表設置項 whiteListsHost 和 userAgent 設置項,在創建和更新時對其進行設置。在網絡請求開始通過設置項進行過濾。具體實現如下
//對於域名白名單的過濾 if (self.mk.cModel.whiteListsHost.count > 0) { id isExist = [self.mk.cModel.whiteListsHost objectForKey:[self hostFromRequest:request]]; if (!isExist) { return nil; } } //User-Agent來過濾 if (self.mk.cModel.whiteUserAgent.length > 0) { NSString *uAgent = [request.allHTTPHeaderFields objectForKey:@"User-Agent"]; if (uAgent) { if (![uAgent hasSuffix:self.mk.cModel.whiteUserAgent]) { return nil; } } }
緩存的實現有兩種,一種是 NSURLCache 另一種是 NSURLProtocol , STMURLCache 同時支持了這兩種,通過 STMURLCacheModel 裡的 isUsingURLProtocol 設置項來選擇使用哪個。
沒有緩存的 request 會對其進行請求將獲取數據按照hash地址存兩份於本地,一份是數據,一份記錄時間和類型,時間記錄可以用於判斷失效時間。對於判斷是否有緩存可以根據請求地址對應的文件進行判斷。具體實現如下:
- (NSCachedURLResponse *)localCacheResponeWithRequest:(NSURLRequest *)request { __block NSCachedURLResponse *cachedResponse = nil; NSString *filePath = [self filePathFromRequest:request isInfo:NO]; NSString *otherInfoPath = [self filePathFromRequest:request isInfo:YES]; NSDate *date = [NSDate date]; NSFileManager *fm = [NSFileManager defaultManager]; if ([fm fileExistsAtPath:filePath]) { //有緩存文件的情況 BOOL expire = false; NSDictionary *otherInfo = [NSDictionary dictionaryWithContentsOfFile:otherInfoPath]; if (self.cacheTime > 0) { NSInteger createTime = [[otherInfo objectForKey:@"time"] integerValue]; if (createTime + self.cacheTime < [date timeIntervalSince1970]) { expire = true; } } if (expire == false) { //從緩存裡讀取數據 NSData *data = [NSData dataWithContentsOfFile:filePath]; NSURLResponse *response = [[NSURLResponse alloc] initWithURL:request.URL MIMEType:[otherInfo objectForKey:@"MIMEType"] expectedContentLength:data.length textEncodingName:[otherInfo objectForKey:@"textEncodingName"]]; NSCachedURLResponse *cachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:data]; return cachedResponse; } else { //cache失效了 [fm removeItemAtPath:filePath error:nil]; //清除緩存data [fm removeItemAtPath:otherInfoPath error:nil]; //清除緩存其它信息 return nil; } } else { //從網絡讀取 self.isSavedOnDisk = NO; id isExist = [self.responseDic objectForKey:request.URL.absoluteString]; if (isExist == nil) { [self.responseDic setValue:[NSNumber numberWithBool:TRUE] forKey:request.URL.absoluteString]; NSURLSession *session = [NSURLSession sharedSession]; NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { if (error) { cachedResponse = nil; } else { NSDictionary *dic = [NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:@"%f",[date timeIntervalSince1970]],@"time",response.MIMEType,@"MIMEType",response.textEncodingName,@"textEncodingName", nil]; BOOL resultO = [dic writeToFile:otherInfoPath atomically:YES]; BOOL result = [data writeToFile:filePath atomically:YES]; if (resultO == NO || result == NO) { } else { } cachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:data]; } }]; [task resume]; return cachedResponse; } return nil; } }
在設置配置項和更新配置項時需要創建一個 STMURLCacheModel 的單例來進行設置和更新配置項給 NSURLProtocol 的實現來使用。通過 isUsingURLProtocol 設置項區分, NSURLProtocol 是通過registerClass方式將protocol實現的進行注冊。
- (STMURLCache *)configWithMk { self.mk.cModel.isSavedOnDisk = YES; if (self.mk.cModel.isUsingURLProtocol) { STMURLCacheModel *sModel = [STMURLCacheModel shareInstance]; sModel.cacheTime = self.mk.cModel.cacheTime; sModel.diskCapacity = self.mk.cModel.diskCapacity; sModel.diskPath = self.mk.cModel.diskPath; sModel.cacheFolder = self.mk.cModel.cacheFolder; sModel.subDirectory = self.mk.cModel.subDirectory; sModel.whiteUserAgent = self.mk.cModel.whiteUserAgent; sModel.whiteListsHost = self.mk.cModel.whiteListsHost; [NSURLProtocol registerClass:[STMURLProtocol class]]; } else { [NSURLCache setSharedURLCache:self]; } return self; }
關閉時兩者也是不同的,通過設置項進行區分
- (void)stop { if (self.mk.cModel.isUsingURLProtocol) { [NSURLProtocol unregisterClass:[STMURLProtocol class]]; } else { NSURLCache *c = [[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil]; [NSURLCache setSharedURLCache:c]; } [self.mk.cModel checkCapacity]; }
白名單處理還有讀取緩存和前者都類似,但是在緩存Data時 NSURLCached 的方案裡是通過發起一次新的請求來獲取數據,而 NSURLProtocol 在 NSURLConnection 的 Delegate 裡可以獲取到,少了一次網絡的請求,這裡需要注意的是在 - (void) connection:(NSURLConnection )connection didReceiveData:(NSData )data 每次從這個回調裡獲取的數據不是完整的,要在 - (void) connectionDidFinishLoading:(NSURLConnection *)connection 這個會調裡將分段數據拼接成完整的數據保存下來。具體完整的代碼實現可以看 STMURLProtocol 裡的代碼實現。
通過 map 網絡請求可以緩存請求,也可以 mock 接口請求進行測試。
完整代碼:< GitHub - ming1016/STMURLCache: iOS預加載Web頁面方案 >