实现一个优雅的 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 WebKitclass 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// 初始化 WKWebViewwebView = 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 inself.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: { _ incompletion(["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) inif 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) inif 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().timeIntervalSince1970guard 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().timeIntervalSince1970print("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 WebKitclass ViewController: UIViewController, WKScriptMessageHandler {var webView: WKWebView!override func viewDidLoad() {super.viewDidLoad()let contentController = WKUserContentController()contentController.add(self, name: "jsBridge")let config = WKWebViewConfiguration()config.userContentController = contentControllerwebView = 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 JavaScriptCoreclass ViewController: UIViewController {var jsContext: JSContext!override func viewDidLoad() {super.viewDidLoad()jsContext = JSContext()// 定义一个 Native 方法,供 JavaScript 调用let showAlert: @convention(block) (String) -> Void = { message inDispatchQueue.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 的高效运行。