背景

Webview在客户端的运用场景越来越多,离线加载能节省网络加载耗时,提高用户体会。随着内部安卓端离线加载落地(《Android离线加载落地》),iOS端也进行了相关前期可行性和计划的调研

计划挑选

目前干流的离线包的恳求阻拦计划有两种:

  1. 通过NSURLProtocol完成,注册scheme阻拦

  2. 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阻拦的一些调研与实践,如有错误欢迎我们指正与交流~