JSBridge:混合开发中的双向通信[Android、iOS&JavaScript]

WebView是一个移动端控件,提供JS运行的沙箱环境,支持加载URL和拦截请求。JSBridge作为连接原生环境和Web环境的桥梁,实现双向通信。在高版本系统中,通过回调可以获取JS执行结果;低版本则无法执行回调。JSBridge允许Native调用Web端的JavaScript代码,反之亦然,通过挂载在window上的函数或对象进行交互。Android需开启JavaScript执行权限,而iOS在调用时没有参数类型限制。文章还介绍了不同版本的WebView在回调机制和通信方式上的差异,以及JSB的实现原理和优缺点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

什么是WebView

WebView 是移动端中的一个控件,它为 JS 运行提供了一个沙箱环境。WebView 能够加载指定的 url,拦截页面发出的各种请求等各种页面控制功能,JSB 的实现就依赖于 WebView 暴露的各种接口。

由于历史原因,IOS以8为分界,Android以4.4为分界,分为高低两个版本。而它们的区别在于 —— 回调。高版本可以通过执行回调拿到 JS 执行完毕的返回值,然后准确进行下一步操作。而低版本无法执行回调!

什么是 JSB

Hybrid App 的核心。我们开发的 h5 页面运行在端上的 WebView 容器之中,很多业务场景下 h5 需要依赖端上提供的信息/能力,这时我们需要一个可以连接原生运行环境和 JS 运行环境的桥梁 。 这个桥梁就是 JSB,JSB 让 Web 端和 Native 端得以实现双向通信。

JSB的目的

JSB 的目的就是“让 Native 可以调用 web 端的 JavaScript 代码,让 web 端可以调用 Native 的原生代码”。

native 调用 web 端代码

无论 Android 还是 iOS,在调用 web 端代码的时候,必须是调用的“挂载在window上的函数”。拿一个例子来说:

<Button
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="调用JS方法"
    android:onClick="onJSFunction1"/>
public void onJSFunction1 (View v) {
    mWebView.evaluateJavascript("javascript:onFunction('android调用JS方法')", new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String s) {
            AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
            builder.setMessage(s);
            builder.setNegativeButton("确定", null);
            builder.create().show();
        }
    });
}

evaluateJavascript 就是调用 JS 中的方法:onFunction1,并传入参数,在回调中进行处理 —— 这个回调至关重要。

在 js 中:

window.onFunction = function(str) {
	alert(str);
	return "这是 onFunction 方法的返回值";
}

android端调用web端代码

上面的 java 代码中,mWebView 是实例化的 腾讯X5内核组件。在它的配置中首先注意到这样一行代码:

/**
 * 允许加载的网页执行 JavaScript 方法
 */
webSettings.setJavaScriptEnabled(true);

允许网页执行 JS 方法。这个非常重要:它是 Native 是否能够向 web 通信的关键!这一点下面我们会提到。

在进行完一些基础配置后,我们会构建一个 JSBridge 对象:

addJavascriptInterface(
        new MyJaveScriptInterface(mContext, this),
        "AndroidJSBridge");

这个对象就叫做“AndroidJSBridge”,在这个处理中,这个 JSB 对象会被挂载到网页的 window 对象下,从而作为原生端和 web 端通信的桥梁。

web 调用 native 端代码

上面说了在初始化后 window 对象下会有一个 AndroidJSBridge 对象,在网页中也可以直接通过 window.AndroidJSBridge 拿到这个对象,从而调用 Android 端提供给网页端的方法(这里以Android为例):
现在 Android 提供了这样一个方法:

@JavascriptInterface
public void androidTestFunction1 (String str) {
    AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
    builder.setMessage(str);
    builder.setNegativeButton("确定", null);
    builder.create().show();
}

目的是在 APP 弹出一个 Alert 对话框,对话框中的内容为 JavaScript 传入的字符串。

注意:对于 Android 来说,当 js 调用方法并传参时,Android方法接收的参数只能是“基本数据类型”。对于“复杂数据类型”,只能通过JSON.stringify(Object) 转化成 string 类型。
而 iOS 则不受此限制。

在 web 项目中,拿原生项目来说,我们直接这么写即可:

<input type="button" value="调用androidTestFunction1" @click="toAndroidFunction1()" />

<script>
function toAndroidFunction1() {
	window.AndroidJSBridge.androidTestFunction1('调用 android 下的 function1 方法')
}
</script>

web 端调用 android 方法
当然,在 android 方法中,我们也可以直接 return 数据,到了 web 端就是“回调”了。

function toAndroidFunction2() {
	let result = window.AndroidJSBridge.androidTestFunction1('androidTestFunction2方法的返回值');
	alert(result);
}

web 端调用有返回值的 android方法
诶,为什么调用的 alert 也是弹出的 android 原生弹出框?
因为在初始化的时候,原生对 alert 进行了一次“劫持”:

/**
 * 监听网页中的url加载事件
 */
private void initChromeClient () {
    setWebChromeClient(new WebChromeClient(){

        /**
         * alert()
         * 监听alert弹出框,使用原生弹框代替alert。
         */
        @Override
        public boolean onJsAlert(WebView webView, String s, String s1, JsResult jsResult) {

                AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
                builder.setMessage(s1);
                builder.setNegativeButton("确定", null);
                builder.create().show();
            jsResult.confirm();

            return true;
        }
    });
}

双向通信

Native向web发送消息

其实原理上面已经说了:

Native 向 Web 发送消息基本原理上是在 WebView 容器中动态地执行一段 JS 脚本,通常情况下是调用一个挂载在全局上下文的方法。
具体来说 —— Native 端可以直接调用挂载在 window 上的全局方法并传入相应的函数执行参数,并且在函数执行结束后 Native 端可以直接拿到执行成功的返回值。

场景:页面上是一个web page,在其上有一个原生控件 Button 和 Input。在输入框中输入一段代码更改 web 页面上某个标签的 innerHTML

<EditText
    android:id="@+id/editText_name"
    android:layout_width="edit_parent"
    android:layout_height="edit_content"
    android:hint="请输入你想要执行的js代码" />

<Button
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Native向Web发送消息" />
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import cn.sunday.hybridappdemo.views.X5WebView;
 
public class MainActivity extends AppCompatActivity {
	private X5WebView mWebView;
	
    private Button button;
    private EditText editText_name;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = (Button) findViewById(R.id.Button_quedin);
        this.editText_name = (EditText) findViewById(R.id.editText_name);
        
        button.setOnClickListener(new View.OnClickListener() {//注册监听
            @Override //监听点击事件
            public void onClick(View v) {
                String name = editText_name.getText().toString();
                mWebView.evaluateJavascript("javascript:onShowPageNative(" + name + ")");
            }
        })
    })
}
window.onShowPageNative = function(str) {
	new Function(str);
}

将文本框输入的字符视为 JS 字符串并调用相关 API 直接执行

你是否想到了著名的用于解决跨域问题的jsonp?其实很多地方都使用了“将代码作为字符串传递再执行”的原理。最出名的就是微信小程序的(双线程)混合架构模型中view层向逻辑层通信。

Web 向 Native 发送消息

Web 向 Native 发送消息本质上就是某段 JS 代码的执行端上是可感知的,目前业界主流的实现方案有两种,分别是拦截式和注入式。

拦截式

和浏览器类似 WebView 中发出的所有请求都是可以被 Native 容器感知到的,因此拦截式具体指的是 Native 拦截 Web 发出的 URL 请求,双方在此之前约定一个 JSB 请求格式,如果该请求是 JSB 则进行相应的处理,若不是则直接转发。

Native 拦截请求的钩子方法:

  • Android:shouldOverrideUrlLoading
  • IOS:
    • 8+:decidePolicyForNavigationAction
    • 8-:shouldStartLoadWithRequest

拦截式的流程存在几个问题:

通过何种方式发出请求?
Web 端发出请求的方式非常多样,例如 <a>iframe.srclocation.href、ajax 等,但 <a> 需要用户手动触发,location.href 可能会导致页面跳转,安卓端拦截 ajax 的能力有所欠缺,因此绝大多数拦截式实现方案均采用 iframe 来发送请求。

如何规定 JSB 的请求格式?
一个标准的 URL 由 <scheme>://<host>:<port><path> 组成,相信大家都有过从微信或手机浏览器点击某个链接意外跳转到其他 App 的经历,如果有仔细留意过这些链接的 URL 你会发现目前主流 App 都有其专属的一个 scheme来作为该应用的标识,例如微信的 URL scheme 就是 weixin://。JSB 的实现借鉴这一思路,定制业务自身专属的一个 URL scheme 来作为 JSB 请求的标识,例如字节内部的 bytedance://

// Web 通过动态创建 iframe,将 src 设置为符合双端规范的 url scheme
const CUSTOM_PROTOCOL_SCHEME = 'vdian'

function web2Native(event) {    
    const messagingIframe = document.createElement('iframe');
    messagingIframe.style.display = 'none';
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + event;
    document.documentElement.appendChild(messagingIframe);

    setTimeout(() => {
        document.documentElement.removeChild(messagingIframe);
    }, 200)
}

拦截式在双端都具有非常好的向下兼容性,曾经是最主流的 JSB 实现方案,但目前在高版本的系统中已经逐渐被淘汰,理由是它有如下几个劣势:

  • 连续发送时可能会造成消息丢失(可以使用消息队列解决该问题)
  • URL 字符串长度有限制
  • 性能一般,URL request 创建请求有一定的耗时(Android 端 200-400ms)
注入式

注入式的原理是通过 WebView 提供的接口向 JS 全局上下文对象(window)中注入对象或者方法,当 JS 调用时,可直接执行相应的 Native 代码逻辑,从而达到 Web 调用 Native 的目的。
—— 也就是上面说的“web 端调用 Native 端代码”。

这种方法简单而直观,并且不存在参数长度限制和性能瓶颈等问题,目前主流的 JSB SDK 都将注入式方案作为优先使用的对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

恪愚

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

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

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

打赏作者

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

抵扣说明:

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

余额充值