通過NSURLProtocol來做UIWebView的cache

最近的工作與UIWebView太相關了,索性把這方面的技術都寫到博客裏來;
我提供的這個版本的NSURLProtocol是基於NSURLSession類來做的(NSURLConnection類在9.0之後就會被遺棄)。
支持的功能:
1、緩存URL的請求數據;
2、支持緩存的過期處理;
3、支持配置過濾URL,注意,NSURLProtocol這個類一旦被註冊,就是屬於APP級別的協議類,他會監聽所有的http以及https的請求。所以很多的時候我們需要設置一個過濾url數組,這個過濾url只需要提供url前綴即可(當然你不設置理論上也沒有關係都走一次監聽而已)

code:

//
//  CacheURLProtocol.m
//  CommonProject
//
//  Created by wuyoujian on 16/6/2.
//  Copyright © 2016年 wuyoujian. All rights reserved.
//

#import "CacheURLProtocol.h"
#import <CommonCrypto/CommonDigest.h>

static NSUIntegerconst kCacheExpireTime = 600;//緩存的時間 默認設置爲600秒

@interface WebCachedData :NSObject <NSCoding>
@property (nonatomic,strong) NSData *data;
@property (nonatomic,strong) NSURLResponse *response;
@property (nonatomic,strong) NSURLRequest *redirectRequest;
@property (nonatomic,strong) NSDate *date;
@end

static NSString *const kDataKey =@"data";
static NSString *const kResponseKey =@"response";
static NSString *const kRedirectRequestKey =@"redirectRequest";
static NSString *const kDateKey =@"date";

@implementation WebCachedData

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [aCoder encodeObject:[selfdata] forKey:kDataKey];
    [aCoder encodeObject:[selfresponse] forKey:kResponseKey];
    [aCoder encodeObject:[selfredirectRequest] forKey:kRedirectRequestKey];
    [aCoder encodeObject:[selfdate] forKey:kDateKey];
}

- (id)initWithCoder:(NSCoder *)aDecoder {
    self = [superinit];
    if (self !=nil) {
        [selfsetData:[aDecoder decodeObjectForKey:kDataKey]];
        [selfsetResponse:[aDecoder decodeObjectForKey:kResponseKey]];
        [selfsetRedirectRequest:[aDecoder decodeObjectForKey:kRedirectRequestKey]];
        [selfsetDate:[aDecoder decodeObjectForKey:kDateKey]];
    }

    returnself;
}

@end

static NSString *const kOurRecursiveRequestFlagProperty =@"COM.WEIMEITC.CACHE";
static NSString *const kSessionQueueName =@"WEIMEITC_SESSIONQUEUENAME";
static NSString *const kSessionDescription =@"WEIMEITC_SESSIONDESCRIPTION";


@interface CacheURLProtocol ()<NSURLSessionDataDelegate>

@property (nonatomic,strong) NSURLSession                  *session;
@property (nonatomic,copy) NSURLSessionConfiguration       *configuration;
@property (nonatomic,strong) NSOperationQueue              *sessionQueue;
@property (nonatomic,strong) NSURLSessionDataTask          *task;
@property (nonatomic,strong) NSMutableData                 *data;
@property (nonatomic,strong) NSURLResponse                 *response;

- (void)appendData:(NSData *)newData;
@end




@implementation CacheURLProtocol

static NSObject *CacheURLProtocolIgnoreURLsMonitor;
static NSArray  *CacheURLProtocolIgnoreURLs;



+ (BOOL)registerProtocolWithIgnoreURLs:(NSArray*)ignores {
    [selfunregisterCacheURLProtocol];
    [selfsetIgnoreURLs:ignores];
    return [[selfclass] registerClass:[selfclass]];
}


+ (void)unregisterCacheURLProtocol {
    [selfsetIgnoreURLs:nil];
    [[selfclass] unregisterClass:[selfclass]];
}

- (void)dealloc {
    [self.taskcancel];

    [selfsetTask:nil];
    [selfsetSession:nil];
    [selfsetData:nil];
    [selfsetResponse:nil];
    [selfsetSessionQueue:nil];
    [selfsetConfiguration:nil];
    [[selfclass] setIgnoreURLs:nil];
}

+(void)initialize {
    staticdispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        CacheURLProtocolIgnoreURLsMonitor = [NSObjectnew];
    });
}

#pragma mark - URLProtocol APIs
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    // 可以修改request對象
    return request;
}

+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
    return [superrequestIsCacheEquivalent:a toRequest:b];
}

+ (BOOL)canInitWithTask:(NSURLSessionTask *)task {
    return [selfcanInitWithURLRequest:task.currentRequest];
}

- (instancetype)initWithTask:(NSURLSessionTask *)task cachedResponse:(nullableNSCachedURLResponse *)cachedResponse client:(nullableid <NSURLProtocolClient>)client {

    self = [superinitWithTask:task cachedResponse:cachedResponseclient:client];
    if (self !=nil) {
        [selfconfigProtocolParam];
    }
    returnself;
}

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    return [selfcanInitWithURLRequest:request];
}

- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id <NSURLProtocolClient>)client {

    self = [superinitWithRequest:request cachedResponse:cachedResponseclient:client];
    if (self !=nil) {
        [selfconfigProtocolParam];
    }
    returnself;
}

- (void)startLoading {

    WebCachedData *cache = [NSKeyedUnarchiverunarchiveObjectWithFile:[selfcachePathForRequest:[selfrequest]]];

    // 這地方不能判斷cache.data字段,有可能是一個重定向的request
    if (cache) {
        // 本地有緩存
        NSData *data = [cachedata];

        NSURLResponse *response = [cacheresponse];
        NSURLRequest *redirectRequest = [cacheredirectRequest];
        NSDate *date = [cachedate];
        if ([selfexpireCacheData:date]) {
            // 數據過期
            NSLog(@"request Data-expire!");
            NSMutableURLRequest *recursiveRequest = [[selfrequest] mutableCopy];
            [[selfclass] setProperty:@YESforKey:kOurRecursiveRequestFlagPropertyinRequest:recursiveRequest];
            self.task = [self.sessiondataTaskWithRequest:recursiveRequest];
            [self.taskresume];

        } else {
            if (redirectRequest) {
                [[selfclient] URLProtocol:selfwasRedirectedToRequest:redirectRequestredirectResponse:response];
            } else {

                if (data) {
                    NSLog(@"cached Data!");
                    [[selfclient] URLProtocol:selfdidReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
                    [[selfclient] URLProtocol:selfdidLoadData:data];
                    [[selfclient] URLProtocolDidFinishLoading:self];
                } else {
                    // 本地沒有緩存上data
                    NSLog(@"request Data-uncached data!");
                    NSMutableURLRequest *recursiveRequest = [[selfrequest] mutableCopy];
                    [[selfclass] setProperty:@YESforKey:kOurRecursiveRequestFlagPropertyinRequest:recursiveRequest];
                    self.task = [self.sessiondataTaskWithRequest:recursiveRequest];
                    [self.taskresume];
                }
            }
        }

    } else {

        // 本地無緩存
        NSLog(@"request Data-no data!");
        NSMutableURLRequest *recursiveRequest = [[selfrequest] mutableCopy];
        [[selfclass] setProperty:@YESforKey:kOurRecursiveRequestFlagPropertyinRequest:recursiveRequest];
        self.task = [self.sessiondataTaskWithRequest:recursiveRequest];
        [self.taskresume];
    }
}

- (void)stopLoading {
    [self.taskcancel];

    [selfsetTask:nil];
    [selfsetData:nil];
    [selfsetResponse:nil];
}

#pragma mark - NSURLSession delegate

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)newRequest completionHandler:(void (^)(NSURLRequest *))completionHandler {

    if (response !=nil) {
        NSMutableURLRequest *redirectableRequest = [newRequestmutableCopy];
        [[selfclass] removePropertyForKey:kOurRecursiveRequestFlagPropertyinRequest:redirectableRequest];

        NSString *cachePath = [selfcachePathForRequest:[selfrequest]];
        WebCachedData *cache = [[WebCachedDataalloc] init];
        [cache setResponse:response];
        [cache setData:[selfdata]];
        [cache setDate:[NSDatedate]];
        [cache setRedirectRequest:redirectableRequest];
        [NSKeyedArchiverarchiveRootObject:cache toFile:cachePath];

        [[selfclient] URLProtocol:selfwasRedirectedToRequest:redirectableRequestredirectResponse:response];

        [self.taskcancel];
        [[selfclient] URLProtocol:selfdidFailWithError:[NSErrorerrorWithDomain:NSCocoaErrorDomaincode:NSUserCancelledErroruserInfo:nil]];

        completionHandler(redirectableRequest);
    } else {
        completionHandler(newRequest);
    }
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void(^)(NSURLSessionResponseDisposition))completionHandler {

    [selfsetResponse:response];
    [selfsetData:nil];
    [[selfclient] URLProtocol:selfdidReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];

    completionHandler(NSURLSessionResponseAllow);
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {

    [[selfclient] URLProtocol:selfdidLoadData:data];
    [selfappendData:data];
}


- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)dataTask didCompleteWithError:(NSError *)error {

    if (error ) {
        [self.clientURLProtocol:selfdidFailWithError:error];
    } else {
        NSString *cachePath = [selfcachePathForRequest:[selfrequest]];
        WebCachedData *cache = [[WebCachedDataalloc] init];
        [cache setResponse:[selfresponse]];
        [cache setData:[selfdata]];
        [cache setDate:[NSDatedate]];
        [NSKeyedArchiverarchiveRootObject:cache toFile:cachePath];

        [[selfclient] URLProtocolDidFinishLoading:self];
    }
}

#pragma mark - private APIs

+ (NSArray *)ignoreURLs {
    NSArray *iURLs;
    @synchronized(CacheURLProtocolIgnoreURLsMonitor) {
        iURLs = CacheURLProtocolIgnoreURLs;
    }
    return iURLs;
}

+ (void)setIgnoreURLs:(NSArray *)iURLs {
    @synchronized(CacheURLProtocolIgnoreURLsMonitor) {
        CacheURLProtocolIgnoreURLs = iURLs;
    }
}

+ (BOOL)canInitWithURLRequest:(NSURLRequest*)request {

    // 過濾掉不需要走URLProtocol
    NSArray *ignores = [selfignoreURLs];
    for (NSString *urlin ignores) {
        if ([[request.URLabsoluteString] hasPrefix:url]) {
            returnNO;
        }
    }

    // 如果是startLoading裏發起的request忽略掉,避免死循環
    BOOL recurisve = [selfpropertyForKey:kOurRecursiveRequestFlagPropertyinRequest:request] == nil;

    // 沒有標識位kOurRecursiveRequestFlagProperty的並且是以http開的scheme都走代理;
    if (recurisve && [[request.URLscheme] hasPrefix:@"http"]) {
        returnYES;
    }

    returnNO;
}

- (void)configProtocolParam {
    NSURLSessionConfiguration *config = [[NSURLSessionConfigurationdefaultSessionConfiguration] copy];
    [config setProtocolClasses:@[ [selfclass] ]];
    [selfsetConfiguration:config];

    NSOperationQueue *q = [[NSOperationQueuealloc] init];
    [q setMaxConcurrentOperationCount:1];
    [q setName:kSessionQueueName];
    [selfsetSessionQueue:q];

    NSURLSession *s = [NSURLSessionsessionWithConfiguration:_configurationdelegate:selfdelegateQueue:_sessionQueue];
    s.sessionDescription =kSessionDescription;
    [selfsetSession:s];
}

- (NSString*)md5Encode:(NSString*)srcString {
    constchar *cStr = [srcString UTF8String];
    unsignedchar result[16];
    CC_MD5( cStr, (unsignedint)strlen(cStr), result);

    NSString *formatString =@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x";

    return [NSStringstringWithFormat:formatString,
            result[0], result[1], result[2], result[3],
            result[4], result[5], result[6], result[7],
            result[8], result[9], result[10], result[11],
            result[12], result[13], result[14], result[15]];
}

- (NSString *)cachePathForRequest:(NSURLRequest *)aRequest {
    NSString *cachesPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory,NSUserDomainMask, YES)lastObject];
    NSString *fileName = [selfmd5Encode:[[aRequest URL]absoluteString]];
    return [cachesPathstringByAppendingPathComponent:fileName];
}

- (void)appendData:(NSData *)newData {
    if ([selfdata] == nil) {
        self.data = [[NSMutableDataalloc] initWithCapacity:0];
    }

    if (newData) {
        [[selfdata] appendData:newData];
    }
}

- (BOOL)expireCacheData:(NSDate *)date {

    if (!date) {
        returnYES;
    }

    NSTimeInterval timeInterval = [[NSDatedate] timeIntervalSinceDate:date];
    BOOL bRet = timeInterval <kCacheExpireTime;
    if (!bRet) {
        // 過期刪除緩存
        NSString *filename = [selfcachePathForRequest:[selfrequest]];
        NSFileManager *defaultManager = [NSFileManagerdefaultManager];
        if ([defaultManagerisDeletableFileAtPath:filename]) {
            [defaultManager removeItemAtPath:filenameerror:nil];
        }
    }

    return !bRet;
}

@end
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章