背景
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:]->[WKContentViewdidUpdateVisibleRect:visibleRectInContentCoordinatesunobscuredRect:unobscuredRectInContentCoordinatesunobscuredRectInScrollViewCoordinates: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:^(idx) {? ? ? ? @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即可
上面三種方法的其實都是大同小異的,只是適合不同的場景。優劣也不用說了,一眼就能看出來了。