WKWebViewJavascriptBridge 通信机制深度解析
深入剖析 WKWebViewJavascriptBridge 的实现原理,包括 JavaScript 侧和 Swift 侧的初始化流程、消息队列机制、回调管理以及完整的双向通信流程,提供实用代码示例。
WKWebViewJavascriptBridge 实现 iOS 与 JS 通信的原理分析
引言
在混合式移动应用开发中,原生代码(如 Swift 或 Objective-C)与 JavaScript 之间的无缝通信是关键。WKWebViewJavascriptBridge 是一个轻量级、高性能的开源库,专为 iOS 应用的 WKWebView 提供 Swift 和 JavaScript 的双向通信支持。它通过抽象底层的消息传递机制,让开发者能够专注于混合式功能的实现,而无需处理复杂的通信细节。本文将深入剖析 WKWebViewJavascriptBridge 的实现原理,结合代码分析其工作机制,并提供使用示例和最佳实践。
什么是 WKWebViewJavascriptBridge?
WKWebViewJavascriptBridge 是一个为 WKWebView 设计的通信桥梁,允许在 Swift(或 Objective-C)与 JavaScript 之间发送消息。它受到 WebViewJavascriptBridge 的启发,专注于 WKWebView 的支持,放弃了对 UIWebView 的兼容,以提升安全性和性能。其主要特点包括:
- 高性能:基于 WKWebView 的脚本消息处理,优于传统的请求拦截方式。
- 高速度:无需担心警框安全超时问题。
- 轻量级:仅包含 3 个核心文件,易于集成。
- 非侵入式:无需继承其他基类。
- 兼容性:支持 Swift 3.2 至 5.0,适用于 iOS 9.0 及以上版本。
WKWebView 相较于 UIWebView 具有更快的加载速度、更低的内存开销,并且默认关闭了文件访问权限(allowFileAccessFromFileURLs 和 allowUniversalAccessFromFileURLs),从而避免了跨域访问的安全漏洞。
高层架构
WKWebViewJavascriptBridge 的核心在于通过 WKWebView 的脚本消息处理机制(WKScriptMessageHandler)实现双向通信。其架构分为两个主要部分:
- 原生侧(Swift/Objective-C):
- 通过
WKWebViewJavascriptBridge类注册处理器,接收 JavaScript 发来的消息。 - 调用 JavaScript 中的处理器,发送消息到网页。
- 通过
- JavaScript 侧:
- 通过注入的
WKWebViewJavascriptBridgeJS脚本注册处理器,接收原生侧的消息。 - 调用原生侧的处理器,发送消息到 iOS。
- 通过注入的
通信依赖于 WKWebView 的用户内容控制器(WKUserContentController),通过添加脚本消息处理器(如 iOS_Native_InjectJavascript 和 iOS_Native_FlushMessageQueue)捕获消息。消息以 JSON 格式传递,并通过队列机制确保可靠性和顺序。
JavaScript 侧的实现
JavaScript 侧的实现主要在 WKWebViewJavascriptBridgeJS 文件中,定义了桥梁的核心功能,包括初始化、注册处理器、调用处理器和消息处理。
1. 桥梁初始化
JavaScript 侧通过 setupWKWebViewJavascriptBridge 函数初始化桥梁:
1
2
3
4
5
6
function setupWKWebViewJavascriptBridge(callback) {
if (window.WKWebViewJavascriptBridge) { return callback(WKWebViewJavascriptBridge); }
if (window.WKWVJBCallbacks) { return window.WKWVJBCallbacks.push(callback); }
window.WKWVJBCallbacks = [callback];
window.webkit.messageHandlers.iOS_Native_InjectJavascript.postMessage(null);
}
- 逻辑:
- 检查
WKWebViewJavascriptBridge是否已存在,若存在,直接调用回调。 - 检查是否存在回调队列(
WKWVJBCallbacks),若存在,将回调加入队列。 - 否则,创建回调队列并通过
window.webkit.messageHandlers.iOS_Native_InjectJavascript.postMessage(null)通知原生侧注入桥梁脚本.
- 检查
- WKWVJBCallbacks:这是一个回调队列,用于在桥梁初始化完成前存储回调函数。初始化完成后,
_callWVJBCallbacks函数会遍历队列,依次执行所有回调,确保异步初始化不丢失注册操作。 - 作用:触发原生侧注入
WKWebViewJavascriptBridgeJS脚本,设置WKWebViewJavascriptBridge对象。
注入的脚本定义了以下核心函数:
registerHandler:注册处理器。callHandler:调用原生处理器。_fetchQueue:获取消息队列。_handleMessageFromiOS:处理原生侧消息。
2. 注册处理器
使用 registerHandler 注册处理器,接收原生侧的消息:
1
2
3
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
- 参数:
handlerName:处理器名称。handler:处理器函数,接收数据和回调。
- 存储:处理器存储在
messageHandlers对象中,键为处理器名称,值为函数.
3. 调用原生处理器
使用 callHandler 调用原生侧的处理器:
1
2
3
4
5
6
7
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({ handlerName:handlerName, data:data }, responseCallback);
}
- 参数:
handlerName:原生处理器名称。data:传递的数据。responseCallback:响应回调函数。
- 逻辑:
- 处理参数兼容性(如果只有两个参数且第二个是函数,则视为回调)。
- 调用
_doSend发送消息。
_doSend 函数:
1
2
3
4
5
6
7
8
9
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackID = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
responseCallbacks[callbackID] = responseCallback;
message['callbackID'] = callbackID;
}
sendMessageQueue.push(message);
window.webkit.messageHandlers.iOS_Native_FlushMessageQueue.postMessage(null);
}
- 逻辑:
- 如果有回调,生成唯一回调 ID 并存储。
- 将消息添加到
sendMessageQueue队列。 - 通知原生侧通过
iOS_Native_FlushMessageQueue刷新队列。
4. 处理原native消息
原生侧的消息通过 _handleMessageFromiOS 处理:
1
2
3
function _handleMessageFromiOS(messageJSON) {
_dispatchMessageFromiOS(messageJSON);
}
_dispatchMessageFromiOS 函数解析消息并分发:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function _dispatchMessageFromiOS(messageJSON) {
var message = JSON.parse(messageJSON);
var messageHandler;
var responseCallback;
if (message.responseID) {
responseCallback = responseCallbacks[message.responseID];
if (!responseCallback) { return; }
responseCallback(message.responseData);
delete responseCallbacks[message.responseID];
} else {
if (message.callbackID) {
var callbackResponseId = message.callbackID;
responseCallback = function(responseData) {
_doSend({ handlerName:message.handlerName, responseID:callbackResponseId, responseData:responseData });
};
}
var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log("WKWebViewJavascriptBridge: WARNING: no handler for message from iOS:", message);
} else {
handler(message.data, responseCallback);
}
}
}
- 逻辑:
- 解析 JSON 消息。
- 如果包含
responseID,调用对应的回调并移除。 - 否则,查找注册的处理器并调用,若有
callbackID,创建响应回调。
Swift 侧的实现
Swift 侧的实现主要由 WKWebViewJavascriptBridgeBase 和 WKWebViewJavascriptBridge 两个类组成。
1. 桥梁初始化
创建 WKWebViewJavascriptBridge 实例:
1
2
3
4
5
6
7
public init(webView: WKWebView) {
super.init()
self.webView = webView
base = WKWebViewJavascriptBridgeBase()
base.delegate = self
addScriptMessageHandlers()
}
- 逻辑:
- 初始化
WKWebViewJavascriptBridgeBase实例。 - 设置委托为自身。
- 调用
addScriptMessageHandlers添加脚本消息处理器。
- 初始化
addScriptMessageHandlers 方法:
1
2
3
4
private func addScriptMessageHandlers() {
webView?.configuration.userContentController.add(LeakAvoider(delegate: self), name: iOS_Native_InjectJavascript)
webView?.configuration.userContentController.add(LeakAvoider(delegate: self), name: iOS_Native_FlushMessageQueue)
}
- 处理器:
iOS_Native_InjectJavascript:处理初始化请求。iOS_Native_FlushMessageQueue:处理消息队列刷新。
- LeakAvoider:
LeakAvoider是一个代理类,通过弱引用WKScriptMessageHandler避免WKUserContentController持有强引用导致内存泄漏。这种设计在长期运行的 WebView 中尤为重要,可防止控制器或桥梁对象无法释放。
2. 注入 JavaScript 脚本
当接收到 iOS_Native_InjectJavascript 消息时,调用 injectJavascriptFile:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func injectJavascriptFile() {
let js = WKWebViewJavascriptBridgeJS
delegate?.evaluateJavascript(javascript: js, completion: { [weak self] (_, error) in
guard let self = self else { return }
if let error = error {
self.log(error)
return
}
self.startupMessageQueue?.forEach({ (message) in
self.dispatch(message: message)
})
self.startupMessageQueue = nil
})
}
- 逻辑:
- 注入
WKWebViewJavascriptBridgeJS脚本。 - 如果有启动消息队列(
startupMessageQueue),分发所有消息。 - 清空启动消息队列。
- 注入
- startupMessageQueue:用于在桥梁脚本注入完成前缓存消息,避免初始化期间的消息丢失。
3. 注册处理器
使用 register 方法注册处理器:
1
2
3
public func register(handlerName: String, handler: @escaping WKWebViewJavascriptBridgeBase.Handler) {
base.messageHandlers[handlerName] = handler
}
- 存储:处理器存储在
messageHandlers字典中。
4. 调用 JavaScript 处理器
使用 callHandler 方法调用 JavaScript 处理器:
1
2
3
public func call(handlerName: String, data: Any? = nil, callback: WKWebViewJavascriptBridgeBase.Callback? = nil) {
base.send(handlerName: handlerName, data: data, callback: callback)
}
send 方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func send(handlerName: String, data: Any?, callback: Callback?) {
var message = [String: Any]()
message["handlerName"] = handlerName
if data != nil {
message["data"] = data
}
if callback != nil {
uniqueId += 1
let callbackID = "native_iOS_cb_\(uniqueId)"
responseCallbacks[callbackID] = callback
message["callbackID"] = callbackID
}
queue(message: message)
}
- 逻辑:
- 创建消息字典,包含处理器名称和数据。
- 如果有回调,生成唯一回调 ID 并存储。
- 调用
queue方法处理消息。
queue 方法:
1
2
3
4
5
6
7
private func queue(message: Message) {
if startupMessageQueue == nil {
dispatch(message: message)
} else {
startupMessageQueue?.append(message)
}
}
dispatch 方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private func dispatch(message: Message) {
guard var messageJSON = serialize(message: message, pretty: false) else { return }
messageJSON = messageJSON.replacingOccurrences(of: "\\", with: "\\\\")
// ... 其他转义处理 ...
let javascriptCommand = "WKWebViewJavascriptBridge._handleMessageFromiOS('\(messageJSON)');"
if Thread.current.isMainThread {
delegate?.evaluateJavascript(javascript: javascriptCommand)
} else {
DispatchQueue.main.async {
self.delegate?.evaluateJavascript(javascript: javascriptCommand)
}
}
}
- 逻辑:
- 序列化消息为 JSON。
- 转义特殊字符。
- 注入 JavaScript 代码,调用
_handleMessageFromiOS。 - 检查当前线程,若非主线程,使用
DispatchQueue.main.async确保 UI 操作(如evaluateJavaScript)在主线程执行,防止线程安全问题。
5. 处理消息队列
当接收到 iOS_Native_FlushMessageQueue 消息时,调用 flushMessageQueue:
1
2
3
4
5
6
7
8
9
10
private func flushMessageQueue() {
webView?.evaluateJavaScript("WKWebViewJavascriptBridge._fetchQueue();") { (result, error) in
if error != nil {
print("WKWebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: \(String(describing: error))")
}
guard let resultStr = result as? String else { return }
self.base.flush(messageQueueString: resultStr)
}
}
flush 方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
func flush(messageQueueString: String) {
guard let messages = deserialize(messageJSON: messageQueueString) else {
log(messageQueueString)
return
}
for message in messages {
log(message)
if let responseID = message["responseID"] as? String {
guard let callback = responseCallbacks[responseID] else { continue }
callback(message["responseData"])
responseCallbacks.removeValue(forKey: responseID)
} else {
var callback: Callback?
if let callbackID = message["callbackID"] {
callback = { (_ responseData: Any?) -> Void in
let msg = ["responseID": callbackID, "responseData": responseData ?? NSNull()] as Message
self.queue(message: msg)
}
} else {
callback = { (_ responseData: Any?) -> Void in }
}
guard let handlerName = message["handlerName"] as? String else { continue }
guard let handler = messageHandlers[handlerName] else {
log("NoHandlerException, No handler for message from JS: \(message)")
continue
}
handler(message["data"] as? [String : Any], callback)
}
}
}
- 逻辑:
- 反序列化消息队列。
- 遍历消息:
- 如果包含
responseID,调用对应回调。 - 否则,查找处理器并调用,若有
callbackID,创建响应回调。
- 如果包含
通信流程详解
以下是完整的通信流程:
1. 初始化桥梁
- JavaScript 侧:调用
setupWKWebViewJavascriptBridge,将回调加入WKWVJBCallbacks,发送iOS_Native_InjectJavascript消息。 - 原生侧:接收消息,调用
injectJavascriptFile,注入WKWebViewJavascriptBridgeJS脚本,执行WKWVJBCallbacks中的回调。 - 结果:页面中设置
WKWebViewJavascriptBridge对象,双方准备好通信。
2. JavaScript 调用原生处理器
- JavaScript 侧:
- 调用
bridge.callHandler,添加消息到sendMessageQueue。 - 发送
iOS_Native_FlushMessageQueue消息。
- 调用
- 原生侧:
- 接收消息,调用
flushMessageQueue。 - 执行
WKWebViewJavascriptBridge._fetchQueue()获取队列。 - 解析消息,调用注册的处理器。
- 接收消息,调用
- 回调:如果有回调,处理器通过
callback发送响应,JavaScript 侧通过responseCallbacks处理。
3. 原生调用 JavaScript 处理器
- 原生侧:
- 调用
bridge.callHandler,序列化消息。 - 注入 JavaScript 代码,调用
_handleMessageFromiOS。
- 调用
- JavaScript 侧:
- 解析消息,调用注册的处理器。
- 如果有回调,发送响应消息。
- 回调:原生侧通过
responseCallbacks处理响应,异步处理确保非阻塞。
使用示例
以下是一个完整的示例,展示如何使用 WKWebViewJavascriptBridge,包括错误处理。
原生侧(Swift)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import WKWebViewJavascriptBridge
import WebKit
class ViewController: UIViewController {
var webView: WKWebView!
var bridge: WKWebViewJavascriptBridge!
override func viewDidLoad() {
super.viewDidLoad()
let configuration = WKWebViewConfiguration()
webView = WKWebView(frame: view.bounds, configuration: configuration)
view.addSubview(webView)
bridge = WKWebViewJavascriptBridge(webView: webView)
bridge.isLogEnable = true // 启用日志以调试
// 注册处理器
bridge.register(handlerName: "testiOSCallback") { (parameters, callback) in
guard let params = parameters else {
print("Error: No parameters received")
callback?(["error": "Invalid parameters"])
return
}
print("Native received data: \(params)")
callback?(["response": "Native processed data"])
}
// 调用 JavaScript 处理器
bridge.call(handlerName: "testJavascriptHandler", data: ["foo": "bar"]) { (response) in
print("Native received response: \(response ?? [:])")
}
// 加载网页
if let url = URL(string: "https://example.com") {
webView.load(URLRequest(url: url))
}
}
}
JavaScript 侧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function setupWKWebViewJavascriptBridge(callback) {
if (window.WKWebViewJavascriptBridge) { return callback(WKWebViewJavascriptBridge); }
if (window.WKWVJBCallbacks) { return window.WKWVJBCallbacks.push(callback); }
window.WKWVJBCallbacks = [callback];
window.webkit.messageHandlers.iOS_Native_InjectJavascript.postMessage(null);
}
// 在网页加载时初始化
document.addEventListener('DOMContentLoaded', function() {
setupWKWebViewJavascriptBridge(function(bridge) {
// 注册处理器
bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
if (!data) {
console.log('Error: No data received');
responseCallback({ error: 'Invalid data' });
return;
}
console.log('JS received data: ' + JSON.stringify(data));
responseCallback('JS processed data');
});
// 调用原生处理器
bridge.callHandler('testiOSCallback', {'foo': 'bar'}, function(response) {
console.log('JS received response: ' + JSON.stringify(response));
});
});
});
注意:
- 确保网页加载的 HTML 包含
setupWKWebViewJavascriptBridge脚本,通常在DOMContentLoaded事件中执行。 - 示例中添加了参数检查,处理无效数据的情况。
实现原理总结
WKWebViewJavascriptBridge 的实现依赖于以下关键机制:
- 脚本消息处理器:
- 原生侧通过
WKUserContentController添加iOS_Native_InjectJavascript和iOS_Native_FlushMessageQueue处理器。 - JavaScript 侧通过
window.webkit.messageHandlers发送消息。
- 原生侧通过
- 消息队列:
- JavaScript 侧使用
sendMessageQueue存储待发送消息。 - 原生侧使用
startupMessageQueue存储初始化期间的消息。
- JavaScript 侧使用
- JSON 序列化:
- 消息以 JSON 格式传递,确保跨语言兼容性。
- 原生侧通过
serialize和deserialize方法处理 JSON。
- 回调机制:
- 双方通过唯一 ID(
callbackID和responseID)管理回调。 - 回调存储在
responseCallbacks中,处理后移除。
- 双方通过唯一 ID(
- 线程安全:
- 原生侧通过
DispatchQueue.main.async确保evaluateJavaScript在主线程执行,避免 UI 线程问题。
- 原生侧通过
优势与注意事项
优势
- 简单易用:提供直观的 API,开发者无需深入了解底层机制。
- 高性能:直接使用
WKScriptMessageHandler进行消息传递,避免了请求拦截的网络开销。JSON 序列化确保高效的数据传输,消息队列机制减少重复的 JavaScript 注入操作。 - 轻量级:仅 3 个文件,易于集成。
- 安全:基于 WKWebView,默认关闭跨域访问权限,降低安全风险。
注意事项
- 初始化时机:确保在网页加载前初始化桥梁,并在
DOMContentLoaded事件中调用setupWKWebViewJavascriptBridge,否则可能错过消息。 - 错误处理:启用日志(
isLogEnable = true)以调试问题。处理无效参数或处理器不存在的情况,避免异常。 - 内存管理:
LeakAvoider类防止内存泄漏,开发者应避免直接将控制器作为WKScriptMessageHandler。 - 线程安全:消息分发在主线程执行,开发者无需额外处理线程问题,但应注意异步回调的非阻塞特性。
- 版本兼容性:支持 iOS 9.0 及以上,但 WKWebView 在较低版本上的性能可能略有差异。建议测试目标系统版本以确保兼容性。
结论
WKWebViewJavascriptBridge 通过巧妙地利用 WKWebView 的脚本消息处理机制,实现了 iOS 和 JavaScript 之间的高效双向通信。其设计简洁、性能卓越,提供了易于使用的 API,让开发者能够轻松地在混合式应用中实现复杂的交互功能。理解其内部实现原理不仅有助于更好地使用该库,还能在需要时进行定制和优化。对于希望在 iOS 应用中集成 Web 内容的开发者来说,WKWebViewJavascriptBridge 是一个不可或缺的工具。
引用: