实现一个优雅的 jsBridge 方案

实现一个优雅的 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() 完成之前,主线程会被阻塞,导致应用无法响应用户的操作。

异步执行确保流畅的用户体验

通过异步执行,evaluateJavaScriptpostMessage 会立即返回,允许主线程继续处理其他任务,如用户界面更新和交互。这确保了应用的流畅性和响应性。

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");
}

总结

设计 evaluateJavaScriptwindow.webkit.messageHandlers.jsBridge.postMessage 为异步执行,是为了确保应用的响应性和用户体验,提升性能和效率,防止死锁和资源争用,提供更好的错误处理机制,并提高开发效率和代码可维护性。通过异步执行,这些方法可以立即返回,避免阻塞主线程或 JavaScript 执行,从而确保应用在进行跨语言调用时的流畅性和稳定性。

谨记:didReceiveMessage 是在 main 线程上调用执行的

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) 方法是在主线程上被调用的。这确保了所有的 UI 更新和操作都在主线程上完成,保持线程安全和 UI 的一致性。为了避免在处理消息时阻塞主线程,可以将耗时操作放到后台线程中执行,并在需要时回到主线程更新 UI。通过这种方式,可以确保应用的响应性和用户体验。


在实际开发中,jsBridge 会存在性能问题么 ?怎么监控 ?

在实际开发中,jsBridge 的性能问题主要来自于以下几个方面:

  1. 频繁的跨语言调用:每次 JavaScript 和 Native 代码之间的调用都涉及到上下文切换,这可能会导致性能瓶颈,尤其是在频繁调用的情况下。
  2. 数据传输开销:较大的数据通过 jsBridge 传输可能会影响性能,因为需要进行序列化和反序列化操作。
  3. 线程阻塞:某些操作可能会在主线程上执行,导致界面卡顿或阻塞。
  4. 复杂的业务逻辑:如果 jsBridge 处理的业务逻辑过于复杂,也可能影响整体性能。

性能监控

为了监控和优化 jsBridge 的性能,可以采用以下方法:

  1. 日志记录:在 jsBridge 的每次调用前后记录时间戳,以计算调用的耗时。

  2. 性能分析工具:使用系统自带或第三方性能分析工具,如 Xcode 的 Instruments,来监测 CPU 使用率、内存占用和线程活动等指标。

  3. 定期分析和优化:通过定期的性能分析和代码审查,识别和优化性能瓶颈。

具体实现

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

  1. 启动 Instruments:打开 Xcode,选择菜单栏中的 Xcode > Open Developer Tool > Instruments
  2. 选择模板:选择合适的模板,如 Time Profiler 或 Activity Monitor。
  3. 运行应用:通过 Instruments 运行你的应用,开始性能监测。
  4. 分析结果:查看和分析 CPU 使用率、内存占用和线程活动,找出性能瓶颈。
3. 定期分析和优化

定期进行代码审查和性能分析,找出性能瓶颈,并进行优化。例如:

  • 减少频繁调用:合并多次调用,减少跨语言调用的次数。
  • 优化数据传输:通过压缩或分批传输数据,减少数据传输的开销。
  • 优化线程处理:避免在主线程上执行耗时操作,将其放到后台线程处理。

小结

jsBridge 的性能问题主要来自于频繁的跨语言调用、数据传输开销、线程阻塞和复杂的业务逻辑。通过日志记录、使用性能分析工具和定期分析优化,可以有效监控和提升 jsBridge 的性能。


使用 WKScriptMessage (实现 Native JavaScript 的交互)与使用 JavaScriptCore 有什么不一样 ?

在 iOS 开发中,实现 Native 和 JavaScript 交互的主要方式有两种:WKScriptMessageJavaScriptCore。它们在功能、使用场景和性能上有一些明显的不同。

WKScriptMessage

WKScriptMessage 是 WKWebView 的一种机制,允许 JavaScript 发送消息给 Native 代码。它是通过 WKUserContentController 来添加消息处理器,并通过 WKScriptMessageHandler 来处理这些消息。

特点
  1. 现代化

    • 适用于 WKWebView,这是苹果推荐的现代 WebView 实现,性能和安全性都优于 UIWebView
  2. 简单易用

    • 配置和使用相对简单,只需要添加消息处理器并处理消息。
  3. 安全性

    • 通过消息传递,避免了直接调用 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 处理。

特点
  1. 直接调用

    • 允许直接在 Native 代码中执行 JavaScript 代码,并获取结果。
    • 允许在 JavaScript 中直接调用 Native 方法。
  2. 高级功能

    • 支持更复杂的 JavaScript 操作,例如创建和操作 JSContext、JSValue 等。
  3. 灵活性

    • 适用于需要复杂 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)
    }
}

对比总结

特性WKScriptMessageJavaScriptCore
适用 WebViewWKWebViewUIWebView(已弃用)或不使用 WebView
消息传递通过消息传递直接调用
复杂度简单易用功能强大但复杂
性能较高取决于使用场景
安全性需要处理潜在的安全问题
使用场景适用于现代 WebView 交互适用于需要复杂 JavaScript 处理的场景

性能问题与监控

性能问题
  • WKScriptMessage:由于通过消息传递,性能通常较好,但频繁的大数据传输可能会导致性能问题。
  • JavaScriptCore:直接调用,性能较高,但复杂操作可能会影响性能。
性能监控
  1. 日志记录:在调用前后记录时间戳,计算耗时。
  2. 使用 Instruments:分析 CPU 使用率、内存占用和线程活动。
  3. 代码审查:定期分析和优化代码。

小结

WKScriptMessageJavaScriptCore 各有优缺点,选择哪种方式取决于具体的使用场景和需求。通过适当的监控和优化,可以确保 jsBridge 的高效运行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

依旧风轻

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值