实现一个优雅的 jsBridge 方案
在 iOS 项目中,有时需要实现 JavaScript 和 Native 代码之间的通信。本文介绍一种优雅的 jsBridge 实现方案,支持互相调用和回调机制,并附带详细的代码和注释。
步骤 1: 定义桥接协议
首先,定义一个通用的消息格式,用于传递方法名和参数,以及回调标识符。例如:
{
"method": "showAlert",
"params": {
"title": "Hello",
"message": "This is a message from JavaScript"
},
"callbackId": "callback_1"
}
步骤 2: 设置 WebView
我们需要设置 WKWebView 并配置消息处理器,以处理来自 JavaScript 的消息。
import WebKit
class ViewController: UIViewController, WKScriptMessageHandler {
var webView: WKWebView!
var callbacks: [String: (Any?) -> Void] = [:]
override func viewDidLoad() {
super.viewDidLoad()
// 设置 WKWebView 配置
let contentController = WKUserContentController()
// 在 WKWebView 中注册一个名为 jsBridge 的消息处理器,以便接收和处理从 JavaScript 发送到 Native 代码的消息。
contentController.add(self, name: "jsBridge")
let config = WKWebViewConfiguration()
config.userContentController = contentController
// 初始化 WKWebView
webView = WKWebView(frame: self.view.bounds, configuration: config)
self.view.addSubview(webView)
// 加载网页
if let url = URL(string: "https://your-web-page-url.com") {
webView.load(URLRequest(url: url))
}
}
// 处理从 JavaScript 发送的消息
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard let messageBody = message.body as? [String: Any],
let method = messageBody["method"] as? String,
let params = messageBody["params"] as? [String: Any],
let callbackId = messageBody["callbackId"] as? String else {
return
}
handleJSMethod(method, params: params, callbackId: callbackId)
}
// 处理具体的 JavaScript 调用的方法
func handleJSMethod(_ method: String, params: [String: Any], callbackId: String) {
if method == "showAlert" {
if let title = params["title"] as? String,
let message = params["message"] as? String {
showAlert(title: title, message: message) { result in
self.callJSCallback(callbackId: callbackId, result: result)
}
}
}
// 处理其他方法
}
// 显示警告框,并在完成后调用回调
func showAlert(title: String, message: String, completion: @escaping (Any?) -> Void) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
completion(["status": "OK"])
}))
self.present(alert, animated: true, completion: nil)
}
// 调用 JavaScript 回调函数
func callJSCallback(callbackId: String, result: Any?) {
// 将结果转换为 JSON 格式,并进行 Base64 编码
let jsonResult = (try? JSONSerialization.data(withJSONObject: result ?? [:]))?.base64EncodedString() ?? ""
// 构建 JavaScript 代码,调用回调函数 (jsBridge,_invokeCallback 都是通过注入 JS 代码实现的)
let jsCode = "window.jsBridge._invokeCallback('\(callbackId)', '\(jsonResult)')"
webView.evaluateJavaScript(jsCode, completionHandler: nil)
}
deinit {
// 移除消息处理器以避免内存泄漏
webView.configuration.userContentController.removeScriptMessageHandler(forName: "jsBridge")
}
}
步骤 3: 注入 JS 代码
在网页加载完成后注入 JavaScript 代码,使得网页可以调用 Native 方法并处理回调。为了避免重复注入,我们在 JavaScript 中添加一个标志变量。
override func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
let jsCode = """
(function() {
if (window.jsBridgeInjected) {
return;
}
window.jsBridgeInjected = true;
window.jsBridge = {
callbacks: {},
// 调用 Native 方法
callNative: function(method, params, callback) {
const callbackId = 'cb_' + (new Date()).getTime();
this.callbacks[callbackId] = callback;
// 能够这样调用的原因是:在 WKWebView 中进行过配置 ->`contentController.add(self, name: "jsBridge")`
window.webkit.messageHandlers.jsBridge.postMessage({
method: method,
params: params,
callbackId: callbackId
});
},
// Native 调用的回调方法
_invokeCallback: function(callbackId, result) {
if (this.callbacks[callbackId]) {
const resultObj = JSON.parse(atob(result));
this.callbacks[callbackId](resultObj);
delete this.callbacks[callbackId];
}
}
};
})();
"""
webView.evaluateJavaScript(jsCode, completionHandler: nil)
}
怎样使用 ?
在 JS 中调用 Native 方法并处理回调
在你的网页 JavaScript 代码中,通过 jsBridge.callNative
方法调用 Native 方法,并处理回调:
function showAlert() {
jsBridge.callNative("showAlert", { title: "Hello", message: "This is a message from JavaScript" }, function(result) {
console.log("Alert closed with status:", result.status);
});
}
更完整的代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JSBridge Example</title>
<script>
function showAlert() {
if (window.jsBridge && window.jsBridge.callNative) {
jsBridge.callNative("showAlert", { title: "Hello", message: "This is a message from JavaScript" }, function(result) {
console.log("Alert closed with status:", result.status);
});
} else {
console.error('jsBridge or callNative is not defined');
}
}
</script>
</head>
<body>
<h1>JSBridge Example</h1>
<button onclick="showAlert()">Show Alert</button>
</body>
</html>
Native 调用 JavaScript 方法
假设我们有一个 JavaScript 函数 displayMessage
,用于显示消息。我们可以通过以下方式从 Native 调用它:
JavaScript 代码:
function displayMessage(params) {
alert(params.message);
}
为了确保 displayMessage 函数能够从 Native 中调用:
- 它需要在被调用时,定义所在文件已经加载完成,displayMessage 已被成功定义好;
- 可被全局访问,这意味着你需要在 JavaScript 代码的全局作用域中定义该函数;
示例实现:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JSBridge Example</title>
<script>
function displayMessage(params) {
alert(params.message);
}
(function() {
if (window.jsBridgeInjected) {
return;
}
window.jsBridgeInjected = true;
window.jsBridge = {
callbacks: {},
// 调用 Native 方法
callNative: function(method, params, callback) {
const callbackId = 'cb_' + (new Date()).getTime();
this.callbacks[callbackId] = callback;
window.webkit.messageHandlers.jsBridge.postMessage({
method: method,
params: params,
callbackId: callbackId
});
},
// Native 调用的回调方法
_invokeCallback: function(callbackId, result) {
if (this.callbacks[callbackId]) {
const resultObj = JSON.parse(atob(result));
this.callbacks[callbackId](resultObj);
delete this.callbacks[callbackId];
}
}
};
})();
</script>
</head>
<body>
<h1>JSBridge Example</h1>
</body>
</html>
通过在网页的全局作用域中定义 JavaScript 函数,并确保在网页加载完成后调用这些函数,可以实现从 Native 代码调用 JavaScript 函数。这种方式可以确保函数在需要时已经定义并可用。
为了让 Native 代码能够调用 JavaScript 方法,我们需要在 Swift 代码中添加调用 JavaScript 函数的功能。以下是如何在 Native 代码中调用 JavaScript 的示例:
// 调用 JavaScript 方法
func callJSFunction(functionName: String, params: [String: Any]) {
// 将参数转换为 JSON 格式
let jsonData = try? JSONSerialization.data(withJSONObject: params)
let jsonString = String(data: jsonData!, encoding: .utf8)
// 构建 JavaScript 代码
let jsCode = "\(functionName)(\(jsonString!))"
// 执行 JavaScript 代码
webView.evaluateJavaScript(jsCode, completionHandler: { (result, error) in
if let error = error {
print("Error calling JS function: \(error)")
} else {
print("JS function called successfully with result: \(String(describing: result))")
}
})
}
示例:从 Native 调用 JavaScript
Swift 代码:
// 调用 JavaScript 函数
callJSFunction(functionName: "displayMessage", params: ["message": "Hello from Native!"])
总结
通过以上步骤和代码示例,你可以实现一个优雅的 jsBridge,使 JavaScript 和 Native 代码之间可以互相调用并支持回调机制。这种实现方式可以确保数据传输的稳定性和安全性,同时提升应用的交互能力。
技术细节
异步执行的设计哲学
设计 evaluateJavaScript
(Native端调用) 和 window.webkit.messageHandlers.jsBridge.postMessage
(Js端调用) 为异步执行的主要原因是为了确保应用的响应性和用户体验,同时避免阻塞主线程或 JavaScript 执行。这种设计带来了多方面的好处,以下是详细的解释。
1. 保证主线程的响应性
避免 UI 卡顿
在 iOS 应用中,主线程(也称为 UI 线程)负责处理用户界面更新和响应用户交互。如果在主线程上执行耗时操作,例如等待 JavaScript 代码执行或处理消息,会导致界面卡顿,影响用户体验。
示例:
webView.evaluateJavaScript("heavyComputation()") { (result, error) in
// 回调处理结果
}
如果 evaluateJavaScript
是同步执行的,那么在 heavyComputation()
完成之前,主线程会被阻塞,导致应用无法响应用户的操作。
异步执行确保流畅的用户体验
通过异步执行,evaluateJavaScript
和 postMessage
会立即返回,允许主线程继续处理其他任务,如用户界面更新和交互。这确保了应用的流畅性和响应性。
2. 提升性能和效率
并行处理
异步执行允许并行处理多个任务。例如,WebView 可以同时加载页面内容和执行 JavaScript 代码,而不会相互阻塞。这种并行处理提高了整体性能和效率。
示例:
window.webkit.messageHandlers.jsBridge.postMessage({ method: "doWork" });
// 同时执行其他任务
console.log("Message sent, continue executing other tasks.");
3. 防止死锁和资源争用
避免死锁
同步调用可能会导致死锁,特别是在跨语言调用时。如果 JavaScript 和 Native 代码相互等待对方完成操作,可能会导致死锁,导致应用无法继续执行。
管理资源争用
异步执行可以更好地管理资源争用,避免长时间占用某些资源(如网络、文件系统等),从而提高系统的稳定性和可靠性。
4. 提供更好的错误处理机制
异步回调处理错误
异步执行允许通过回调机制处理错误和异常。这样可以在不阻塞主线程的情况下,优雅地处理和记录错误,确保应用的健壮性。
示例:
webView.evaluateJavaScript("document.title") { (result, error) in
if let error = error {
print("JavaScript execution failed with error: \(error)")
} else if let result = result {
print("JavaScript execution result: \(result)")
}
}
5. 提高开发效率和代码可维护性
简化代码逻辑
异步执行通过回调或 Promises 简化了代码逻辑,避免了复杂的同步处理和锁定机制。这提高了代码的可读性和可维护性。
示例:
function sendMessageToNative() {
console.log("Sending message to native code");
window.webkit.messageHandlers.jsBridge.postMessage({
method: "showAlert",
params: {
title: "Hello",
message: "This is a message from JavaScript"
}
});
console.log("Message sent to native code");
}
总结
设计 evaluateJavaScript
和 window.webkit.messageHandlers.jsBridge.postMessage
为异步执行,是为了确保应用的响应性和用户体验,提升性能和效率,防止死锁和资源争用,提供更好的错误处理机制,并提高开发效率和代码可维护性。通过异步执行,这些方法可以立即返回,避免阻塞主线程或 JavaScript 执行,从而确保应用在进行跨语言调用时的流畅性和稳定性。
谨记:didReceiveMessage 是在 main 线程上调用执行的
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) 方法是在主线程上被调用的。这确保了所有的 UI 更新和操作都在主线程上完成,保持线程安全和 UI 的一致性。为了避免在处理消息时阻塞主线程,可以将耗时操作放到后台线程中执行,并在需要时回到主线程更新 UI。通过这种方式,可以确保应用的响应性和用户体验。
在实际开发中,jsBridge 会存在性能问题么 ?怎么监控 ?
在实际开发中,jsBridge 的性能问题主要来自于以下几个方面:
- 频繁的跨语言调用:每次 JavaScript 和 Native 代码之间的调用都涉及到上下文切换,这可能会导致性能瓶颈,尤其是在频繁调用的情况下。
- 数据传输开销:较大的数据通过 jsBridge 传输可能会影响性能,因为需要进行序列化和反序列化操作。
- 线程阻塞:某些操作可能会在主线程上执行,导致界面卡顿或阻塞。
- 复杂的业务逻辑:如果 jsBridge 处理的业务逻辑过于复杂,也可能影响整体性能。
性能监控
为了监控和优化 jsBridge 的性能,可以采用以下方法:
-
日志记录:在 jsBridge 的每次调用前后记录时间戳,以计算调用的耗时。
-
性能分析工具:使用系统自带或第三方性能分析工具,如 Xcode 的 Instruments,来监测 CPU 使用率、内存占用和线程活动等指标。
-
定期分析和优化:通过定期的性能分析和代码审查,识别和优化性能瓶颈。
具体实现
1. 日志记录
在每次 jsBridge 调用前后记录时间戳,并计算耗时:
JavaScript 端:
(function() {
window.jsBridge = {
callbacks: {},
callNative: function(method, params, callback) {
const startTime = Date.now();
const callbackId = 'cb_' + startTime;
this.callbacks[callbackId] = (result) => {
const endTime = Date.now();
console.log(`Call to ${method} took ${endTime - startTime}ms`);
callback(result);
};
window.webkit.messageHandlers.jsBridge.postMessage({
method: method,
params: params,
callbackId: callbackId
});
},
_invokeCallback: function(callbackId, result) {
if (this.callbacks[callbackId]) {
this.callbacks[callbackId](result);
delete this.callbacks[callbackId];
}
}
};
})();
Swift 端:
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
let startTime = Date().timeIntervalSince1970
guard let messageBody = message.body as? [String: Any],
let method = messageBody["method"] as? String,
let params = messageBody["params"] as? [String: Any],
let callbackId = messageBody["callbackId"] as? String else {
return
}
handleJSMethod(method, params: params, callbackId: callbackId)
let endTime = Date().timeIntervalSince1970
print("Call to \(method) took \((endTime - startTime) * 1000)ms")
}
2. 使用性能分析工具
Xcode Instruments:
- 启动 Instruments:打开 Xcode,选择菜单栏中的
Xcode > Open Developer Tool > Instruments
。 - 选择模板:选择合适的模板,如 Time Profiler 或 Activity Monitor。
- 运行应用:通过 Instruments 运行你的应用,开始性能监测。
- 分析结果:查看和分析 CPU 使用率、内存占用和线程活动,找出性能瓶颈。
3. 定期分析和优化
定期进行代码审查和性能分析,找出性能瓶颈,并进行优化。例如:
- 减少频繁调用:合并多次调用,减少跨语言调用的次数。
- 优化数据传输:通过压缩或分批传输数据,减少数据传输的开销。
- 优化线程处理:避免在主线程上执行耗时操作,将其放到后台线程处理。
小结
jsBridge 的性能问题主要来自于频繁的跨语言调用、数据传输开销、线程阻塞和复杂的业务逻辑。通过日志记录、使用性能分析工具和定期分析优化,可以有效监控和提升 jsBridge 的性能。
使用 WKScriptMessage (实现 Native JavaScript 的交互)与使用 JavaScriptCore 有什么不一样 ?
在 iOS 开发中,实现 Native 和 JavaScript 交互的主要方式有两种:WKScriptMessage
和 JavaScriptCore
。它们在功能、使用场景和性能上有一些明显的不同。
WKScriptMessage
WKScriptMessage
是 WKWebView 的一种机制,允许 JavaScript 发送消息给 Native 代码。它是通过 WKUserContentController
来添加消息处理器,并通过 WKScriptMessageHandler
来处理这些消息。
特点
-
现代化:
- 适用于
WKWebView
,这是苹果推荐的现代 WebView 实现,性能和安全性都优于UIWebView
。
- 适用于
-
简单易用:
- 配置和使用相对简单,只需要添加消息处理器并处理消息。
-
安全性:
- 通过消息传递,避免了直接调用 Native 方法的安全隐患。
使用示例
Swift 端:
import WebKit
class ViewController: UIViewController, WKScriptMessageHandler {
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let contentController = WKUserContentController()
contentController.add(self, name: "jsBridge")
let config = WKWebViewConfiguration()
config.userContentController = contentController
webView = WKWebView(frame: self.view.bounds, configuration: config)
self.view.addSubview(webView)
if let url = URL(string: "https://your-web-page-url.com") {
webView.load(URLRequest(url: url))
}
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == "jsBridge" {
if let body = message.body as? [String: Any] {
handleJSMessage(body)
}
}
}
func handleJSMessage(_ message: [String: Any]) {
// 处理来自 JavaScript 的消息
}
deinit {
webView.configuration.userContentController.removeScriptMessageHandler(forName: "jsBridge")
}
}
JavaScript 端:
function sendMessageToNative() {
window.webkit.messageHandlers.jsBridge.postMessage({
method: "showAlert",
params: {
title: "Hello",
message: "This is a message from JavaScript"
}
});
}
JavaScriptCore
JavaScriptCore
是 iOS 上的一个框架,提供了一个 JavaScript 引擎,允许在 Native 代码中直接执行 JavaScript 代码,并在 JavaScript 中调用 Native 方法。它主要用于 UIWebView
或不使用 WebView 的纯 JavaScript 处理。
特点
-
直接调用:
- 允许直接在 Native 代码中执行 JavaScript 代码,并获取结果。
- 允许在 JavaScript 中直接调用 Native 方法。
-
高级功能:
- 支持更复杂的 JavaScript 操作,例如创建和操作 JSContext、JSValue 等。
-
灵活性:
- 适用于需要复杂 JavaScript 处理的场景。
使用示例
Swift 端:
import JavaScriptCore
class ViewController: UIViewController {
var jsContext: JSContext!
override func viewDidLoad() {
super.viewDidLoad()
jsContext = JSContext()
// 定义一个 Native 方法,供 JavaScript 调用
let showAlert: @convention(block) (String) -> Void = { message in
DispatchQueue.main.async {
let alert = UIAlertController(title: "Alert", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
}
jsContext.setObject(showAlert, forKeyedSubscript: "showAlert" as NSString)
// 执行 JavaScript 代码
let jsCode = """
showAlert("Hello from JavaScript");
"""
jsContext.evaluateScript(jsCode)
}
}
对比总结
特性 | WKScriptMessage | JavaScriptCore |
---|---|---|
适用 WebView | WKWebView | UIWebView(已弃用)或不使用 WebView |
消息传递 | 通过消息传递 | 直接调用 |
复杂度 | 简单易用 | 功能强大但复杂 |
性能 | 较高 | 取决于使用场景 |
安全性 | 高 | 需要处理潜在的安全问题 |
使用场景 | 适用于现代 WebView 交互 | 适用于需要复杂 JavaScript 处理的场景 |
性能问题与监控
性能问题
- WKScriptMessage:由于通过消息传递,性能通常较好,但频繁的大数据传输可能会导致性能问题。
- JavaScriptCore:直接调用,性能较高,但复杂操作可能会影响性能。
性能监控
- 日志记录:在调用前后记录时间戳,计算耗时。
- 使用 Instruments:分析 CPU 使用率、内存占用和线程活动。
- 代码审查:定期分析和优化代码。
小结
WKScriptMessage
和 JavaScriptCore
各有优缺点,选择哪种方式取决于具体的使用场景和需求。通过适当的监控和优化,可以确保 jsBridge 的高效运行。