Jacky's blog
首页
  • 学习笔记

    • web
    • android
    • iOS
    • vue
  • 分类
  • 标签
  • 归档
收藏
  • tool
  • algo
  • python
  • java
  • server
  • growth
  • frida
  • blog
  • SP
  • more
GitHub (opens new window)

Jack Yang

编程; 随笔
首页
  • 学习笔记

    • web
    • android
    • iOS
    • vue
  • 分类
  • 标签
  • 归档
收藏
  • tool
  • algo
  • python
  • java
  • server
  • growth
  • frida
  • blog
  • SP
  • more
GitHub (opens new window)
  • tutorial
  • jetpack

  • components

  • androidx

  • 动态化
  • apm

  • module

  • harmony

  • tool

  • other

    • Flutter 高频面试问答
    • 生产环境Message分发处理设计
    • 事件分发机制
    • 调研抖音对harmonyOS4的优化
    • Android 评论at功能的实现
    • 探索抖音禁止录屏
    • 对32位手机崩溃的优化记录
    • GradientDrawable
    • android window
    • color
    • webview白屏检测
    • android Resource
    • deeplink技术
    • android-xml
    • ANDROID IPC
    • BottomSheetBehavior研究与思考
    • viewPager
    • Android密钥系统
    • compiler
    • 提升UI加载速度的几点思考
    • Android零耗时首帧体验
    • jsbridge
      • 前置内容
        • jsbridge 的起源
        • js 中的 eval 函数
        • 什么是双向通道
      • 实现原理
        • js 通信原理
        • JavaScript 调用 Native
        • Native 调用 JavaScript
        • JSBridge 接口实现
      • TODO
      • 参考
    • retrofit动态代理设计
    • gif与属性动画的对比
  • kotlin

  • 《android》
  • other
Jacky
2023-08-25
目录

jsbridge

JSBridge 简单来讲, 主要是 给 JavaScript 提供调用 Native 功能的接口, 让混合开发中的『前端部分』可以方便地使用地址位置、摄像头甚至支付等 Native 功能

# 前置内容

# jsbridge 的起源

为什么是 JSBridge ?而不是 PythonBridge 或是 RubyBridge ?

当然不是因为 JavaScript 语言高人一等(虽然斯坦福大学已经把算法导论的语言从 Java 改成 JavaScript, 小得意一下, 嘻嘻), 主要的原因还是因为 JavaScript 主要载体 Web 是当前世界上的 最易编写 、 最易维护 、最易部署 的 UI 构建方式。工程师可以用很简单的 HTML 标签和 CSS 样式快速的构建出一个页面, 并且在服务端部署后, 用户不需要主动更新, 就能看到最新的 UI 展现

因此, 开发维护成本 和 更新成本 较低的 Web 技术成为混合开发中几乎不二的选择, 而作为 Web 技术逻辑核心的 JavaScript 也理所应当肩负起与其他技术『桥接』的职责, 并且作为移动不可缺少的一部分, 任何一个移动操作系统中都包含可运行 JavaScript 的容器, 例如 WebView 和 JSCore。所以, 运行 JavaScript 不用像运行其他语言时, 要额外添加运行环境。因此, 基于上面种种原因, JSBridge 应运而生

PhoneGap(Codova 的前身)作为 Hybrid 鼻祖框架, 应该是最先被开发者广泛认知的 JSBridge 的应用场景;而对于 JSBridge 的应用在国内真正兴盛起来, 则是因为杀手级应用微信的出现, 主要用途是在网页中通过 JSBridge 设置分享内容

移动端混合开发中的 JSBridge, 主要被应用在两种形式的技术方案上:

  • 基于 Web 的 Hybrid 解决方案: 例如微信浏览器、各公司的 Hybrid 方案
  • 非基于 Web UI 但业务逻辑基于 JavaScript 的解决方案: 例如 React-Native

【注】: 微信小程序基于 Web UI, 但是为了追求运行效率, 对 UI 展现逻辑和业务逻辑的 JavaScript 进行了隔离。因此小程序的技术方案介于上面描述的两种方式之间

# js 中的 eval 函数

try {
    var fn = eval('WebViewJavascriptBridge.' + handlerName);
} catch (e) {
    console.log(e);
}
1
2
3
4
5

这段代码是 JavaScript 中的一个片段, 它尝试通过字符串拼接来构造一个函数名, 然后使用 eval 函数来执行这个函数。具体来说:

  • handlerName 是一个变量, 它包含一个函数名的字符串, 例如 "myFunction"

  • eval 函数用于将字符串解析为 JavaScript 代码并执行。在这里, 它将字符串 "WebViewJavascriptBridge.myFunction" 解析为一个函数并执行它

  • try...catch 语句用于捕获可能在 eval 执行时发生的异常。如果构造的函数名无效或不存在, eval 会引发一个异常, 该异常将被 catch 块捕获, 并在控制台上记录异常信息

# 什么是双向通道

JSBridge 就像其名称中的『Bridge』的意义一样, 是 Native 和非 Native 之间的桥梁, 它的核心是 构建 Native 和非 Native 间消息通信的通道, 而且是 双向通信的通道

所谓 双向通信的通道:

  • JS 向 Native 发送消息 : 调用相关功能、通知 Native 当前 JS 的相关状态等
  • Native 向 JS 发送消息 : 回溯调用结果、消息推送、通知 JS 当前 Native 的状态等

# 实现原理

JavaScript 是运行在一个单独的 JS Context 中(例如, WebView 的 Webkit 引擎、JSCore)。由于这些 Context 与原生运行环境的天然隔离, 我们可以将这种情况与 RPC(Remote Procedure Call, 远程过程调用)通信进行类比, 将 Native 与 JavaScript 的每次互相调用看做一次 RPC 调用

在 JSBridge 的设计中, 可以把前端看做 RPC 的客户端, 把 Native 端看做 RPC 的服务器端, 从而 JSBridge 要实现的主要逻辑就出现了: 通信调用(Native 与 JS 通信) 和 句柄解析调用。(如果你是个前端, 而且并不熟悉 RPC 的话, 你也可以把这个流程类比成 JSONP 的流程)

通过以上的分析, 可以清楚地知晓 JSBridge 主要的功能和职责, 接下来就以 Hybrid 方案 为案例从这几点来剖析 JSBridge 的实现原理

# js 通信原理

# JavaScript 调用 Native

Android

一、addJavascriptInterface

在 4.2 版本之前 Android 注入 JavaScript 对象的接口是 addJavascriptInterface。 在 4.2 中引入新的接口 @JavascriptInterface(上面代码中使用的)来替代这个接口, 解决安全问题。所以 Android 注入对对象的方式是 有兼容性问题的

二、拦截 URL SCHEME

Web 端通过某种方式(例如 iframe.src)发送 URL Scheme 请求, 之后 Native 拦截到请求并根据 URL SCHEME(包括所带的参数)进行相关操作

在时间过程中, 这种方式有一定的 缺陷:

  • 使用 iframe.src 发送 URL SCHEME 会有 url 长度的隐患
  • 创建请求, 需要一定的耗时, 比注入 API 的方式调用同样的功能, 耗时会较长

但是之前为什么很多方案使用这种方式呢?因为它 支持 iOS6

【注】: 有些方案为了规避 url 长度隐患的缺陷, 在 iOS 上采用了使用 Ajax 发送同域请求的方式, 并将参数放到 head 或 body 里。这样, 虽然规避了 url 长度的隐患, 但是 WKWebView 并不支持这样的方式

【注 2】: 为什么选择 iframe.src 不选择 locaiton.href ?因为如果通过 location.href 连续调用 Native, 很容易丢失一些调用

# Native 调用 JavaScript

相比于 JavaScript 调用 Native, Native 调用 JavaScript 较为简单, 毕竟不管是 iOS 的 UIWebView 还是 WKWebView, 还是 Android 的 WebView 组件, 都以子组件的形式存在于 View/Activity 中, 直接调用相应的 API 即可

Android

在 Kitkat(4.4)之前并没有提供 iOS 类似的调用方式, 只能用 loadUrl 一段 JavaScript 代码 webView.loadUrl("javascript:" + javaScriptString);. Kitkat 之后的版本, 也可以用 evaluateJavascript 方法实现:

webView.evaluateJavascript(javaScriptString, new ValueCallback<String>() {
    @Override
    public void onReceiveValue(String value){

    }
});
1
2
3
4
5
6

【注】: 使用 loadUrl 的方式, 并不能获取 JavaScript 执行后的结果

iOS

# JSBridge 接口实现

从上面的剖析中, 可以得知, JSBridge 的接口主要功能有两个: 调用 Native(给 Native 发消息) 和 接被 Native 调用(接收 Native 消息)。因此, JSBridge 可以设计如下:

window.JSBridge = {
    // 调用 Native
    invoke: function (msg) {
        // 判断环境, 获取不同的 nativeBridge
        nativeBridge.postMessage(msg);
    },
    receiveMessage: function (msg) {
        // 处理 msg
    },
};
1
2
3
4
5
6
7
8
9
10

在上面的文章中, 提到过 RPC 中有一个非常重要的环节是 句柄解析调用 , 这点在 JSBridge 中体现为 句柄与功能对应关系。同时, 我们将句柄抽象为 桥名(BridgeName), 最终演化为 一个 BridgeName 对应一个 Native 功能或者一类 Native 消息。 基于此点, JSBridge 的实现可以优化为如下:

window.JSBridge = {
    // 调用 Native
    invoke: function (bridgeName, data) {
        // 判断环境, 获取不同的 nativeBridge
        nativeBridge.postMessage({
            bridgeName: bridgeName,
            data: data || {},
        });
    },
    receiveMessage: function (msg) {
        var bridgeName = msg.bridgeName,
            data = msg.data || {};
        // 具体逻辑
    },
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

JSBridge 大概的雏形出现了。现在终于可以着手解决这个问题了: 消息都是单向的, 那么调用 Native 功能时 Callback 怎么实现的?

对于 JSBridge 的 Callback , 其实就是 RPC 框架的回调机制。当然也可以用更简单的 JSONP 机制解释:

当发送 JSONP 请求时, url 参数里会有 callback 参数, 其值是 当前页面唯一 的, 而同时以此参数值为 key 将回调函数存到 window 上, 随后, 服务器返回 script 中, 也会以此参数值作为句柄, 调用相应的回调函数

由此可见, callback 参数这个 唯一标识 是这个回调逻辑的关键。这样, 我们可以参照这个逻辑来实现 JSBridge: 用一个自增的唯一 id, 来标识并存储回调函数, 并把此 id 以参数形式传递给 Native, 而 Native 也以此 id 作为回溯的标识。这样, 即可实现 Callback 回调逻辑

(function () {
    var id = 0,
    callbacks = {};

    window.JSBridge = {
        // 调用 Native
        invoke: function(bridgeName, callback, data) {
            // 判断环境, 获取不同的 nativeBridge
            var thisId = id ++; // 获取唯一 id
            callbacks[thisId] = callback; // 存储 Callback
            nativeBridge.postMessage({
                bridgeName: bridgeName,
                data: data || {},
                callbackId: thisId // 传到 Native 端
            });
        },
        receiveMessage: function(msg) {
            var bridgeName = msg.bridgeName,
                data = msg.data || {},
                callbackId = msg.callbackId; // Native 将 callbackId 原封不动传回
            // 具体逻辑
            // bridgeName 和 callbackId 不会同时存在
            if (callbackId) {
                if (callbacks[callbackId]) { // 找到相应句柄
                    callbacks[callbackId](msg.data); // 执行调用
                }
            } elseif (bridgeName) {

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

最后用同样的方式加上 Native 调用的回调逻辑, 同时对代码进行一些优化, 就大概实现了一个功能比较完整的 JSBridge。其代码如下:

主要的代码逻辑是:

(function () {
    var id = 0,
    callbacks = {},
    registerFuncs = {};

    window.JSBridge = {
        // 调用 Native
        invoke: function(bridgeName, callback, data) {
            // 判断环境, 获取不同的 nativeBridge
            var thisId = id ++; // 获取唯一 id
            callbacks[thisId] = callback; // 存储 Callback
            nativeBridge.postMessage({
                bridgeName: bridgeName,
                data: data || {},
                callbackId: thisId // 传到 Native 端
            });
        },
        receiveMessage: function(msg) {
            var bridgeName = msg.bridgeName,
                data = msg.data || {},
                callbackId = msg.callbackId, // Native 将 callbackId 原封不动传回
                responstId = msg.responstId;
            // 具体逻辑
            // bridgeName 和 callbackId 不会同时存在
            if (callbackId) {
                if (callbacks[callbackId]) { // 找到相应句柄
                    callbacks[callbackId](msg.data); // 执行调用
                }
            } elseif (bridgeName) {
                if (registerFuncs[bridgeName]) { // 通过 bridgeName 找到句柄
                    var ret = {},
                        flag = false;
                    registerFuncs[bridgeName].forEach(function(callback) => {
                        callback(data, function(r) {
                            flag = true;
                            ret = Object.assign(ret, r);
                        });
                    });
                    if (flag) {
                        nativeBridge.postMessage({ // 回调 Native
                            responstId: responstId,
                            ret: ret
                        });
                    }
                }
            }
        },
        register: function(bridgeName, callback) {
            if (!registerFuncs[bridgeName])  {
                registerFuncs[bridgeName] = [];
            }
            registerFuncs[bridgeName].push(callback); // 存储回调
        }
    };
})();
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

当然, 这段代码片段只是一个示例, 主要用于剖析 JSBridge 的原理和流程, 里面存在诸多省略和不完善的代码逻辑, 读者们可以自行完善

在 Native 端配合实现 JSBridge 的 JavaScript 调用 Native 逻辑也很简单, 主要的代码逻辑是

  • 接收到 JavaScript 消息
  • 解析参数, 拿到 bridgeName、data 和 callbackId
  • 根据 bridgeName 找到功能方法, 以 data 为参数执行
  • 执行返回值和 callbackId 一起回传前端

Native 调用 JavaScript 也同样简单, 直接自动生成一个唯一的 ResponseId, 并存储句柄, 然后和 data 一起发送给前端即可

# TODO

  • jsbridge 源码中为什么有两个 iframe, 是为了解决什么问题

# 参考

  • jsbridge (opens new window)
  • jsbridge 原理 (opens new window)
  • 一分钟说完 JSONP 请求, 面试满分答案 (opens new window)
#hybrid
上次更新: 2025/10/09, 23:53:03
Android零耗时首帧体验
retrofit动态代理设计

← Android零耗时首帧体验 retrofit动态代理设计→

最近更新
01
npx 使用指南
10-12
02
cursor
09-28
03
inspect
07-20
更多文章>
Theme by Vdoing | Copyright © 2019-2025 Jacky | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式