原文地址 最近开发中遇到这样一个需求:需要将 WKWebview 请求的资源缓存到本地,下次请求,判断本地是否有对应的资源,如果有,就将缓存的资源返回,如果没有在去重新下载。
这时我们想到的就是使用 NSURLProtocol 拦截。
但是尝试之后发现在 WKWebview 中直接使用 NSURLProtocol 会不生效,这是因为:
WKWebview 在独立 app 进程之外的进程执行网络操作,请求数据不经过主线程。
这时,就需要一些 magic 的方案来解决此问题了。
通过查阅资料发现,在 webkit 的源码中,apple 在单元测试 TestProtocol.mm 中用到了 NSURLProtocol,用法如下:
+ (NSString *)scheme
{
return testScheme;
}
+ (void)registerWithScheme:(NSString *)scheme
{
testScheme = [scheme retain];
[NSURLProtocol registerClass:[self class]];
[WKBrowsingContextController registerSchemeForCustomProtocol:testScheme];
}
下面在看下 WKBrowsingContextController.mm 源码中 registerSchemeForCustomProtocol 的实现:
+ (void)registerSchemeForCustomProtocol:(NSString *)scheme
{
WebProcessPool::registerGlobalURLSchemeAsHavingCustomProtocolHandlers(scheme);
}
意思是通过向 WebProcessPool 注册全局自定义 scheme ,从而实现拦截。
下面看下开篇提到的问题该如何实现:
- 首先注册需要拦截的 schema,结束的时候需要取消注册,否则可能会导致其他页面加载不出来,代码如下:
- (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]];
}
- 然后实现自定义的 URL Protocol,需要继承 NSURLProtocol,需要重写下面几个方法:
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
- (void)startLoading;
- (void)stopLoading;
canInitWithRequest: 方法是用来判断请求是否进入自定义的 NSURLProtocol 加载器,使用方式如下:
static NSString *kURLProtocolHandledKey = @"urlProtocolHandleKey";
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
NSString *scheme = [[request URL] scheme];
if ([scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"]) {
// 判断是否已经处理过了,防止无限循环
if ([NSURLProtocol propertyForKey:kURLProtocolHandledKey inRequest:request]) {
return NO;
}
if ([request.URL.pathExtension isEqualToString:@"png"]) {
return YES;
}
}
return NO;
}
canonicalRequestForRequest: 方法用来重新设置 NSURLRequest 的信息, 在这个这方法里面可以对请求做些自定义操作,如添加请求头等,使用如下:
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
NSMutableURLRequest *mutableReqeust = [request mutableCopy];
[mutableReqeust setValue:@"gzip" forHTTPHeaderField:@"Accept-Encoding"];
return mutableReqeust;
}
startLoading 方法是被拦截请求开始执行的地方,代码如下:
- (void)startLoading {
NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
// 标识该request已经处理过了
[NSURLProtocol setProperty:@YES forKey:kURLProtocolHandledKey inRequest:mutableReqeust];
NSString *fileName = @"xxx"/// 该请求对应的本地沙盒地址
if ([[NSFileManager defaultManager] fileExistsAtPath:fileName]) {
// 使用本地资源
NSError *error = nil;
NSData *data = [NSData dataWithContentsOfFile:filePath options:NSDataReadingUncached error:&error];
/// 将 data 返回
[self sendResponseWithData:data mimeType:[self getMimeTypeWithFilePath:filePath]];
} else {
/// 1、下载资源
/// 2、将下载的资源存到沙盒里
/// 3、将数据返回
}
}
下面看下如何将本地的 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];
}
stopLoading 方法,用来取消请求,代码如下:
- (void)stopLoading{
[self.task cancel];
}
由于这是一个 magic 方法,调用的是私有 API ,会导致审核被拒,那么该如何规避这个风险呢?
答案就是 混淆,代码如下:
- (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"];
}
}
扫一扫关注我,get更多技术好文