github
地址:JXBWKWebView,若是以为项目不错能够点个star支持一下,谢谢~前端
前言
目前iOS系统已经更新到iOS11,大多数项目向下兼容最多兼容到iOS8,所以,在项目中对WebView组件进行重构再封装时,打算直接舍弃UIWebView
转用WKWebView
。java
若是你目前正在网上浏览关于WKWebView
的一些文章,相信你已经清楚了WKWebView
的优势,也目击了你们在使用WKWebView
的过程当中遇到的坑,而这篇文章,会对到目前为止你们遇到的关于WKWebView
的问题给出详细的解决方案,文章的最后,也会讲述关于对WKWebView
进行性能优化的方案。git
解决的问题
-
goback
返回页面不刷新 Cookie
-
POST
请求失效 crash
navigationBackItem
- 进度条
-
Native
与JS
的交互 - 优化
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
。而后在根据true
或false
在执行相应的页面刷新动做或者直接ajax
请求接口更新数据。json
关于onload
和onpageshow
事件在safari
和chrome
上的区别以下:api
. | 事件 | Chrome | Safari |
---|---|---|---|
第一次加载页面 | onload | 触发 | 触发 |
第一次加载页面 | onpageshow | 触发 | 触发 |
从其余页面返回 | onload | 触发 | 不触发 |
从其余页面返回 | onpageshow | 触发 | 触发 |
关于cookie
WKWebView
属于webkit
框架,其将浏览器内核渲染进程提取出 App
主进程,由另一个进程进行管理,减小了至关一部分的性能损失,这也是性能上比UIWebView
优越的缘由之一。跨域
既然WKWebView
的工做进程独立于App Process
以外,咱们暂且称为WK Process
(随便起的)。
在使用AFN
进行网络请求时,若是server
使用set-cookie
将cookie
写入header
,AFN
接受到响应后会将cookie
保存到NSHTTPCookieStorage
,下次若是是同域的request url
,AFN
会将cookie
从NSHTTPCookieStorage
中取出而后做为request header
的cookie
发送给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 header
的set-cookie
:
NSString *cookieString = [[response allHeaderFields] valueForKey:@"Set-Cookie"];
那么,WKWebView
将cookie
存入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
请求(能够理解域名发生变化,也就是说不一样域)
,上面的解决方案就用不了了,这时就须要你在WKWebView
的decidePolicyForNavigationAction
代理方法中拦截URL
,判断当前URL
与初次请求的URL
是否同域,若是不一样域,在该代理方法中获取到当前请求的request
对象并copy
出一个新的对象,经过addValue:forHeaderField:
方法将cookie
手动添加到header
中,而后让WKWebView
使用loadRequest
从新加载这个copy
出来的新的request
对象。
问题就没了吗?NO
,上面的解决方法一样有局限,即只能解决后续的同域ajax
请求不加cookie
的问题。若是发生iframe
跨域请求,咱们拦截不到请求,因此也无法给请求的header
手动添加cookie
,WKWebView
只适合加载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
,只不过Apple
把http
和https
这两个scheme
给过滤掉了,致使咱们拦截不到WKWebView
发送的网路请求。
所以,在咱们自定义NSURLProtocol
时,要经过使用私有api
来注册一些scheme
,注册scheme
的类名叫WKBrowsingContextController
,WKWebView
中有一个属性叫browsingContextController
,就是这个类的对象。注册的方法叫registerSchemeForCustomProtocol:
,知道这个私有api
,咱们就能够经过target-action
的方式,注册WKWebView
发起网络请求时须要拦截的URL scheme
,此时注册的scheme
至少要包括3种,分别是http
、https
、post
。
问题还没玩,解决一个问题的同时每每伴随另外一个问题的产生。
使用这种方案拦截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
不会在进程间通讯发送http
的body
。
由于WKWebView
属于webkit
框架,所以WKWebView
的网络请求、内容加载/渲染都是在WK Process
中进行,但NSURLProtocol
拦截请求还在App Process
,一旦注册http(s) scheme
后,网络请求将从独立进程中发送到App Process
,这样自定义的NSURLProtocol
才能拦截到网络请求,为了提高进程间通讯效率,出于性能上的考虑,Apple
会将request
的body
数据丢弃,由于body
数据(二进制类型)大小没有限制,size
偏大的话就会对数据传输效率有严重影响进而影响到拦截请求时的操做及延时后续的网络请求,所以,Apple
在进行进程间通讯时会把post
请求的body
丢弃。
如何解决?
终极思路就是虽然http
的body
会在进程间通讯时被丢弃,但header
不会。
所以,解决问题步骤以下:
-
WKWebView
在loadRequest
前对request
对象进行一些处理,这个request
对象咱们记为old request
。
1.记下old request
的scheme
和NSData
类型的http body
。
2.获取当前old request
的URL
,替换URL
的scheme
为post
(这也是咱们为何要在前面使用NSURLProtocol
注册post scheme
的缘由),并根据这个替换好的URL
从新生成一个新的NSMutableURLRequest
对象,这个对象记为new request
。
3.给new request
的header
赋值,把步骤1中获取的scheme
和http body
手动添加到这个new request
的header
中,若是这个post
请求须要附带cookie
的话,你也要把cookie
从old request
中拿出来放到new request
的header
中。
4.让WKWebView
加载这个new request
。
-
WKWebView
发送新的request
时(这个request url
的scheme
是post
),咱们能够在自定义NSURLProtocol
中拦截到这个请求,执行以下步骤:
1.替换scheme
,此时的scheme
是post
,你须要把post scheme
替换成old request
的scheme
,这个字段咱们以前已经保存下来了。
2.替换scheme
后会生成一个新的URL
,根据这个新的URL
生成一个NSURLMutableRequest
对象,将以前保存的http body
、cookie
放到这个新的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
所在的UIViewController
的push
或present
的动画还没有结束,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
系统,能够经过在completionHandler
里retain WKWebView
防止completionHandler
被过早释放。
解决方案是使用method swizzling hook
了这个系统方法,在回调中对self
进行了强引用来保证在执行completionHandler
的时候self
还在。
navigationBackItem
实现导航栏back item
的方式有两种。
- 自定义导航栏
这个比较简单,根据WebView
是否能够goback
决定navigationBarButtonItems
的个数和功能。
- 使用系统默认的导航返回按钮,相似于微信
难点在于咱们要获取到点击系统导航返回按钮时的事件,而后进行一些处理。
点击返回按钮时,实际上调用了UINavigationController
的navigationBar:shouldPopItem
方法,咱们可使用method swizzling hook
住这个方法,在这个方法中经过调用代理方法的方式告诉WKWebView
所在的UIViewController
进行相应的处理。
UIProgressView
这个简单,也很少说了。
Native与JS的交互
- 拦截URL
在WKWebView
的decidePolicyForNavigationAction
代理方法中可对URL
进行拦截,通常使用拦截URL
的方式URL
的格式以下:
scheme://host?paramKey=paramValue
通常状况下scheme
对应业务,host
是业务对应的服务(method
),?
后面就是参数。
使用拦截URL
的交互方式时,业务逻辑不复杂状况下,JS
调用Native
没什么问题,但当业务逻辑复杂时,JS
须要拿到Native
处理好的回调数据的话,处理起来将十分麻烦。
而且使用拦截URL
的交互方式,不利于从此JS
与Native
的业务拓展。
- 使用
Bridge
WKWebView
对JS
与Native
经过Bridge
交互提供了很是好的支持,咱们能够经过ScriptMessageHandler
来达成各类交互的目的。使用ScriptMessageHandler
添加脚本的具体代码在此很少赘述,你们可自行研究。重点说一下Bridge
的脚本代码。
如今关于Bridge
的开源解决方案有不少,但基本都遵循一个模式,在注入的Bridge
脚本代码中,定义好供JS
调用的方法名称,该方法一般包括以下几个参数:
1.要调用的native
业务模块名称(有些有,有些没有,若是项目中实施模块化建议加上)。
2.要调用的native
服务名称(一般是方法名)。
3.传递给native
的参数(也就是方法须要的参数)。
4.callback
,JS
调用native
的方法后脚本须要调用的回调。
详细来描述一下使用Bridge
整个交互过程,从建立Bridge
脚本到Bridge
脚本执行callback
:Bridge
脚本下称脚本。
1.脚本为JS
提供JavaScript
语言的方法,该方法用来调用native
方法,方法的4个参数如前所述。
2.在该方法中,会根据前述的部分参数生成一个惟一标识符,记为identifier
。
3.在脚本中给全局对象(window
)绑定一个字典属性,key
是步骤2中的identifier
,value
是callback
。
4.调用messagehandler
的postMessage
函数,将前述的参数和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
封装了WKWebView
的evaluateJavaScript
操做,这个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
)传递过去,目标对象将任务处理完成后,调用param
的success block, failure block, progress block
,将任务处理的结果回传给JS
。
- 交互总结
不管是拦截URL
仍是使用Bridge
,最后调用native
方法的机制都是利用target-action
,使用target-action
机制的缘由之一就是可减小类与类之间的耦合程度,减小硬编码的同时有利于从此的业务扩展。
固然,若是你不喜欢target-action
的方案,也能够自行扩展。
拦截WKWebView的网络请求
经过观看WebKit
的源码能够了解到WKWebView
是支持拦截网络请求的,可是WebKit
没有注册须要拦截的scheme
,因此咱们只能进行手动注册了。
手动注册须要调用WKWebView
的私有api
,注册scheme
的私有api
是registerSchemeForCustomProtocol:
,注销的私有api
是unregisterSchemeForCustomProtocol:
,有些同窗会考虑到在项目中使用私有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
页面彻底展现给用户所须要的时间远比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
就能够理解为是在线打开的。
把一个H5
的HTML/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.提供native
与H5
的交互解决方案,支持自定义MessageHandler
操做。
11.提供H5
秒开解决方案,server
使用Go
实现。
12.iOS
和Android
为JS
提供统一的原生调用方式。
github
地址:JXBWKWebView,若是以为项目不错能够点个star支持一下,谢谢~