美文网首页
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