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