WKWebView 拦截网络请求实现秒开方案和实践(附demo)

demo地址

在 WKWebView 的秒开方案中,使用 WKURLSchemeTask 拦截网络请求已经成为现在的主流方式,本文主要介绍一下该方案中需要注意的几个地方:使用版本限制、如何拦截所有 http 和 https 请求、This task has already been stopped 问题处理、大视频文件播放白屏时间长问题处理、重定向导致的白屏处理。

如果还有其他更好的处理方案和文章里没提到的坑,非常非常非常欢迎留言!

效果图:

一、使用版本限制

本方案不支持 iOS 13.0 13.1 13.2 13.3 这四个版本,会导致崩溃,我暂时没找到解决的办法,只能避开这四个版本,如果有解决的方案欢迎留言指导。

崩溃:

demo处理:

ini 复制代码
//iOS 13.0 13.1 13.2 13.3 这四个版本不要使用离线包, 会导致崩溃
NSString *systemVersion = [[UIDevice currentDevice] systemVersion];
if ([systemVersion compare:@"13.0"] == NSOrderedAscending || [systemVersion compare:@"13.3"] == NSOrderedDescending) {
    ZWKURLHandler *handler = [[ZWKURLHandler alloc] init];
    //直接拦截所有 http 和 https 的请求
    [config setURLSchemeHandler:handler forURLScheme:@"https"];
    [config setURLSchemeHandler:handler forURLScheme:@"http"];
    _urlHandler = handler;
}

二、如何拦截所有http和https请求

在 WKWebView 的分类里重写 handlesURLScheme 方法。

demo处理:

less 复制代码
@interface WKWebView (HandlesURLScheme)
@end
@implementation WKWebView (HandlesURLScheme)
#pragma mark 这里不返回 NO 的话, WKWebViewConfiguration 里 setURLSchemeHandler 就会崩溃
+ (BOOL)handlesURLScheme:(NSString *)urlScheme {
    return NO;
}

@end

三、This task has already been stopped 问题处理

该问题出现的原因是打开网页后快速返回,多次重复后有概率出现,而且概率不低。

处理的核心是在自己发起的网络请求回调里,把数据传给 webview 的地方加 try catch,其他非核心的处理有:控制器销毁后,在 startURLSchemeTask 里做拦截处理;当网络请求返回数据后,不要处理已经在 stopURLSchemeTask 中标记了的urlSchemeTask。

demo处理:

objectivec 复制代码
///记录urlSchemeTask是否被stop
@property (nonatomic,strong) NSMutableDictionary *holdUrlSchemeTasks;
///记录controller是否销毁,避免发起多余的请求
@property (nonatomic, assign) BOOL isControllerDealloced;

- (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask {
    if (_isControllerDealloced) {
        //过滤掉多余的请求
        return;
    }
}

- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask {
    NSString *key = urlSchemeTask.request.requestId;
    if ([self.holdUrlSchemeTasks objectForKey:key]) {
        [self.holdUrlSchemeTasks removeObjectForKey:key];
    }
}

- (void)handleOnlineRequst:(NSURLRequest *)request urlSchemeTask:(__weak id <WKURLSchemeTask>)urlSchemeTask {
    //转网络请求
    NSMutableURLRequest *mutaRequest = [request mutableCopy];
    __weak typeof(self) weakSelf = self;
    NSString *requestUrlStr = request.URL.absoluteString.copy;
    
    NSURLSessionTask *task = [self.session dataTaskWithRequest:mutaRequest completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (weakSelf.isControllerDealloced || !urlSchemeTask || ![weakSelf.holdUrlSchemeTasks objectForKey:urlSchemeTask.request.requestId]) {
            //过滤掉已经stop的网络请求
            return;
        }
        //避免 The task has already been stopped 这个断言导致的崩溃
        @try {
            if (error) {
                [urlSchemeTask didReceiveResponse:response];
                [urlSchemeTask didFailWithError:error];
            }else{
                //处理下重定向
                NSHTTPURLResponse *httpResp = [weakSelf handleRedirectUrlWithResponse:response requestUrlStr:requestUrlStr];
                [urlSchemeTask didReceiveResponse:httpResp];
                if (data) {
                    [urlSchemeTask didReceiveData:data];
                }
                [urlSchemeTask didFinish];
            }
        } @catch (NSException *exception) {
            NSLog(@"urlSchemeTask 停了停了");
        }
    }];
    
    [task resume];
    NSString *requestId = urlSchemeTask.request.requestId;
    self.holdUrlSchemeTasks[requestId] = @1;
}

四、大视频文件播放问题

该问题出现场景是 webView 播放一个很大的视频时,有时 startURLSchemeTask 传过来 request 的 Range 是整个视频的大小,如果不做处理,会白屏很长时间,或者请求超时导致视频播放异常。

处理方案:手动进行文件切片。

demo处理:

ini 复制代码
#pragma mark 大文件切片
- (NSMutableURLRequest * _Nullable)sliceDataRequest:(NSURLRequest *)request {
    NSString *range = [request.allHTTPHeaderFields objectForKey:@"Range"];
    if (range) {
        NSMutableURLRequest *mutaRequest = [request mutableCopy];
        NSString *rangeStr = [range stringByReplacingOccurrencesOfString:@"bytes=" withString:@""];
        NSArray *ranges = [rangeStr componentsSeparatedByString:@"-"];
        long long startPos = [ranges.firstObject longLongValue];
        long long endPos = [ranges.lastObject longLongValue];
        //这里切成 5M 一片
        if (endPos - startPos > 5 * 1024 *1024) {
            [mutaRequest setValue:[NSString stringWithFormat:@"bytes=%lld-%lld", startPos, MIN(endPos, startPos + 5 * 1024 *1024)] forHTTPHeaderField:@"Range"];
            return mutaRequest;
        }
    }
    return nil;
}

五、重定向导致的白屏处理

重定向处理较麻烦,需要 WKURLSchemeHandler 和 WKWebView代理共同合作,直接上 code。

拦截到重定向

objectivec 复制代码
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
    NSURL *url = response.URL;
    if (url.pathExtension.length == 0 || [url.pathExtension isEqualToString:@"html"]) {
        //接收到 HTML 文档的重定向时,这里返回 nil, 那么在 dataTaskWithRequest 的回调里就能收到 code = 302 的response, 然后在 webview 里对 302 做特殊处理就行
        completionHandler(nil);
    } else {
        completionHandler(request);
    }
}

NSURLSessionTask 处理重定向

ini 复制代码
- (NSHTTPURLResponse *)handleRedirectUrlWithResponse:(NSURLResponse *)response requestUrlStr:(NSString *)requestUrlStr {
    NSHTTPURLResponse *httpResp = (NSHTTPURLResponse *)response;
    NSInteger statusCode = httpResp.statusCode;
    NSString *newRequestUrl = httpResp.allHeaderFields[@"Location"];
    // 302 重定向
    if (statusCode >= 300 && statusCode < 400 && newRequestUrl && requestUrlStr.length > 0) {
        NSMutableDictionary *allHeaderFields = httpResp.allHeaderFields.mutableCopy;
        //重定向的时候,将原来的 url 通过 response 传给webview
        allHeaderFields[@"redirectUrl"] = requestUrlStr;
        
        httpResp = [[NSHTTPURLResponse alloc] initWithURL:httpResp.URL statusCode:httpResp.statusCode HTTPVersion:@"HTTP/1.1" headerFields:allHeaderFields];
    }
    return httpResp;
}

WKWebView代理里处理重定向

objectivec 复制代码
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
    if ([navigationResponse.response isKindOfClass:[NSHTTPURLResponse class]]) {
        NSHTTPURLResponse *httpResp = (NSHTTPURLResponse *)navigationResponse.response;
        NSInteger statusCode = httpResp.statusCode;
        NSString *newRequestUrl = httpResp.allHeaderFields[@"Location"];
        NSString *redirectUrl = httpResp.allHeaderFields[@"redirectUrl"];
        // 302 重定向
        if (statusCode >= 300 && statusCode < 400 && redirectUrl && newRequestUrl) {
            //记录下重定向之前的url, 不要显示错误界面
            self.redirectUrl = redirectUrl;
            //这里cancel掉, 然后直接load新的url
            decisionHandler(WKNavigationResponsePolicyCancel);
            _request = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:newRequestUrl] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10];
            [self.webView loadRequest:_request];
            return;
        }
    }
    decisionHandler(WKNavigationResponsePolicyAllow);
}

//不要显示错误界面
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error {
    //如果是重定向的url,这里 return 掉, 不要显示错误界面
    if (self.redirectUrl.length > 0 && error.userInfo && [error.userInfo objectForKey:@"NSErrorFailingURLStringKey"]) {
        NSString *failingURLString = [NSString stringWithFormat:@"%@", error.userInfo[@"NSErrorFailingURLStringKey"]];
        if ([self.redirectUrl isEqualToString:failingURLString]) {
            self.redirectUrl = nil;
            return;
        }
    }
}

//页面加载完成
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
    self.redirectUrl = nil;
}

内容参考:

WKWebview秒开的实践及踩坑之路

WKWebView完美网络请求拦截

再发一次demo地址:

demo地址

相关推荐
我在北京coding1 小时前
6套bootstrap后台管理界面源码
前端·bootstrap·html
明月看潮生4 小时前
青少年编程与数学 01-011 系统软件简介 07 iOS操作系统
ios·青少年编程·操作系统·系统软件
90后的晨仔5 小时前
RxSwift 框架解析
前端·ios
技术小丁6 小时前
使用 HTML + JavaScript 实现自定义富文本编辑器开发实践(附完整代码)
前端·javascript·html
代码搬运媛9 小时前
React 中 HTML 插入的全场景实践与安全指南
安全·react.js·html
可爱小仙子10 小时前
ios苹果系统,js 滑动屏幕、锚定无效
前端·javascript·ios
未来猫咪花10 小时前
# Flutter状态管理对比:view_model vs Riverpod
flutter·ios·android studio
lljss202012 小时前
html文字红色粗体,闪烁渐变动画效果,中英文切换版本
css·html·css3
疯狂的沙粒13 小时前
在web-view 加载的本地及远程HTML中调用uniapp的API及网页和vue页面是如何通讯的?
前端·uni-app·html
小妖66613 小时前
html 滚动条滚动过快会留下边框线
前端·html