WKWebView刷新机制小探

作者: 笨鱼BennettPenn | 来源:发表于2016-11-07 21:54 被阅读10420次

create at 2016.11.07 20:58

背景

iOS的一个坑。在线上的版本中,iOS10系统中,app内使用WKWebView当作一个普通的子View来展示一个较长的Web内容组成一个hybrid页面时,会发生白屏的。经过原生端的开发的排除,确认是WKWebView的机制问题,并不是页面加载不完整或者是被劫持而导致的问题。

为了更严谨的排出问题所在,我拉去了原声端的代码再次确认代码逻辑是否存在导致该问题所在的bug。因为该页面是一个自定义的UITableView,WKWebView只是UITableView的一个Cell里面的子View,而且和UITableView的model层,也有很多的业务逻辑,看起来比较费劲。经过了几轮的调试,知识找到了一个导致导致死循环的一个调用,那边的开发使用了RAC绑定WKWebView内嵌UIScrollView的contentSize,去刷新UITableView,UITableView的回调获取cell的高度的时候会导致循环调用,一直的刷新UITableView获取cell的高度,除了会消耗性能,并没有看出逻辑有太大问题。因为,目前并不会导致页面出现一些莫名其妙的问题,也不知道原来写这部分代码逻辑的同事初衷是什么,所以并没有改动这部分代码。另外,用了Charles看了一下这个页面的请求,并不是页面劫持导致的问题。

  • 不是请求劫持导致的问题
  • http请求完整
  • 问题必现,证明是通用性问题

尝试设置WKWebView的frame比contentSize小,在滚动WKWebView的时候,里面的内容是可以全部展示的,并没有出现白屏的问题。可以得出的结论是:WKWebView作为一个元素放在UITabViewCell里面,是没问题问题的(当然,性能问题在讨论范围)。

调试了大半天,并没有找到问题的根源。于是先建立一个demo工程,先确认和排出一些问题。

  • UITableViewCell中嵌套WKWebView是否会导致刷新问题
  • UITabView中计算获取嵌套了WKWebView的UITabViewCell计算高度是否准确

建立工程,在UITableVie的一个UITableViewCell里面嵌套了一个WKWebView来重现工程中的情况。

Reveal

先通过Reveal工具来看一下WKWebView的树,先大概了解一些WKWebView的结构。

WKScrollView

WKScrollView继承于UIScrollView,在初始化的时将初始化一个WKScrollViewDegelageForwarder代理实例

下图是WKScrollView的delegate的setter方法,可以清晰的看到各个delegate的类型

在WKScrollViewDegelageForwarder的实现中,明确的看到,WKScrollView的delegate(externalDelegate实例)的消息都通过message_forward的形式转发到WKWebView(internalDelegate)实例中。

下图是WKScrollViewDegelageForwarder类的转发实现

WKContentView

WKContentView就是WKWebView内容渲染的容器。在Reveal的树状图上面可以看到,渲染页面中,展示在页面上的渲染单元是WKCompositingView,WKCompositingView可以嵌套WKCompositingView。其中的一个WKCompositingView实例,将包含多个WKCompositingView子实例。类似于UITableView的重用机制,多个WKCompositingView的父View就相当于UITableView,WKCompositingView就相当于UITableViewCell,只展示可视区域的内容,达到性能优化的目的。

从下图可以看到,一个WKWebView加载的web内容,切割成多个WKCompositingView,单个WKCompositingView重用单元的面积是375x512点。

WKWebView

初始化

在WKWebView初始化的代码中,可以看到这样的一段初始化代码

init

ScrollView回调

在WKWebView中,ScrollView相关的回调的调用链都是这样的一个调用关系:

scrollview delegate's callback 
->[WKWebView _updateVisibleContentRectAfterScrollInView:]
->[WKWebView _updateContentRectsWithState:]
->[WKContentView didUpdateVisibleRect:visibleRectInContentCoordinates
                       unobscuredRect:unobscuredRectInContentCoordinates
unobscuredRectInScrollViewCoordinates:unobscuredRect
                        obscuredInset:CGSizeMake(_obscuredInsets.left, _obscuredInsets.top)
                                scale:scaleFactor 
                         minimumScale:[_scrollView minimumZoomScale]
                        inStableState:inStableState
 isChangingObscuredInsetsInteractively:_isChangingObscuredInsetsInteractively
      enclosedInScrollableAncestorView:scrollViewCanScroll([self _scroller])];

从调用链上清晰可以看到,当WKScrollView滚动的时候,WKScrollView滚动相关回调的消息,将会发送到WKWebView内,WKWebView实例内scrollView的的回调将会调用WKContntView的刷新方法,刷新需要渲染的web内容。

猜想

当了解到WKWebView内容的刷新机制以后,就可以合理的进行猜想了。
因为WKWebView作为一个普通的UIView添加在UITableViewCell的contentView上,因为项目中UITableView和WKWebView的ScrollView都是竖向滚动的,这两个手势动作将会冲突,WKWebView只是一个子View,需要通过设置内置ScrollView的滚动属性来将WKWebView的滚动功能关闭,保证父View--UITableView滚动功能的正常使用。

因为WKWebView使用过绑定内置ScrollView的滚动回调来刷新WKContentView内需要渲染的web内容的,因为WKWebView已经被设定为禁止滚动,自然不会再刷新需要渲染当初在不在可视区域的内容了。因为UITableView的滚动回调并没有和WKWebView的内的滚动是绑定关系,所以在UITableView滚动的时候,并不会触发WKWebView的刷新。这就是为什么在进入页面的时候,上面一部分内容可以正常显示,二下半部分显示白屏的原因。当然,在目前来说只是一个猜想。

验证

前面的猜想,在经过对源代码的阅读,理论上是说得通的。现在就通过demo的代码验证。有了上面的原理,那么UITablbeView滚动的时候,触发WKWebView刷新页面即可?可知的是,WKWebView是调用_updateVisibleContentRectAfterScrollInView:方法来对WKContentView来刷新内容的。

由下图可知,而WKWebView的_updateVisibleContentRects方法实现,也只是调用了_updateVisibleContentRectAfterScrollInView:,也就是说直接调用WKWebView实例的_updateVisibleContentRects就可以刷新了。

下面,用RAC来监听一下UITableView实例的contentOffset属性,在contentOffset发生变化的时候,也就是UITableView实例滚动的时候,就去调用一下WKWebView实例的_updateVisibleContentRects方法去刷新需要渲染的内容。

    @weakify(self);
    [RACObserve(self.tableView, contentOffset) subscribeNext:^(id x) {
        @strongify(self);
        if ([self.webView respondsToSelector:@selector(_updateVisibleContentRects)]) {
            ((void(*)(id,SEL,BOOL))objc_msgSend)(self.webView,@selector(_updateVisibleContentRects),NO);
        }
    }];

在运行demo工程的时候,结果按照猜想的发生了,滚动到WKWebView下方时,原来会白屏的区域正常的渲染内容了。

解决方案

上方猜想被证实了,那么说这个方案时可以行的,而且对源代码的理解并没有太大的偏差。按照原理,可以使用一下几个方法来解决白屏的问题

  • 用KVO方法监听UITableView的contnetOffset属性,contentOffset发生变化也就是说UITableView发生滚动,调用WKWebView实例的_updateVisibleContentRects,刷新需要渲染的内容
  • UITableView是继承自UIScrollView的,在代码中实现UIScrollView的delegate,在delegate实现中手动调用WKWebView实例等UIScrollViewDelegate的方法,原理和第一种方法一样
  • 使用CADisplayLink类,在CADisplayLink的回调方法里面调用WKWebView实例的_updateVisibleContentRects即可

上面三种方法的其实都是大同小异的,只是适合不同的场景。优劣也不用说了,一眼就能看出来了。

相关文章

网友评论

  • SDBridge:分享一个Demo. WKWebView 监听JS端的所有的console.log日志
    https://www.jianshu.com/p/09e4799c5328
    https://github.com/housenkui/WKWebView-Console/
    笨鱼BennettPenn:@Coder_Hou :+1:
  • yixiaojichunqiu:标题写成小探太谦虚了,起码是中探,一叶知秋,博主给力
  • 神风大人:你好,我现在有一个需求,就是在网页加载完成后,在网页最下方再添加一个原生按钮。但是我调整了webview.scrollView.contentSize后也没效果。请问一下这个需要怎么弄?
  • 聪zero:我们webview加载的本地html资源,然后present一个视频录制界面去录视频,最后dimiss回到wkwebview,有的时候回来了,wkwebview就白屏了,看输出也没报错,这是什么原因?
  • Ko_Neko:被整个问题整整折磨了两天,各处搜索无果。都是说不要把webView放进Cell里。后来仔细观察UI层想要研究下WKCompositingView,搜索这个关键字才搜到这篇文章。简直救了命!!
  • 高高叔叔:我不知道你这个和刷新有什么关系啊。
  • halo_C:楼主您好,请问
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    webView.scrollView.setContentOffset(CGPoint(x: 0.0, y: 300), animated: false)
    }
    为何WkWebView这样调用无法滚动,而我改成UIWebView却可以滚动,有没有什么解决办法,请教!!
  • 张三疯疯子:楼主思路清晰,解决问题有条不紊,非常棒,谢谢楼主解决了我的困惑
  • 285b71b2e49e:你好,我想请教一下,使用WKWebView 的时候,加载个页面,然后回到后台一段时间,玩其他东西,一段时间后打开APP发现WKWebView变白屏了,但是WKWebView加载的网页内容还是可以点击的,操作都能点,就WKWebView要init一下才能显示,问,这个白屏要怎么判断
    洁简:同样的问题
    Lonely寂寞先生:请问你们解决了这个问题没有
    Ko_Neko:我好想也碰到这个问题 你看在becomActive里面手动调用一下 webView setneedslayout行不行呢
  • 5831487e4a23:@all 只要在scrollView的代理方法设置WKWeb实例 setNeedsLayout就好了。我不知道楼主为什么给我们一个“调用WKWebView实例的_updateVisibleContentRects”,难道你不知道没这个方法?
    笨鱼BennettPenn:只是借这个问题的契机去看一下WebKit而已:grin: 。_updateVisibleContentRects是私有方法,开放API看不到的,谢谢关注。
  • 5831487e4a23:"调用WKWebView实例的_updateVisibleContentRects",如何调用?
    DDDDeveloper:@John_Chen 谢谢。
    5831487e4a23:@一个有前途的男人 实现scrollViewDidScroll:方法:
    - (void)scrollViewDidScroll:(UIScrollView *)scrollView
    {
    [super scrollViewDidScroll:scrollView];
    [self.webView setNeedsLayout];
    }
    DDDDeveloper:亲,你的问题解决了吗?可否分享一下
  • Junheng:研究的很底层,让我恍然大悟,也感觉自己阅读底层代码的能力还是不行
  • 蓝精灵112:调用WKWebView实例的_updateVisibleContentRects,刷新需要渲染的内容,这个方法在哪。没找到
  • 卓明:谢谢。WKCompositingView 看了半天不知道这个view干嘛的。。。
  • LK83:大神改变wkwebView的frame的时候 怎么重新渲染wkwebView
  • 太空蛙:大神,wkwebview在ios8上白屏的问题遇到过吗?
  • 小生图图:大神,WKWebView回退上一个页面不会自动刷新,有啥办法能实现跟UIwebView一样回退自动刷新的?
    酷哥不回头看爆炸:你好请问返回刷新的问题你解决了吗?
    f88e19dc2613:@浮在空中的笨鱼 我改变了wk浏览器的useragent想重新加载一边wk刷新一下 要用到什么方法
    笨鱼BennettPenn:@小生图图 你说的重新刷新是指重新渲染页面还是加载数据呢?
  • wentianen:WKScrollView这些类的源码你是怎么看的,这些不是内部类么?
    wentianen:@CircusJonathan https://github.com/WebKit/webkit
    CircusJonathan:webkit的源代码怎么看,求大神指点。
    笨鱼BennettPenn:@wentianen 看webkit的源代码就能看到了
  • 14582a894fe0:UIWebview的时候能很快的把所有的内容给加载出来,WKWebview只有当你滑到那里才会刷新
  • 14582a894fe0:lz你好,我们项目在使用WKWebview的时候遇见了一个问题。内容区域一段一段的加载,特别慢。我地址改成baidu.com也会慢,不过比我们的快多了。 什么问题?
    笨鱼BennettPenn:@杨大哥888d 渲染机制一样的,只渲染可视区域的内容。和数据加载过程没有太大联系的。
    14582a894fe0:@浮在空中的笨鱼 我们的情况是从后台请求数据,然后和本地的一个html文件各种拼接,通过loadHtmlString实现的,里面的图片等资源是通过SDWebimage加载然后调JS刷新的。 我们的wkwebview是可以滑动的,应该可以触发webview 的刷新机制,和你们的还不太一样。
    笨鱼BennettPenn:@杨大哥888d 这是WKWebView的lazy load机制来的,类似于UITableView的循环回收机制,能有效控制WebView的内存使用,优化性能的。UIWebView一次性渲染页面所有内容会对系统造成很大的内存消耗的,这是WKWebView改进的地方。至于为什么加载比百度慢,因为baidu首页没有什么内容元素的,比较简单,还有一个就是baidu这些公司一般都有cdn缓存的,百度加载比较快是正常现象的。不过的和你们后端工程师确认一下。
  • DDDDeveloper:分析的很透彻。请教一下作者,如果我想改变 WebView 内 ScrollView 的默认偏移量,该如何实现? 因为我想达到这样一个效果: 加载出来的网页默认顶部留一部分空白。也就是说 contentOffset 的 y 值默认为负数。谢谢
    DDDDeveloper:@浮在空中的笨鱼 时隔半年再次回看此贴。好贴!
    笨鱼BennettPenn:WebView都内含一个UIScrollview的,你设置UIScrollview的contentInset就可以了。你要设定webView的内容向下偏移,那么webView.contentInset = UIEdgeInsetsMake(40,0,0,0)即可。WKWebView和UIWebView都有效

本文标题:WKWebView刷新机制小探

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