IOS 使用NSURLProtocol 拦截网络请求实现缓存

最近项目需要实现一个WKWebview的缓存功能,然后想到通过拦截http/https请求,然后通过url字符串的MD5串来作为“Key” 存储和读取缓存,缓存数据使用YYCache 这个缓存框架还是很不错的,有通过链表实现的内存缓存,和数据库以及文件实现的磁盘缓存,这个就不多说了,具体可以github 上看源码,今天主要讲通过NSURLProtocol来实现拦截Http/https 中间可能牵涉到一些其他文件,在Demo工程里细说
主要的类NSURLProtocol 是苹果为我们提供的 URL Loading System 的一部分,这是一张从官方文档贴过来的图片:
IOS 使用NSURLProtocol 拦截网络请求实现缓存_第1张图片

在每一个 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 

@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

iOS开发网络篇—NSURLConnection基本使用

使用NSUrlProtocol进行网络拦截

你可能感兴趣的:(IOS,IOS开发)