美文网首页
WKWebview适配总结

WKWebview适配总结

作者: Hunter琼 | 来源:发表于2020-05-22 16:49 被阅读0次

最近因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对象。

image.png

(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) didFailNavigationdidFailProvisionalNavigation 的区别

这两个方法容易被混淆,本人居然一直使用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丢失了. 再次体会下大神的总结.

image.png

(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方法

image.png

客户端主要实现

  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

相关文章

网友评论

      本文标题:WKWebview适配总结

      本文链接:https://www.haomeiwen.com/subject/zgepjhtx.html