1. 程式人生 > >IOS 使用NSURLProtocol 攔截網路請求實現快取

IOS 使用NSURLProtocol 攔截網路請求實現快取

最近專案需要實現一個WKWebview的快取功能,然後想到通過攔截http/https請求,然後通過url字串的MD5串來作為“Key” 儲存和讀取快取,快取資料使用YYCache 這個快取框架還是很不錯的,有通過連結串列實現的記憶體快取,和資料庫以及檔案實現的磁碟快取,這個就不多說了,具體可以github 上看原始碼,今天主要講通過NSURLProtocol來實現攔截Http/https 中間可能牽涉到一些其他檔案,在Demo工程裡細說 主要的類NSURLProtocol 是蘋果為我們提供的 URL Loading System 的一部分,這是一張從官方文件貼過來的圖片: 在這裡插入圖片描述

在每一個 HTTP 請求開始時,URL 載入系統建立一個合適的 NSURLProtocol 物件處理對應的 URL 請求,而我們需要做的就是寫一個繼承自 NSURLProtocol 的類,並通過 - registerClass: 方法註冊我們的協議類,然後 URL 載入系統就會在請求發出時使用我們建立的協議物件對該請求進行處理。

這樣,我們需要解決的核心問題就變成了如何使用 NSURLProtocol 來處理所有的網路請求,這裡使用蘋果官方文件中的 SCYCacheURLProtocol 進行介紹,你可以點選這裡下載原始碼。 決定是否由當前協議來處理請求

+ (BOOL)canInitWithRequest:(NSURLRequest *)request;

傳送請求,可以在這裡處理各種請求

- (void)startLoading

載入本地資料然後返回給網路請求的response,並且結束當前網路請求

- (void)loadProJectData:(NSData *)data{
        NSURLResponse* response = [[NSURLResponse alloc] initWithURL:self.request.URL MIMEType:@"application/octet-stream" expectedContentLength:data.length textEncodingName:nil];
        [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
    [[self client] URLProtocol:self didLoadData:data];
    [[self client] URLProtocolDidFinishLoading:self];
            //  NSLog(@"直接使用快取.............快取的url == %@ ", self.request.URL.absoluteString);
            [[EzwWebToolUtils sharedInstance]addReuseCount];
}

當然瞭如果本地沒有快取資料要向伺服器獲取這個就直接走正常網路請求

/// 請求最新
- (void)loadRequest{
    NSMutableURLRequest *connectionRequest = [[self request] mutableCopyWorkaround];
    [connectionRequest setValue:@"" forHTTPHeaderField:SCYCachingURLHeader];
    NSURLConnection *connection = [NSURLConnection connectionWithRequest:connectionRequest delegate:self];
    [self setConnection:connection];
}

在這個工程中 SCYCacheURLProtocol.m 是需要重點關注的檔案,SCYCacheURLProtocol 就是 NSURLProtocol 的子類:

#import <Foundation/Foundation.h>

@interface SCYCacheURLProtocol : NSURLProtocol

@end
//
//  SCYCacheURLProtocol.m
//  ProvidentFund
//
//  Created by 9188 on 2016/12/12.
//  Copyright © 2016年 9188. All rights reserved.
//

#import "SCYCacheURLProtocol.h"
#import "Reachability.h"
#import "NSString+CDEncryption.h"
#import "SCYWebViewCacheModel.h"
#import "NSURLRequest+MutableCopyWorkaround.h"
#import "SCYLoanHTMLCache.h"

#import "EzwWebToolUtils.h"
#import "EzwSourceDataManager.h"

static NSString *SCYCachingURLHeader = @"SCYCacheURLProtocolCache";

static NSSet *SCYCachingSupportedSchemes;

static NSString * const URLProtocolHandledKey = @"URLProtocolHandledKey";

static NSString * const CacheUrlStringKey = @"cacheUrlStringKey"; // 本地儲存快取urlKey的陣列key

@interface SCYCacheURLProtocol ()<NSURLConnectionDelegate>
@property (nonatomic, strong) NSURLConnection *connection;
@property (nonatomic, strong) NSMutableData *data;
@property (nonatomic, strong) NSURLResponse *response;

@property (nonatomic, strong) SCYWebViewCacheModel *cacheModel;

@end

@implementation SCYCacheURLProtocol
- (SCYWebViewCacheModel *)cacheModel{
    if (!_cacheModel) {
        _cacheModel = [[SCYWebViewCacheModel alloc] init];
    }
    return _cacheModel;
}

+ (void)initialize{
    if (self == [SCYCacheURLProtocol class]){
        SCYCachingSupportedSchemes = [NSSet setWithObjects:@"http", @"https", nil];
    }
}

//決定請求是否需要當前協議處理
+ (BOOL)canInitWithRequest:(NSURLRequest *)request{
    //如果無法獲取cdn 字串
    if([[EzwSourceDataManager sharedInstance]getCDNUrlStr].length==0){
        return NO;
    }
    //scheme url 字串前一部分 http 或者 https// 整個url 字串構成scheme:resourceSpecifier
    if ([SCYCachingSupportedSchemes containsObject:[[request URL] scheme]] &&
        ([request valueForHTTPHeaderField:SCYCachingURLHeader] == nil)){
    
        //看看是否已經處理過了,防止無限迴圈
        if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request]) {
            return NO;
        }
        return YES;
    }
    return NO;
}

//請求經過 + canInitWithRequest: 方法過濾之後,我們得到了所有要處理的請求,接下來需要對請求進行一定的操作
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request{
    NSMutableURLRequest *mutableReqeust = [request mutableCopy];
    return mutableReqeust;
}

/// 開始載入時自動呼叫--呼叫 super 的指定構造器方法,例項化一個物件,然後就進入了傳送網路請求,獲取資料並返回的階段了
- (void)startLoading{
    //打標籤,防止無限迴圈
    [NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:[[self request] mutableCopy]];
    
    //載入ipa 包裡資料
     [[EzwWebToolUtils sharedInstance]addRequestCount];
    NSData *datas = [[EzwSourceDataManager sharedInstance]getDatasFromPathStr:[[self.request URL] absoluteString]];
    if (datas!=NULL) {
        [self loadProJectData:datas];
        return;
    }
    
    // 載入本地
    SCYWebViewCacheModel *cacheModel = (SCYWebViewCacheModel *)[[SCYLoanHTMLCache defaultcache] objectForKey:[[[self.request URL] absoluteString] cd_md5HexDigest]];
    
    if ([self useCache] && cacheModel == nil) { // 可到達(有網)而且無快取  才重新獲取
        [self loadRequest];
    } else if(cacheModel) { // 有快取
        [self loadCacheData:cacheModel];
    } else { // 沒網  沒快取
        NSLog(@"沒網也沒快取.....");
    }
//    NSLog(@"%@",[[self.request URL] absoluteString] );
}
- (void)loadProJectData:(NSData *)data{
        NSURLResponse* response = [[NSURLResponse alloc] initWithURL:self.request.URL MIMEType:@"application/octet-stream" expectedContentLength:data.length textEncodingName:nil];
        [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
    [[self client] URLProtocol:self didLoadData:data];
    [[self client] URLProtocolDidFinishLoading:self];
            //  NSLog(@"直接使用快取.............快取的url == %@ ", self.request.URL.absoluteString);
            [[EzwWebToolUtils sharedInstance]addReuseCount];
}
- (void)stopLoading{
    [[self connection] cancel];
}
#pragma mark - NSURLConnectionDelegate
- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response{
    if (response != nil) {
        NSMutableURLRequest *redirectableRequest = [request mutableCopyWorkaround];
        [redirectableRequest setValue:nil forHTTPHeaderField:SCYCachingURLHeader];
        
        [self cacheDataWithResponse:response redirectRequest:redirectableRequest];
        
        [[self client] URLProtocol:self wasRedirectedToRequest:redirectableRequest redirectResponse:response];
        return redirectableRequest;
    } else {
        return request;
    }
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
    [[self client] URLProtocol:self didLoadData:data];
    [self appendData:data];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
    [[self client] URLProtocol:self didFailWithError:error];
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
    [self setResponse:response];
    [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];  // We cache ourselves.
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
    [[self client] URLProtocolDidFinishLoading:self];
    
    /// 自己專案設定的邏輯  即是伺服器版本號 > 本地版本號   則需要重新整理
    /// 先移除之前的快取,在快取新的。。對吧 這裡邏輯看情況而定
//    if ([SCYLoanUrlType() isEqualToString:@"1"] && SCYLoanSerViceVersion().integerValue > SCYLoanLocalVersion().integerValue) { // 快取最新的時候  移除之前  loacalVersion  localUrl
//        [[SCYLoanHTMLCache defaultcache] removeAllObjects];
//        [[NSUserDefaults standardUserDefaults] removeObjectForKey:CacheUrlStringKey];
//        NSLog(@"重新整理網頁成功.........");
//    }
    
    ////  有快取則不快取
    SCYWebViewCacheModel *cacheModel = (SCYWebViewCacheModel *)[[SCYLoanHTMLCache defaultcache] objectForKey:[[[self.request URL] absoluteString] cd_md5HexDigest]];
    if (!cacheModel) {
        [self cacheDataWithResponse:self.response redirectRequest:nil];
    }
    
}

#pragma mark - private
/**
 *  儲存快取資料
 *  @param response              response
 *  @param redirectableRequest   重定向request
 */
- (void)cacheDataWithResponse:(NSURLResponse *)response  redirectRequest:(NSMutableURLRequest *)redirectableRequest{
    [self.cacheModel setResponse:response];
    [self.cacheModel setData:[self data]];
    [self.cacheModel setRedirectRequest:redirectableRequest];
    
    NSString *cacheStringkey = [[[self.request URL] absoluteString] cd_md5HexDigest];
    //這個方法NSArchive 儲存資料
    [[SCYLoanHTMLCache defaultcache] setObject:self.cacheModel forKey:cacheStringkey withBlock:^{
        // 注意 這裡載入.css   jpg 等資源路徑的時候,這個類已經更新了(即陣列加urlkey陣列的時候,不能在當前類一直加,而是先從本地取了之後再加)
//        NSMutableArray *array = [[[NSUserDefaults standardUserDefaults] objectForKey:CacheUrlStringKey] mutableCopy];
//        if (!array) {   array = @[].mutableCopy;  }
//
//        [array addObject:cacheStringkey];
//        //設定快取
//        [[NSUserDefaults standardUserDefaults] setObject:array forKey:CacheUrlStringKey];
//        NSLog(@".....重置了快取  key == CacheUrlStringKey....");
//        NSLog(@"%@",self.request.URL);
//        NSLog(@".....新增了快取key %@ ...., 當前快取個數%ld",cacheStringkey, array.count);
   //     [[NSUserDefaults standardUserDefaults]synchronize];
        
    }];
}
/// 請求最新
- (void)loadRequest{
    NSMutableURLRequest *connectionRequest = [[self request] mutableCopyWorkaround];
    [connectionRequest setValue:@"" forHTTPHeaderField:SCYCachingURLHeader];
    NSURLConnection *connection = [NSURLConnection connectionWithRequest:connectionRequest delegate:self];
    [self setConnection:connection];
}

- (BOOL)useCache{
    BOOL reachable = (BOOL) [[Reachability reachabilityWithHostName:[[[self request] URL] host]] currentReachabilityStatus] != NotReachable;
    //NSLog(@"網路是否可到達  1可到達   0不可到達............. %d", reachable);
    return reachable;
}

- (void)appendData:(NSData *)newData{
    if ([self data] == nil) {
        [self setData:[newData mutableCopy]];
    } else {
        [[self data] appendData:newData];
    }
}

- (void)loadCacheData:(SCYWebViewCacheModel *)cacheModel{
    if (cacheModel) {
        NSData *data = [cacheModel data];
        NSURLResponse *response = [cacheModel response];
        NSURLRequest *redirectRequest = [cacheModel redirectRequest];
        
       
        if (redirectRequest) {
            [[self client] URLProtocol:self wasRedirectedToRequest:redirectRequest redirectResponse:response];
          //  NSLog(@"redirectRequest............. 重定向");
        } else {
            [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
            [[self client] URLProtocol:self didLoadData:data];
            [[self client] URLProtocolDidFinishLoading:self];
          //  NSLog(@"直接使用快取.............快取的url == %@ ", self.request.URL.absoluteString);
            [[EzwWebToolUtils sharedInstance]addReuseCount];
        }
    } else {
        [[self client] URLProtocol:self didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCannotConnectToHost userInfo:nil]];
    }
}
@end