Recently encountered such a requirement: need to WKWebview request resources cache to the local, the next request, determine whether there is a corresponding local resource, if there is, will be cached resources back, if not to re-download.

The idea is to use NSURLProtocol interception.

Using NSURLProtocol directly in WKWebview does not work because:

WKWebview performs network operations in a process outside the standalone APP process, requesting data that does not pass through the main thread.Copy the code

At this point, you need some magic solutions to solve this problem.

It is found through consulting materials that Apple uses NSURLProtocol in the unit test testProtocol. mm in the source code of WebKit. The usage is as follows:

+ (NSString *)scheme
{
    return testScheme;
}

+ (void)registerWithScheme:(NSString *)scheme
{
    testScheme = [scheme retain];
    [NSURLProtocol registerClass:[self class]];
    [WKBrowsingContextController registerSchemeForCustomProtocol:testScheme];
}
Copy the code

In see below WKBrowsingContextController. Mm registerSchemeForCustomProtocol source of implementation:

+ (void)registerSchemeForCustomProtocol:(NSString *)scheme
{
    WebProcessPool::registerGlobalURLSchemeAsHavingCustomProtocolHandlers(scheme);
}
Copy the code

Interception is implemented by registering a global custom scheme with WebProcessPool.

Here’s how to implement the problem mentioned in the opening paragraph:

  • Register the schema you want to intercept first, and unregister it at the end of the process. Otherwise, other pages may not load, as follows:
- (void)registerCustomProtocol {
    [NSURLProtocol registerClass:[CustomURLProtocol class]];
    Class cls = NSClassFromString(@"WKBrowsingContextController");
    SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
    if ([cls respondsToSelector:sel]) {
        [cls performSelector:sel withObject:@"http"];
        [cls performSelector:sel withObject:@"https"];
    }
}

 - (void)dealloc{
    [NSURLProtocol unregisterClass:[CustomURLProtocol class]];
}
Copy the code
  • NSURLProtocol (); NSURLProtocol (); NSURLProtocol ();
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
- (void)startLoading;
- (void)stopLoading;
Copy the code

The canInitWithRequest: method is used to determine whether the request goes into the custom NSURLProtocol loader as follows:

static NSString *kURLProtocolHandledKey = @"urlProtocolHandleKey";

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    NSString *scheme = [[request URL] scheme];
    if ([scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"] {// Check if it has already been processed to prevent an infinite loopif ([NSURLProtocol propertyForKey:kURLProtocolHandledKey inRequest:request]) {
            return NO;
        }
        
        if ([request.URL.pathExtension isEqualToString:@"png"]) {
            returnYES; }}return NO;
}
Copy the code

CanonicalRequestForRequest: method is used to reset NSURLRequest information, in this this method can do something for the request custom operations, such as adding request first, use the following:

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    NSMutableURLRequest *mutableReqeust = [request mutableCopy];
    [mutableReqeust setValue:@"gzip" forHTTPHeaderField:@"Accept-Encoding"];
    return mutableReqeust;
}
Copy the code

The startLoading method is where the intercepted request starts execution, with the following code:

- (void)startLoading { NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy]; // indicates that the request was processed [NSURLProtocol]setProperty:@YES forKey:kURLProtocolHandledKey inRequest:mutableReqeust];
    
    NSString *fileName = @"xxx"/// This request corresponds to the local sandbox addressif([[NSFileManager defaultManager] fileExistsAtPath: fileName]) {/ / to use local resources NSError * error = nil; NSData *data = [NSData dataWithContentsOfFile:filePath options:NSDataReadingUncached error:&error]; / / / the data return [self sendResponseWithData: data mimeType: [self getMimeTypeWithFilePath: filePath]]. }else{// 1, download resources // 2, download resources to the sandbox /// 3, return data}}Copy the code

Here’s how to return a local NSData:

- (void)sendResponseWithData:(NSData *)data mimeType:(nullable NSString *)mimeType {
    NSURLResponse *response = [[NSURLResponse alloc] initWithURL:super.request.URL
                                                        MIMEType:mimeType
                                           expectedContentLength:-1
                                                textEncodingName:nil];
    [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
    [[self client] URLProtocol:self didLoadData:data];
    [[self client] URLProtocolDidFinishLoading:self];
}
Copy the code

The stopLoading method is used to cancel the request as follows:

- (void)stopLoading{
     [self.task cancel];
}
Copy the code

Since this is a magic method that calls a private API, the audit will be rejected. How can I avoid this risk?

The answer is confusion, as follows:

- (NSString *)base64DecodedString:(NSString *)origin { NSData *data = [[NSData alloc] initWithBase64EncodedString:origin  options:0];return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}

- (void)registerCustomProtocol {
    [NSURLProtocol registerClass:[CustomURLProtocol class]];
    Class cls = NSClassFromString([self base64DecodedString:@"V0tCcm93c2luZ0NvbnRleHRDb250cm9sbGVy"]);
    SEL sel = NSSelectorFromString([self base64DecodedString:@"cmVnaXN0ZXJTY2hlbWVGb3JDdXN0b21Qcm90b2NvbDo="]);    if ([cls respondsToSelector:sel]) {
        [cls performSelector:sel withObject:@"http"];
        [cls performSelector:sel withObject:@"https"]; }}Copy the code

Scan follow me, get more technical good articles