WKWebView使用指南|功能丰富的JXBWKWebView

github地址:JXBWKWebView,若是以为项目不错能够点个star支持一下,谢谢~前端

前言


目前iOS系统已经更新到iOS11,大多数项目向下兼容最多兼容到iOS8,所以,在项目中对WebView组件进行重构再封装时,打算直接舍弃UIWebView转用WKWebViewjava

若是你目前正在网上浏览关于WKWebView的一些文章,相信你已经清楚了WKWebView的优势,也目击了你们在使用WKWebView的过程当中遇到的坑,而这篇文章,会对到目前为止你们遇到的关于WKWebView的问题给出详细的解决方案,文章的最后,也会讲述关于对WKWebView进行性能优化的方案。git

解决的问题


  • goback返回页面不刷新
  • Cookie
  • POST请求失效
  • crash
  • navigationBackItem
  • 进度条
  • NativeJS的交互
  • 优化H5页面启动速度

入坑


goback Api返回不刷新

在以前使用UIWebView时,调用goback后,页面会刷新。使用WKWebView后,调用goback,即使调用reload方法,H5依然不会刷新。github

缘由是调用goback时,UIWebView会触发onload事件,WKWebView不会触发onload事件,若是前端依旧在onload事件中处理iOS的页面返回事件,是处理不了的,解决方案是让前端使用onpageshow事件监听WKWebView的页面goback事件。web

前端代码以下:ajax

window.addEventListener("pageshow", function(event){
    if(event.persisted){
        location.reload();
    }
});

为了查看页面是直接从服务器上载入仍是从缓存中读取,可使用 PageTransitionEvent对象的persisted属性来判断。chrome

若是页面从浏览器的缓存中读取该属性返回ture,不然返回 false。而后在根据truefalse在执行相应的页面刷新动做或者直接ajax请求接口更新数据。json

关于onloadonpageshow事件在safarichrome上的区别以下:api

. 事件 Chrome Safari
第一次加载页面 onload 触发 触发
第一次加载页面 onpageshow 触发 触发
从其余页面返回 onload 触发 不触发
从其余页面返回 onpageshow 触发 触发
关于cookie

WKWebView属于webkit框架,其将浏览器内核渲染进程提取出 App主进程,由另一个进程进行管理,减小了至关一部分的性能损失,这也是性能上比UIWebView优越的缘由之一。跨域

既然WKWebView的工做进程独立于App Process以外,咱们暂且称为WK Process(随便起的)。

在使用AFN进行网络请求时,若是server使用set-cookiecookie写入headerAFN接受到响应后会将cookie保存到NSHTTPCookieStorage,下次若是是同域的request urlAFN会将cookieNSHTTPCookieStorage 中取出而后做为request headercookie发送给server端,而这一切发生在App Process

那么在WK Process工做的WKWebView在发送网络请求及收到响应后对cookie的处理是否也会使用NSHTTPCookieStorage 呢,通过测试后,答案是yes,但在存取的过程当中会有一些问题须要注意。

先说存:

测试进行:iphone 6p iOS:10
测试过程:
1.client使用AFN发送一个网络请求
2.server接收到请求后,使用set-cookie写入cookie
3.client接收到success response后,使用以下方式输出log

NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:fields forURL:url];
for (NSHTTPCookie *cookie in cookies) {
    NSLog(@"cookie,name:= %@,valuie = %@",cookie.name,cookie.value);
}

4.进入WKWebView所在页面,使用loadRequest随便发送一个同域的网络请求,在decidePolicyForNavigationResponse代理方法中,使用以下代码输出log

NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response;
NSArray *cookies =[NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:response.URL];
for (NSHTTPCookie *cookie in cookies) {
    NSLog(@"wkwebview中的cookie:%@", cookie);
}

也可使用以下代码输出该请求的server response headerset-cookie

NSString *cookieString = [[response allHeaderFields] valueForKey:@"Set-Cookie"];

那么,WKWebViewcookie存入NSHTTPCookieStorage 的时机是何时?
1.JS执行document.cookie或服务器set-cookie注入的Cookie会很快同步到NSHTTPCookieStorage中。
2.H5页面进行跳转时会将Cookie同步到NSHTTPCookieStorage中。
3.控制器页面跳转时会将Cookie同步到NSHTTPCookieStorage中。

再说取:

WKWebView使用loadRequest发送网络时不会主动将cookie存入到NSHTTPCookieStorage 中,即便是同域的请求。

因此,若是你有一个请求须要附带cookie,就不能直接加载URL,须要你根据URL建立一个URLMutableRequest对象,将须要附加的cookie使用addValue:forHTTPHeaderField:方法手动将cookie添加到request header中,但这仅能解决首次请求不带cookie的问题,若是页面发送ajax请求,cookie一样带不上,解决方案是经过document.cookie设置cookie,也就是说在你实例化WKWebView时就应该注入相关script

上面咱们说的都是在同域的状况下,若是发生302请求(能够理解域名发生变化,也就是说不一样域),上面的解决方案就用不了了,这时就须要你在WKWebViewdecidePolicyForNavigationAction代理方法中拦截URL,判断当前URL与初次请求的URL是否同域,若是不一样域,在该代理方法中获取到当前请求的request对象并copy出一个新的对象,经过addValue:forHeaderField:方法将cookie手动添加到header中,而后让WKWebView使用loadRequest从新加载这个copy出来的新的request对象。

问题就没了吗?NO,上面的解决方法一样有局限,即只能解决后续的同域ajax请求不加cookie的问题。若是发生iframe跨域请求,咱们拦截不到请求,因此也无法给请求的header手动添加cookieWKWebView只适合加载mainFrame 请求。

因此,要和前端同窗提早打好招呼,尽可能避免使用iframe,能使用ajax的地方尽可能使用ajax,另外一方面,iframe如今已经不怎么提倡使用了,除非是解决一些特殊的问题。

POST请求

使用WKWebView没法正常发送POST请求。

因此,这个时候咱们须要经过自定义NSURLProtocol拦截WKWebView的网络请求,而且,使用NSURLProtocol拦截WKWebView网络请求的好处还有就是:
1.若是产品需求要求client须要日志采集,包括全部的网络请求记录,经过这种方式你是能够获取到的。
2.若是公司对用户体验的要求较高,能够在这里实现WKWebView初始化和相关网络请求的并发执行,以缩短用户在client打开H5的速度,甚至能够秒开,达到和native相同的体验。

但问题是正常状况下NSURLProtocol是拦截不到WKWebView的网络请求的。

经过观看webkit的源码(github直接搜webkit)能够获得的结果是,经过WKWebView发送一个网络请求其实也会走NSURLProtocol,只不过Applehttphttps这两个scheme给过滤掉了,致使咱们拦截不到WKWebView发送的网路请求。

所以,在咱们自定义NSURLProtocol时,要经过使用私有api来注册一些scheme,注册scheme的类名叫WKBrowsingContextController WKWebView中有一个属性叫browsingContextController,就是这个类的对象。注册的方法叫registerSchemeForCustomProtocol:,知道这个私有api,咱们就能够经过target-action的方式,注册WKWebView发起网络请求时须要拦截的URL scheme,此时注册的scheme至少要包括3种,分别是httphttpspost

问题还没玩,解决一个问题的同时每每伴随另外一个问题的产生。

使用这种方案拦截WKWebView的网络请求形成的问题就是post请求body数据被清空,仍是Apple所为,看webkit源码:

void ArgumentCoder<ResourceRequest>::encodePlatformData(Encoder& encoder, const ResourceRequest& resourceRequest)
{
    RetainPtr<CFURLRequestRef> requestToSerialize = resourceRequest.cfURLRequest(DoNotUpdateHTTPBody);

    bool requestIsPresent = requestToSerialize;
    encoder << requestIsPresent;

    if (!requestIsPresent)
        return;

    // We don't send HTTP body over IPC for better performance.
    // Also, it's not always possible to do, as streams can only be created in process that does networking.
    RetainPtr<CFDataRef> requestHTTPBody = adoptCF(CFURLRequestCopyHTTPRequestBody(requestToSerialize.get()));
    RetainPtr<CFReadStreamRef> requestHTTPBodyStream = adoptCF(CFURLRequestCopyHTTPRequestBodyStream(requestToSerialize.get()));
    if (requestHTTPBody || requestHTTPBodyStream) {
        CFMutableURLRequestRef mutableRequest = CFURLRequestCreateMutableCopy(0, requestToSerialize.get());
        requestToSerialize = adoptCF(mutableRequest);
        CFURLRequestSetHTTPRequestBody(mutableRequest, nil);
        CFURLRequestSetHTTPRequestBodyStream(mutableRequest, nil);
    }

    RetainPtr<CFDictionaryRef> dictionary = adoptCF(WKCFURLRequestCreateSerializableRepresentation(requestToSerialize.get(), IPC::tokenNullTypeRef()));
    IPC::encode(encoder, dictionary.get());

    // The fallback array is part of CFURLRequest, but it is not encoded by WKCFURLRequestCreateSerializableRepresentation.
    encoder << resourceRequest.responseContentDispositionEncodingFallbackArray();
    encoder.encodeEnum(resourceRequest.requester());
}

主要看代码中间那两句注释,大体的意思就是Apple不会在进程间通讯发送httpbody

由于WKWebView属于webkit框架,所以WKWebView的网络请求、内容加载/渲染都是在WK Process中进行,但NSURLProtocol拦截请求还在App Process,一旦注册http(s) scheme后,网络请求将从独立进程中发送到App Process,这样自定义的NSURLProtocol才能拦截到网络请求,为了提高进程间通讯效率,出于性能上的考虑,Apple会将requestbody数据丢弃,由于body数据(二进制类型)大小没有限制,size偏大的话就会对数据传输效率有严重影响进而影响到拦截请求时的操做及延时后续的网络请求,所以,Apple在进行进程间通讯时会把post请求的body丢弃。

如何解决?
终极思路就是虽然httpbody会在进程间通讯时被丢弃,但header不会。

所以,解决问题步骤以下:

  • WKWebViewloadRequest前对request对象进行一些处理,这个request对象咱们记为old request

1.记下old requestschemeNSData类型的http body
2.获取当前old requestURL,替换URLschemepost(这也是咱们为何要在前面使用NSURLProtocol注册post scheme的缘由),并根据这个替换好的URL从新生成一个新的NSMutableURLRequest对象,这个对象记为new request
3.给new requestheader赋值,把步骤1中获取的schemehttp body手动添加到这个new requestheader中,若是这个post请求须要附带cookie的话,你也要把cookieold request中拿出来放到new requestheader中。
4.让WKWebView加载这个new request

  • WKWebView发送新的request时(这个request urlschemepost),咱们能够在自定义NSURLProtocol中拦截到这个请求,执行以下步骤:

1.替换scheme,此时的schemepost,你须要把post scheme替换成old requestscheme,这个字段咱们以前已经保存下来了。
2.替换scheme后会生成一个新的URL,根据这个新的URL生成一个NSURLMutableRequest对象,将以前保存的http bodycookie放到这个新的request对象的header中。
3.使用NSURLSession,根据新的request对象发送网络请求,而后经过NSURLProtocol Client将加载结果返回给WKWebView

注意:在这几个步骤中一共产生了3个request对象。

crash

1.alert弹窗
引发crash的缘由是js调用alert()引发的,也就是说,当WKWebView销毁的时候,JS恰好执行了alert(),原生的 alert 弹窗可能弹不出来,completionHandler回调最后没有被执行,致使crash;另外一种状况是在WKWebView刚打开,JS就执行alert(),这个时候因为 WKWebView所在的UIViewControllerpushpresent的动画还没有结束,alert框可能弹不出来,completionHandler最后没有被执行,致使crash

解决方案:获取当前window上最终的UIViewController,判断UIViewController是否未被销毁、UIViewController是否已经加载完成、动画是否执行完毕。

2.另外一个crash发生在WKWebView退出前调用:
执行JS代码的状况下。WKWebView 退出并被释放后致使completionHandler变成野指针,而此时 javaScript Core 还在执行JS代码,待 javaScript Core 执行完毕后会调用completionHandler(),致使crash。这个crash只发生在iOS 8 系统上,参考Apple Open Source,在iOS9及之后系统苹果已经修复了这个bug,主要是对completionHandler block作了copy(refer: https://trac.webkit.org/changeset/179160);对于iOS 8系统,能够经过在completionHandlerretain WKWebView防止completionHandler被过早释放。

解决方案是使用method swizzling hook了这个系统方法,在回调中对self进行了强引用来保证在执行completionHandler的时候self还在。

navigationBackItem

实现导航栏back item的方式有两种。

  • 自定义导航栏

这个比较简单,根据WebView是否能够goback决定navigationBarButtonItems的个数和功能。

  • 使用系统默认的导航返回按钮,相似于微信

难点在于咱们要获取到点击系统导航返回按钮时的事件,而后进行一些处理。

点击返回按钮时,实际上调用了UINavigationControllernavigationBar:shouldPopItem方法,咱们可使用method swizzling hook住这个方法,在这个方法中经过调用代理方法的方式告诉WKWebView所在的UIViewController进行相应的处理。

UIProgressView

这个简单,也很少说了。

Native与JS的交互
  • 拦截URL

WKWebViewdecidePolicyForNavigationAction代理方法中可对URL进行拦截,通常使用拦截URL的方式URL的格式以下:

scheme://host?paramKey=paramValue

通常状况下scheme对应业务,host是业务对应的服务(method),?后面就是参数。

使用拦截URL的交互方式时,业务逻辑不复杂状况下,JS调用Native没什么问题,但当业务逻辑复杂时,JS须要拿到Native处理好的回调数据的话,处理起来将十分麻烦。

而且使用拦截URL的交互方式,不利于从此JSNative的业务拓展。

  • 使用Bridge

WKWebViewJSNative经过Bridge交互提供了很是好的支持,咱们能够经过ScriptMessageHandler来达成各类交互的目的。使用ScriptMessageHandler添加脚本的具体代码在此很少赘述,你们可自行研究。重点说一下Bridge的脚本代码。

如今关于Bridge的开源解决方案有不少,但基本都遵循一个模式,在注入的Bridge脚本代码中,定义好供JS调用的方法名称,该方法一般包括以下几个参数:
1.要调用的native业务模块名称(有些有,有些没有,若是项目中实施模块化建议加上)。
2.要调用的native服务名称(一般是方法名)。
3.传递给native的参数(也就是方法须要的参数)。
4.callbackJS调用native的方法后脚本须要调用的回调。

详细来描述一下使用Bridge整个交互过程,从建立Bridge脚本到Bridge脚本执行callback
Bridge脚本下称脚本。
1.脚本为JS提供JavaScript语言的方法,该方法用来调用native方法,方法的4个参数如前所述。
2.在该方法中,会根据前述的部分参数生成一个惟一标识符,记为identifier
3.在脚本中给全局对象(window)绑定一个字典属性,key是步骤2中的identifiervaluecallback
4.调用messagehandlerpostMessage函数,将前述的参数和identifier 都发送给native(没发callback,callback的做用主要就是步骤3)。
5.前端调用你的脚本中的代码调用native的方法,具体代码可参见Apple官方文档。
5.native在自定义的MessageHandler对象的userContentController:didReceiveScriptMessage:代理方法中接收到JS传过来的参数(记为param)。获取到了模块名称、服务名称、参数、identifier等,额外的,须要建立几个block,对应JS那边的callback,好比JS那边有个success callback,那么在native就要有一个success block,而建立的这些block,咱们会赋值给前面说的那个param里面,那么如今,这个param有以下几个值:

targetName(模块名称)
actionName(服务名称)
identifier(经过该属性最后咱们能够找到js的callback)
success block
failure block
progress block
上面这些参数基本上已经够了,若是须要扩展就本身加吧

那么这些block里面的操做主要是什么呢?block封装了WKWebViewevaluateJavaScript操做,这个block最后能够拿到native处理任务后的结果和identifier,而后把结果转换为json数据,经过identifier找到JS那边的callback,而后把结果的json数据做为callback的参数回传给JS那边。代码以下:

NSString *resultDataString = [self jsonStringWithData:resultDictionary];
    
NSString *callbackString = [NSString stringWithFormat:@"window.Callback('%@', '%@', '%@')", identifier, result, resultDataString];

[message.webView evaluateJavaScript:callbackString completionHandler:nil];

6.利用target-action机制,根据targetName实例化对象,根据actionName调用方法,并把参数(param)传递过去,目标对象将任务处理完成后,调用paramsuccess block, failure block, progress block,将任务处理的结果回传给JS

  • 交互总结

不管是拦截URL仍是使用Bridge,最后调用native方法的机制都是利用target-action,使用target-action机制的缘由之一就是可减小类与类之间的耦合程度,减小硬编码的同时有利于从此的业务扩展。

固然,若是你不喜欢target-action的方案,也能够自行扩展。

拦截WKWebView的网络请求

经过观看WebKit的源码能够了解到WKWebView是支持拦截网络请求的,可是WebKit没有注册须要拦截的scheme,因此咱们只能进行手动注册了。

手动注册须要调用WKWebView的私有api,注册scheme的私有apiregisterSchemeForCustomProtocol:,注销的私有apiunregisterSchemeForCustomProtocol:,有些同窗会考虑到在项目中使用私有api在审核时会被苹果爸爸打回,我这里测试不会,若是你遇到了被打回的状况,能够把私有api拆分红多个字符串,而后把多个字符串拼接在一块儿。

因此拦截WKWebView网络请求的步骤是:
(1)自定义NSURLProtocol,用来处理拦截到的网络请求。
(2)利用系统提供的NSURLProtocol注册(1)中自定义的NSURLProtocol
(3)经过私有api注册须要拦截的网络请求的scheme
(4)在合适的时机注销(3)中注册的scheme

H5启动性能优化

H5最让人诟病的一点就是它的用户体验没有native好,其实H5的交互效果(不包括复杂的动效)已经很是接近于native了,因此剩下的缺点整体来讲就是关于WebView的渲染问题,咱们在写native界面的时候,页面一打开就能看到咱们建立的UI元素,可是远程的H5不能,由于远程H5的页面元素都须要去服务器获取,随后通过渲染才能展现,过程大体以下:

H5启动流程

因此,一个H5页面彻底展现给用户所须要的时间远比native页面长的多。

因此针对于移动端来讲,优化H5启动性能的点主要有两个:
(1)优化WebView的启动速度
(2)让HTML/CSS/JavaScript文件下载的更快一些,也就是离线包方案。

(1)优化WebView的启动速度

App打开的时候并不会初始化浏览器内核,当咱们建立一个WKWebView的时候,系统才会初始化浏览器内核,也就是说,当咱们第一次用WebView打开H5的时候,H5的显示时间须要加上浏览器内核启动时间,因此优化点就在于优化浏览器内核启动时间。

不少解决方案是初始化一个单例WebView,让这一个WebView全局可用,这样打开每一个H5的时候用的都是同一个WebView对象,工做原理有点接近PC端浏览器,这样作的缺点就是若是这个WebView由于某些缘由致使异常终止以后,再用这个WebView打开H5可能会产生一些意料以外的问题,因此,这里推荐使用另一种解决方案。

另一种解决方案就是维护一个全局的WebView复用池,复用原理同UITableViewCell同样,这里不细讲。若是一个WebView一直是正常工做的就放入复用池中,若是一个WebView由于某些缘由异常终止,那么就把这个WebView从复用池中移除。

不管是哪一种复用方案,都会产生一个新问题,当咱们利用复用WebView打开一个新H5的时候,浏览器的浏览历史记录里还保留着上一次打开的H5的痕迹,因此,咱们须要在复用时清除这个痕迹并让页面打开一个空白页。

(2)使用离线包打包H5的静态资源。

咱们经过一个远程URL打开H5就能够理解为是在线打开的。

把一个H5HTML/CSS/JavaScript文件分别打包成静态资源文件保存在服务器,这些保存在服务器的静态资源文件就能够理解为是离线包,移动端能够选择一个合适的时机下载离线包,而后在本地解压缩,当咱们打开一个H5的时候其实打开的是已经下载到本地的HTML文件,免去了在线拉取资源的过程,从而节省了时间。

H5页面须要更新的时候,直接对离线包作增量更新能够了。

更多细节可参考bang的这篇文章

基于WKWebView封装的JXBWKWebView


1.内核决定了goback返回不刷新问题须要前端支持
2.支持natigationBackItem & navigationLeftItems
3.支持自定义rightBarButtonItem
4.支持进度条
5.提供cookie解决方案,首次本身加,后续的ajax请求自动加,302请求自动加
6.支持拦截WKWebView拦截网络请求
7.支持POST请求
8.支持子类继承
9.支持拦截URL的交互方式,支持自定义拦截URL操做。
10.提供nativeH5的交互解决方案,支持自定义MessageHandler操做。
11.提供H5秒开解决方案,server使用Go实现。
12.iOSAndroidJS提供统一的原生调用方式。

github地址:JXBWKWebView,若是以为项目不错能够点个star支持一下,谢谢~

相关文章
相关标签/搜索