背景
Webview在客户端的运用场景越来越多,离线加载能节省网络加载耗时,提高用户体会。随着内部安卓端离线加载落地(《Android离线加载落地》),iOS端也进行了相关前期可行性和计划的调研
计划挑选
目前干流的离线包的恳求阻拦计划有两种:
-
通过NSURLProtocol完成,注册scheme阻拦
-
WKURLSchemeHandler完成,自界说sheme阻拦
由于NSURLProtocol阻拦会存在Post恳求丢掉body的问题,而且阻拦范围是全局的,风险性更大,因而WKURLSchemeHandler相较之下会是更好的挑选,下文的评论也是针对WKURLSchemeHandler的完成和坑点
代码完成
WKURLSchemeHandler是iOS11后提供给开发者支撑加载自界说协议资源的接口,假如需求支撑scheme为http或https恳求的数据办理则需求hookWKWebView的handlesURLScheme:办法
1. hookhandlesURLScheme办法
+ (void)load{
staticdispatch_once_tonceToken;
dispatch_once(&onceToken, ^{
MethodoriginalMethod1= class_getClassMethod(self, @selector(handlesURLScheme:));
MethodswizzledMethod1= class_getClassMethod(self, @selector(ts_handlesURLScheme:));
method_exchangeImplementations(originalMethod1,swizzledMethod1);
});
}
+ (BOOL)ts_handlesURLScheme:(NSString*)urlScheme{
if ([urlSchemeisEqualToString:@"http"] || [urlSchemeisEqualToString:@"https"]) {
returnNO;
} else {
return [selfhandlesURLScheme:urlScheme];
}
}
@end
2. 创立WKURLSchemeHandler
@available(iOS11.0, *)
class TSURLSchemeHandler: NSObject, WKURLSchemeHandler {
func webView(_webView: WKWebView,starturlSchemeTask: WKURLSchemeTask) {
//1.获取Request实例
letrequest=urlSchemeTask.request
//2.判别是否存在本地资源
//...
//2.1加载本地资源
//2.2转发恳求,加载线上资源
}
func webView(_webView: WKWebView,stopurlSchemeTask: WKURLSchemeTask) {
}
}
3. 设置webviewConfiguration
letconfig= WKWebViewConfiguration()
//iOS11以上设置阻拦
if#available(iOS11.0, *) {
lethandler = TSURLSchemeHandler()
config.setURLSchemeHandler(handler,forURLScheme: "https")
config.setURLSchemeHandler(handler,forURLScheme: "http")
}
webview= WKWebView(frame:frame,configuration:config)
留意点
1. Thetaskhasalreadybeenstopped溃散问题
溃散的原因是由于WKURLSchemeTask
//界说一个字典寄存使命
private vartaskDict: [String: WKURLSchemeTask] = [:]
//使命开端
func webView(_webView: WKWebView,starturlSchemeTask: WKURLSchemeTask) {
//保存使命
taskDict[urlSchemeTask.description] =urlSchemeTask
letrequest=urlSchemeTask.request
//建议恳求
URLSession.shared.dataTask(with:request) {data,resp,errin
//判别task是否已结束
guard lettask= self.taskDict[urlSchemeTask.description] else {
print("使命已结束,丢掉task")
return
}
//判别恳求是否成功
guard letresp=respas? HTTPURLResponse else {
leterr=err?? NSError(domain: "",code: -1,userInfo: [NSLocalizedDescriptionKey: "ResponseNil"])
task.didFailWithError(err)
return
}
//接纳呼应头
task.didReceive(resp)
//接纳呼应数据
if letdata=data{
task.didReceive(data)
}
//使命结束
task.didFinish()
//删除使命
self.taskDict.removeValue(forKey:task.description)
}.resume()
}
//使命停止
func webView(_webView: WKWebView,stopurlSchemeTask: WKURLSchemeTask) {
taskDict.removeValue(forKey:urlSchemeTask.description)
}
2. Blob上传溃散问题
当WKWebView创立一个httpBody中的时,会调用WebCore::blobRegistry办法,但回来的是一个空指针,然后导致了溃散。查阅资源,可通过私有api+[WebView_setLoadResourcesSerially:]来处理。webkit-iOSWKWebView-WKURLSchemeHandlercrashonpostingbody(EXC_BAD_ACCESS)-StackOverflow
JS测验代码
letblob= new Blob(["Hello,world!"], {type: "text/plain"});
letformData= new FormData();
formData.append("file",blob, "file.txt");
//运用fetchAPI发送恳求
fetch("xxxx", {
method: "POST",
body:formData
})
处理计划,运用私有API,需留意混杂
letselector= sel_registerName("_setLoadResourcesSerially:")
if letwv= NSClassFromString("WebView") as? NSObject.Type,
wv.responds(to:selector) {
wv.perform(selector,with: false as Any)
}
3. Blob上传内容丢掉问题
处理了上述溃散问题后,还存在上传Blob时数据丢掉状况。经测验,网页中运用<inputtype="file">
上传文件数据能正常传输,而手动结构的Blob则会丢掉数据。
要处理这个问题,需求注入JS脚本来hook前端的ajax恳求处理Blob对象
1. 将Blob二进制数据base64后放到恳求头,客户端阻拦后从恳求头取出数据,从头拼装
2. 将Blob二进制数据base64后先通过messageHandler传给客户端,客户端建议恳求时再取出数据,从头拼装
4. cookie同步问题
WKWebView的cookies是由WKHTTPCookieStore进行办理,而客户端恳求的cookies是由NSHTTPCookieStoreage进行办理,因而可能存在cookie同步的问题
//恳求成功之后,同步cookie到webview
if letheader=resp.allHeaderFieldsas? [String: String],
leturl=resp.url{
letcookies= HTTPCookie.cookies(withResponseHeaderFields:header, for:url)
DispatchQueue.main.async{
cookies.forEach{cookiein
WKWebsiteDataStore.default().httpCookieStore.setCookie(cookie)
}
}
}
总结
iOS上要完成阻拦WKWebview恳求来完成离线化加载相对于Android来说,还是存在不少的坑点,过程中需求运用hook体系办法、私有api等,存在一定的风险性。一起还需依赖注入JS脚本并hook前端代码配合,对前端有一定的侵入性,而且还需留意各种的鸿沟状况。由于完成上存在许多的风险点,上线前必须预留好开关、灰度验证和线上监控。
最终,本文是针对iOS完成Webview阻拦的一些调研与实践,如有错误欢迎我们指正与交流~