背景
通常在 WKWebView 打开一个页面,收到页面数据时,代理方法可感知这个时机:
-webView:didCommitNavigation:
但若是改变页面 hash (也就是位置标识符"#") 打开另一页面时,这个代理方法不会调用,也没有合适的回调接口。
从表现上说,改变 hash 会产生网页历史栈,safari 也会产生历史记录,这种场景应该是有和 -webView:didCommitNavigation:
相对应的回调,官方没做好一致性,需通过 WebKit 源码进一步探索感知方式。
源码分析
断点查看触发-webView:didCommitNavigation:
调用栈如下:
-> UIProcess 进程
-[AnyInstance webView:didCommitNavigation:]
WebKit::NavigationState::NavigationClient::didCommitNavigation
(IPC) WebKit::WebPageProxy::didCommitLoadForFrame
-> WebContent 进程
(IPC) Messages::WebPageProxy::DidCommitLoadForFrame
WebKit::WebFrameLoaderClient::dispatchDidCommitLoad
WebCore::FrameLoader::dispatchDidCommitLoad
WebCore::FrameLoader::receivedFirstData
在第一次收到页面数据后,会进行网页历史栈等状态的处理,最后抛给公开代理。对于改变页面 hash 打开另一页面场景,是在同一个 Document,FrameLoader
作为专门处理页面加载的地方,应该是有处理目标页面是否是同一 Document 的代码分支,扫描一下就找到了一个可疑的函数:
void FrameLoader::loadItem(...) {
...
if (sameDocumentNavigation)
loadSameDocumentItem(item);
else
loadDifferentDocumentItem(item, fromItem, loadType, MayAttemptCacheOnlyLoadForFormSubmissionItem, shouldTreatAsContinuingLoad);
}
追溯函数调用链,发现-webView:didCommitNavigation:
调用栈正是来自else
分支,查看if
分支的处理,最终会通过一个 IPC 消息发送到 APP 进程,在 APP 进程代码实现如下:
void WebPageProxy::didSameDocumentNavigationForFrame(...) {
...
m_navigationClient->didSameDocumentNavigation(...);
...
}
void NavigationState::NavigationClient::didSameDocumentNavigation(...) {
...
[static_cast<id <WKNavigationDelegatePrivate>>(navigationDelegate.get()) _webView:m_navigationState->m_webView navigation:wrapper(navigation) didSameDocumentNavigation:toWKSameDocumentNavigationType(navigationType)];
}
这里的navigationDelegate
就关联了 WKWebView 的公开代理 navigationDelegate
,而这个代理方法在私有代理方法列表躺着:
typedef NS_ENUM(NSInteger, _WKSameDocumentNavigationType) {
_WKSameDocumentNavigationTypeAnchorNavigation,
_WKSameDocumentNavigationTypeSessionStatePush,
_WKSameDocumentNavigationTypeSessionStateReplace,
_WKSameDocumentNavigationTypeSessionStatePop,
} WK_API_AVAILABLE(macos(10.10), ios(8.0));
@protocol WKNavigationDelegatePrivate <WKNavigationDelegate>
...
- (void)_webView:(WKWebView *)webView navigation:(WKNavigation *)navigation didSameDocumentNavigation:(_WKSameDocumentNavigationType)navigationType;
...
@end
粗略分析下源码,当 navigationType
这个枚举是 _WKSameDocumentNavigationTypeAnchorNavigation
时就表示完成了这次改变 hash 的页面切换。实现这个私有代理从源码来看是无副作用的,MR 记录在这里:https://bugs.webkit.org/show_bug.cgi?id=134855 。
结论
所以只需要在 WKWebView 的 navigationDelegate
所属类下面实现 _webView:navigation:didSameDocumentNavigation:
方法就能捕获到改变 hash 的页面切换的操作了,和 -webView:didCommitNavigation:
配对可完整感知 WKWebView 的页面切换完成时机。