最近因Apple的要求:App需要将UIWebview换成WKWebview,经过一个月的开发和测试,重要上线了! 下文重点记录下遇到一些问题.
一 UserAgent的设置
项目中UIWebview设置UA是全局设置的,还要防止UA被篡改,调研很多关于WK设置UA的文章,大多数都是webveiw loadRequest时设置UA,没有达到预期,考虑到用WK设置UA风险高,一旦UA出了问题就彻底晾凉了! UA继续用UIWebview来设置;小心驶得万年船!
二 Cookie的写入
本次改造大部分时间花在了Cookie的问题上,WK作为多进程组件其实是对UIWebveiw的拆分和组装,新增了一些个人比较喜欢的功能:
1 estimatedProgress:这个属性帮我们实现界面加载进度条,告别了仿真进度条的尴尬,WK上的一些原生特殊元素添加也变得游刃有余了.
2 title: 获取界面title,好多界面title是异步请求,在UIWebview无能为力,只好拿着定时器碰运气,WK
3 backForwardList:在某些特定场景下需要返回H5界面的首页,在UIWebview中无法通过API实现的,可以通过JS实现:
[self.webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"if( window.history.length > 1 ) { window.history.go( -( window.history.length - 1 ) ) };"]];
这年头不会点JavaScript寸步难行,不好意思说开发过hybrid App.
有喜有忧,WKWebView实例不会把Cookie存入到App标准的Cookie容器(NSHTTPCookieStorage
)中,需要开发者手动存入,虽然在iOS11Apple在Webkit框架中新增了WKHTTPCookieStore
,可以实现WK Cookie同步;但App需要支持iOS9以上版本,采用了head中写入Cookie,document.cookie
中注入Cookie方式实现.
记录下遇到的问题
(1) 跨域情况.
1.1 服务端不设置Domian
的情况
1.2 本地写Cookie的情况
NSMutableDictionary *cookiePropertiesZtappcliver = [NSMutableDictionary dictionary];
[cookiePropertiesZtappcliver setObject:@"ztappcliver" forKey:NSHTTPCookieName];
NSString *version = [UserLoginHelper sharedInstance].systemVersion;
version = [NSString stringWithFormat:@"ios@%@",version];
[cookiePropertiesZtappcliver setObject:SAFE_STRING(version) forKey:NSHTTPCookieValue];
//为空字符串所有的域名都可以访问 不写NSHTTPCookieDomain 域名没法访问.
[cookiePropertiesZtappcliver setObject:@"" forKey:NSHTTPCookieDomain];
[cookiePropertiesZtappcliver setObject:@"/" forKey:NSHTTPCookiePath];
[cookieArray addObject:cookiePropertiesZtappcliver];
(2) cookie会出现重复的情况
登录时存入WKHTTPCookieStore
容器中cookie和服务端重新写入cookie名称相同时就会出现重复的情况,解决办法是覆盖WKHTTPCookieStore
容器中cookie,具体在 - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler
中去重.
方法为:
if ([GlobalMembers IsArraySafe:cookies]) {
for (NSHTTPCookie *cookie in cookies) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (NSHTTPCookie *ex_cookie in [NSHTTPCookieStorage sharedHTTPCookieStorage].cookies) {
// 响应cookie 如果和客户端请求cookie重复 则
if ([ex_cookie.name isEqualToString:cookie.name]) {
DLog(@"%@ cookie========",cookie.name);
[[NSHTTPCookieStorage sharedHTTPCookieStorage]deleteCookie:ex_cookie];
}
}
});
[[NSHTTPCookieStorage sharedHTTPCookieStorage]setCookie:cookie];
}
}
WK 真的不能同步WKHTTPCookieStore
中的cookie吗Σ(⊙⊙"a ?
可以同步,通过这个例子可以看出,WKWebView 实例其实也会将 Cookie 存储于 NSHTTPCookieStorage 中,但存储时机有延迟,因为WKWebView内也有cookie的容器,而且这个同步是进程级别的同步,而且这个同步是单向.
(3) 删除cookie和WK缓存清理问题
客户端退出登录时会清空Cookie,连带将WK的缓存清空,测试阶段并没有发现问题,后期测试发现用户首次登录某些4g网络下业务无法单点登录,通过和早期支持UIWebiew版本对比得出结论:WK缓存清空导致的,关于WK缓存问题,可以拜读https://blog.csdn.net/u012413955/article/details/79783282
(4) cookie因某种原因丢失的情况
一般是重定向或者iOS9以下系统会出现.
(5) iOS13上无法摇一摇的情况
在iOS13上Apple对于浏览器访问设备运动和方向事件在默认情况下处于关闭状态.
前端解决方案如下:
image.png
警告:Call to requestPermission() failed, reason: Browsing context is not secure 访问链接改成https即可。
(5) evaluateJavaScript
:WK调用JS方法,completionHandler
返回值的id类型。
JS方法:
image.png
completionHandler
:返回的值是个NSCFBoolean
,切记不能当作BOOL值强转处理,因为NSCFBoolean本身是个NSNumber对象。
(6)Webkit:JScore深入理解
1 从iOS7之后,JScore作为系统的Framework被苹果提供个开发者,Webkit
作为iOS的页面渲染及逻辑处理引擎,HTML5通过WebKit处理后就可以显示界面,WebKit
框架中主要包括:WebCore
,JScore
,WebKit Embedding API
,WebKit Ports
。
2 WebKit Embedding API
:负责浏览器和UI交互。
3 WebKit Ports
:让WebKit
方便移植到各个操作系统上。
4 WebCore
:是WebKit
中最核心的渲染引擎。
5 JScore
: 是WebKit
的JS默认内嵌引擎。
5.1 JSCore
采用基于寄存器的指令结构,寄存器指令 执行效率更高。但是由于这样的架构也造成内存开销更大的问题,还存在移植性弱的问题,因为虚拟机中的虚拟寄存器需要去匹配到真实机器中CPU的寄存器,可能会存在真实CPU寄存器不足的问题。
5.2 JSCore
单线程机制,整个js在一条线程中执行。
5.3 JSCore
事件驱动机制。
5.4 WKWebview
中,也封装了JSCore
,但无法使用系统的JSCore Framework
;下面简单学习记录下JSCore Framework
的几个重要概念。
5.4.1 JSVM
一个JSVirtualMachine(以下简称JSVM)实例代表了一个自包含的JS运行环境,或者是一系列JS运行所需的资源。该类有两个主要的使用用途:一是支持并发的JS调用,二是管理JS和Native之间桥对象的内存.
JSCore
被认为是一个虚拟机,那JSVM又是什么?实际上,JSVM就是一个抽象的JS虚拟机,让开发者可以直接操作。在App中,我们可以运行多个JSVM来执行不同的任务。而且每一个JSContext都从属于一个JSVM。但是需要注意的是每个JSVM都有自己独立的堆空间,GC
也只能处理JSVM内部的对象。所以说,不同的JSVM之间是无法传递值的。
5.4.2 JSContext
__weak typeof (UIWebView *)weakWebView = webView;
JSContext *Context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
//weview 持有JSContext 这个会使得引用系数+1
Context[BridgeObject] = ^(){};
在UIWebview中通过KVC获取JSContext
,然后调用JS,访问JS的值和函数,提供JS访问Native的接口。
Objective-C type | JavaScript type
--------------------+---------------------
nil | undefined
NSNull | null
NSString | string
NSNumber | number, boolean
NSDictionary | Object object
NSArray | Array object
NSDate | Date object
NSBlock | Function object
id | Wrapper object
Class | Constructor object
5.4.3 JSExport
(7) WK下键盘弹起,无法输入问题,UIWebview正常 如图:
111111.gif前端代码
image.png
App端的原因:
通过排查,发现客户端WK协议中实现了[self evaluateJavaScript:@"document.documentElement.style.webkitTouchCallout='none';" completionHandler:nil]; [self evaluateJavaScript:@"document.documentElement.style.webkitUserSelect='none';" completionHandler:nil];
这个两个限制是为了防止用户长按弹出一些系统弹窗,强迫症把它去掉了,没想到影响这个键盘弹起输入,按理说长按和键盘输入八竿子打不着啊 ,怎么这样呢,继续入坑查.....
-webkit-touch-callout
:当触摸并按住触摸目标时候是否弹出系统默认弹窗,默认值为inherit
不弹出, 当值为none
静止展示系统默认弹窗.
document.documentElement.style.webkitUserSelect='none';
:在WK中屏蔽长按弹出默认的UIMenuController
真机debug调试界面得出结论:document.documentElement.style.webkitUserSelect='none';
影响了html的contenteditable
属性,导致键盘无法输入.
(8) WK下键盘弹起,报错日志
当键盘弹出时,Xcode提示(WKWebView constrains issue when keyboard pops up),未解决.
(9) didFailNavigation
和 didFailProvisionalNavigation
的区别
这两个方法容易被混淆,本人居然一直使用didFailNavigation
监听webveiw请求失败的操作,而且上线运行了2个版本,啊啊啊!!!
/*! @abstract Invoked when an error occurs during a committed main frame(在提交的主框架期间发生错误时调用)
navigation.
@param webView The web view invoking the delegate method.
@param navigation The navigation.
@param error The error that occurred.
*/
- (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error
/*! @abstract Invoked when an error occurs while starting to load data for
the main frame(在开始为加载数据时发生错误时调用).
@param webView The web view invoking the delegate method.
@param navigation The navigation.
@param error The error that occurred.
*/
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
(9) WK error: Error Domain=NSURLErrorDomain Code=-1007 "too many HTTP redirects":
image.png抓包分析:地址302重定向,客户端似乎没有发起请求,导致一直重定向地址. 302跳转异常.
客户端原因:
在302跳转中,重写请求head
信息,导致跳转下一个界面时,上一个界面设置的cookie丢失了. 再次体会下大神的总结.
(10) Error Domain=WKErrorDomain Code=3 "WKWebView已失效" UserInfo={NSLocalizedDescription=WKWebView已失效}
出现这种情况,在AppDelegate中获取ua时,代码如下
@objc class func wksetUserAgent(){
let webView = WKWebView.init()
webView?.evaluateJavaScript("window.navigator.userAgent;") { (result:Any?, error:Error?) in
if(error != nil){
debugPrint(error ?? "userAgent获取失败");
}else{
debugPrint(result ?? "userAgent获取成功")
}
}
}
swift类提前释放造成的,可以将当前类改成单利(不建议使用单利),定义一个全局局部存储属性.
//WK 设置UA
var webView:WKWebView?
class BaseHelper: NSObject
@objc func wksetUserAgent(){
webView = WKWebView.init()
webView?.evaluateJavaScript("window.navigator.userAgent;") { [weak self](result:Any?, error:Error?) in
let strongSelf = self
if(error != nil){
debugPrint(error ?? "userAgent获取失败");
}else{
debugPrint(result ?? "userAgent获取成功")
}
//strongSelf?.webView = nil;
}
}
deinit {
debugPrint("释放了");
}
如果是OC的话,属性强引用下,在block置空下
@property(nonatomic, strong) WKWebView * webView; // 强引用一下
(10) WK截取整个Html界面
+(UIImage *)wk_saveWebviewImage:(WKWebView *)webview
{
UIGraphicsBeginImageContextWithOptions(webview.bounds.size, webview.opaque, 0.0);
// [webview.layer renderInContext:UIGraphicsGetCurrentContext()];
// UIImage *senderimage = UIGraphicsGetImageFromCurrentImageContext();
// UIGraphicsEndImageContext();
//
// return senderimage;
CGFloat scale = [UIScreen mainScreen].scale;
CGSize boundsSize = webview.bounds.size;
CGFloat boundsWidth = boundsSize.width;
CGFloat boundsHeight = boundsSize.height;
CGSize contentSize = webview.scrollView.contentSize;
CGFloat contentHeight = contentSize.height;
// CGFloat contentWidth = contentSize.width;
CGPoint offset = webview.scrollView.contentOffset;
[webview.scrollView setContentOffset:CGPointMake(0, 0)];
NSMutableArray *images = [NSMutableArray array];
while (contentHeight > 0) {
UIGraphicsBeginImageContextWithOptions(boundsSize, NO, [UIScreen mainScreen].scale);
[webview.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[images addObject:image];
CGFloat offsetY = webview.scrollView.contentOffset.y;
[webview.scrollView setContentOffset:CGPointMake(0, offsetY + boundsHeight)];
contentHeight -= boundsHeight;
}
[webview.scrollView setContentOffset:offset];
CGSize imageSize = CGSizeMake(contentSize.width * scale,
contentSize.height * scale);
UIGraphicsBeginImageContext(imageSize);
[images enumerateObjectsUsingBlock:^(UIImage *image, NSUInteger idx, BOOL *stop) {
[image drawInRect:CGRectMake(0,
scale * boundsHeight * idx,
scale * boundsWidth,
scale * boundsHeight)];
}];
UIImage *fullImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// UIImageView * snapshotView = [[UIImageView alloc]initWithFrame:CGRectMake(0,0, webview.scrollView.contentSize.width, webview.scrollView.contentSize.height)];
// 拉伸图片
//snapshotView.image = [fullImage resizableImageWithCapInsets:UIEdgeInsetsZero];
return fullImage;
}
本人还是沿用以前UIWebveiw的方法,代码如上;功能藏得比较深,也没有做测试,只是替换,通过几个版本迭代,发现截取界面有空白;参考https://www.cnblogs.com/breezemist/p/7569798.html,文章提供的github项目使用偶然会也会出现界面空白的情况,还有一些非必须的截取bug,比如界面加载动画View也被截取上了,不符合要求.
最后将这个操作由前端实现;前端绘制界面,以base64的形式传给客户端解析,这种做法的缺陷也很明显,有卡顿,有时需要好几秒;甚至有些设备会出现图片空白的情况;应该是某些旧的设备因为资源开销问题(绘制图片耗时等),WKwebview崩溃了,同时触发调用了webViewWebContentProcessDidTerminate
方法
客户端主要实现
NSArray *imageArray = [base64Str componentsSeparatedByString:@","];
screenImages = [UIImage imageWithData:[[imageArray objectAtIndex:1] base64DecodedData]];
有好的做法,欢迎交流.
(11) webview ur包含&provId=##provId##&secCode=##secCode##&sign=##sign##&sysId 这种##的地址界面展示空白,无法响应webvew协议方法的问题
有人说是##被编码引起,但客户端只是对汉字编码,所以应该不是这个原因. ?????
未完待续
排查工程中UIWebView grep -r UIWebView .
JSCore参考文献
https://www.cnblogs.com/meituantech/p/9528285.html
网友评论