JsBridge-总结和优化

前言

本文的介绍了WebView以及JsBridge的相关知识,所涉及到的Demo托管在gitlib上。

WebView

WebView是android中加载网页的一个控件,通过该控件可以加载网络/本地的HTML页面,同时也可以运行网络/本地的JS代码。

在WebView中,你可以通过特定方法进行JS和Native之间的相互调用,对于Native调用JS,有以下2种常用方法:

  • loadUrl方法
  • evaluateJavascript方法

而在页面中使用JS调用Native,在android的WebView中提供了以下常用的3种方法,相应的在JS中需要使用对应的调用函数:

  • addJavascriptInterface方法
  • WebViewClient中的shouldOverrideUrlLoading方法
  • WebChromeClient中的onJsxxx方法

Native调用JS方法

进行该操作的前提是需要在WebView中开启对JS的支持,需要在初始化WebView时添加如下代码:

// 设置与Js交互的权限
webSettings.setJavaScriptEnabled(true);

loadUrl

通过WebView中的loadUrl方法,在传入的参数中添加以“javascript:”为前缀,以对应的JS方法为后缀的字符串,使用该方法运行JS代码,在onPageFinished后才有效:

/**
 * 使用loadUrl来调用js
 */
private void callJavaScriptByLoad(){
    //参数名需要以javaScript为开头,方法名必须与js中对应。
    mWebView.loadUrl("javascript:callJS()");
}

evaluateJavascript

evaluateJavascript提供了一个性能更佳高效的选择,该方法不会去刷新页面,从而提高效率,且支持直接通过接口获取JS处理后的回调,但是只支持4.4+以上的系统。

/**
 * 使用evaluateJavascript方法,该方法需要API>19
 * 不会去刷新页面,因此效率更高
 */
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
private void callJSByEvaluate(){
    // 只需要将第一种方法的loadUrl()换成下面该方法即可
    mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String value) {
            //此处为 js 返回的结果
            Toast.makeText(WebViewActivity.this, value, Toast.LENGTH_SHORT).show();
        }
    });
}

以上均会调用JS中名为callJS的函数。

JS调用Native方法

addJavascriptInterface

addJavascriptInterface是WebView提供的一个可以让JS直接访问Native的方法。

1.声明一个提供JS调用的类,该类提供JS使用的方法

public class JSCallJava extends Object{

    private Context mContext;

    public JSCallJava(Context context){
        mContext = context;
    }

    @JavascriptInterface
    public String callJava(){
        Toast.makeText(mContext, "hello java", Toast.LENGTH_SHORT).show();
        return "fromJavascriptInterface";
    }

}

2.在addJavascriptInterface中传入需要提供给JS使用的类,以及对应的在JS中的对象名

    /**
     * 第1种:
     * 映射的JS对象名
     * 4.2版本前直接使用该方法存在严重漏洞
     */
    @SuppressLint("JavascriptInterface")
    private void forJSByJSInterface() {
        //@param o Java方法的对象
        //@param jsMethodName Javascript对象名
        mWebView.addJavascriptInterface(new JSCallJava(this), "caller");
    }

3.在JS代码中调用对应native方法:

function testClick1() {
    //call native method
    var result = caller.callJava();
    document.getElementById("text3").value = result;

}

需要注意的是,在早期版本中(4.2版本前),该方式存在严重的漏洞,攻击者可以通过JS加载java中的反射类去执行本地命令,虽然在高版本中通过使用@JavascriptInterface进行规避该漏洞,但是老版本的问题依旧存在。

WebViewClient的shouldOverrideUrlLoading

WebView中,可以通过拦截URL跳转来实现JS调用Native方法,通过自定义uri协议格式来传递值,在WebViewClient的shouldOverrideUrlLoading中进行拦截处理。

1.为WebView设置WebViewClient,拦截URL跳转:

mWebView.setWebViewClient(new WebViewClient(){

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        Uri uri = Uri.parse(url);
        if ("js".equals(uri.getScheme())) {
            if ("webview".equals(uri.getAuthority())) {
                Toast.makeText(WebViewActivity.this, "shouldOverrideUrlLoading", Toast.LENGTH_SHORT).show();
            }
            //不进行跳转
            return true;
        }
        return super.shouldOverrideUrlLoading(view, url);
    }
});

2.在JS中通过设置URL来进行调用

    function testClick() {
            /*约定的url协议为:js://webview?arg1=111&arg2=222*/
            document.location = "js://webview?arg1=111&arg2=222";
        }

WebChromeClient的onJsxxx

通过设置WebChromeClient,拦截JS的对话框来实现对Native的调用。

在WebChromeClient存在三种弹框,分别为alert,confirm和prompt。三者除了弹窗级别不一样,还有返回的参数不同,其中prompt相对其他两个在JS使用率低,同时可以返回任意参数,所以prompt是最合适用与调用Native的入口。

1.设置WebChromeClient,重写onJsPrompt方法

        mWebView.setWebChromeClient(new WebChromeClient(){
            @Override
            public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
                return super.onJsAlert(view, url, message, result);
            }

            @Override
            public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
                return super.onJsConfirm(view, url, message, result);
            }

            @Override
            public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
                // 根据协议的参数,判断是否是所需要的url(原理同方式2)
                // 一般根据scheme(协议格式) & authority(协议名)判断(前两个参数)
                //假定传入进来的 url = "js://webview?arg1=111&arg2=222"(同时也是约定好的需要拦截的)

                Uri uri = Uri.parse(message);
                // 如果url的协议 = 预先约定的 js 协议
                // 就解析往下解析参数
                if ( uri.getScheme().equals("js")) {

                    // 如果 authority  = 预先约定协议里的 webview,即代表都符合约定的协议
                    // 所以拦截url,下面JS开始调用Android需要的方法
                    if (uri.getAuthority().equals("demo")) {

                        //
                        // 执行JS所需要调用的逻辑
                        Toast.makeText(WebViewActivity.this, "js调用了Android的方法", Toast.LENGTH_SHORT).show();
                        // 可以在协议上带有参数并传递到Android上
                        HashMap<String, String> params = new HashMap<>();
                        Set<String> collection = uri.getQueryParameterNames();

                        //参数result:代表消息框的返回值(输入值)
                        result.confirm("fromPrompt");
                    }
                    return true;
                }
                return super.onJsPrompt(view, url, message, defaultValue, result);
            }
        });

2.JS中使用弹窗进行回调:

function testClick2() {
    //call native method
    // 调用prompt()
    var result=prompt("js://demo?arg1=111&arg2=222");
    document.getElementById("text3").value = result;

}

JsBridge

JsBridge是一个Native和Html交互的一个桥梁,类似RPC的一个控件,使用该控件可以方便的实现JS和Native之间的相互调用和数据的传递。

原理简介

在JsBridge中,最重要的是要制定如下两个协议:

  • 数据的序列化协议
  • 调用控制协议

前者使用了json和URL,而后者,在java层中,以下三个接口中可以查看:

alt

对于协议的实现,对应在Native和JS的桥接类:

  • BridgeWebView
  • WebViewJavascriptBridge.js
alt

在WebView中提到了2中调用JS的方法以及3种调用Java的方法,而在JsBridge中,其实就是使用了loadUrl和shouldOverrideUrlLoading,来实现JS和Native的互调。

在实际使用中,需要用BridgeWebView替代WebView,该类在init时,会默认去加载一个BridgeWebViewClient,而Client重写了shouldOverrideUrlLoading来对从JS获取到的数据进行分发,另外,也在onPageFinished中将WebViewJavascriptBridge.js加载到html页面中。

使用

在Android Studio中使用JsBridge,需要在project的build.gradle中添加maven库:

maven { url "https://jitpack.io" }

然后再module中添加如下依赖:

dependencies {
    compile 'com.github.lzyzsd:jsbridge:1.0.4'
}

在使用前,可以分别在Native和HTML中设置默认的handle,以处理未指定handle的调用。

Native中:

mBridgeWebView.setDefaultHandler(new DefaultHandler());

HTML中:

bridge.init(function(message, responseCallback) {
    …
    responseCallback(data);
});

Native调用JS方法

JsBridge通过实现一个自定义的WebView——BridgeWebView以及在JS层提供WebViewJavascriptBridge.js来实现两端使用代码的简化和统一。所有的操作最后都会在handle中处理,

在Native中,最简单的使用,通过send方法调用JS

mBridgeWebView.send(data);

该方法还可以设置一个回调,接收从JS获取到的结果:

mBridgeWebView.send(data, new CallBackFunction() {
    @Override
    public void onCallBack(String data) {

    }
});

send方法会使用JS中默认的handle进行数据的处理。

更多时候,我们需要使用自定义的方法,这时我们需要在Native中使用callHandler来调用JS:

mBridgeWebView.callHandler(handlerName, data, new CallBackFunction() {
    @Override
    public void onCallBack(String data) {
        …
    }
});

该方法可以指定JS中的handle来处理该数据,当然你需要在JS中注册对应的handle:

bridge.registerHandler(handlerName, function(data, responseCallback) {
    …
    responseCallback(responseData);
});

这样就完成了从Native到JS的一次调用。

JS调用Native方法

因为JsBridge在WebViewJavascriptBridge.js的封装,我们可以在JS中以类似的步骤实现对Native层调用。

使用send方法调用Java中默认的handle:

window.WebViewJavascriptBridge.send(
                data
                , function(responseData) {
                    …
                }
            );

使用callHandle调用Native中指定的方法处理:

window.WebViewJavascriptBridge.callHandler(
    'submitFromWeb'
    , {'param': '中文测试'}
    , function(responseData) {
        document.getElementById("show").innerHTML = "send get responseData from java, data = " + responseData
    }
);

相应的,你需要在Native中注册好对应的handle来处理事件:

mBridgeWebView.registerHandler(handleName, new BridgeHandler() {
    @Override
    public void handler(String data, CallBackFunction function) {
        …
    }
});

以上就可完成一次JS到Native的调用。

XGBridge

XGBridge是参考了一起赚/一起逛项目中的Bridge的实现,以及对JsBridge的二次封装。Demo托管在gitlib上的jsbridgedemo。大致的结构如下图所示:

alt

XGBridge封装在baseSDK中,而Native和JS之间互相调用方法需要依赖一些本地视图以及业务相关,所以抽离出来到project中实现,对应的JavaApi封装了JS使用的Native代码,JSApi则提供了可调用的JS方法。

XGBridge通过抽离WebView降低耦合度,其实例化依赖于WebView,因此,在含有WebView的界面中,通过如下构造方法完成实例化即可:

mXGBridge = new XGBridge(mWebView);

Api的实现,依赖于XGBridge中提供的以下两个方法实现通信:

  • callHandler
  • registerXGBridgeHandler

而具体的处理方法,需要和前端一起制定统一的调用协议,同时可以根据需求不断添加,完善。

Native调用JS方法

所有JS方法都将在JSApi中封装,其通过XGBridge中设置需要调用的JS方法名,传入参数以及回答借口实现。比如一个getJSContent来获取H5页面中的特定数据,JSApi中实现如下(需要对应在JS中实现getJSContent方法):

public class JSApi{
    …
    /**
    * 获取H5的信息
    * @param callback JS处理后的回调,包括其处理后的返回参数
    */
    public void getJSContent(CallBackFunction callback){
        mXGBridge.callHandler("getJSContent", null, callback);
    }

}

在需要使用的Activity中,实例化JSApi,然后在需要的事件中调用JS方法,Demo中展示了一个按钮触发该事件的一个过程:

//实例化JSApi
mJSApi = new DemoJSApi(mXGBridge);

…

//通过按钮触发,调用JS代码
mButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        //调用JS代码
        mJSApi.getJSContent(new CallBackFunction() {
            @Override
            public void onCallBack(String data) {
                Toast.makeText(H5Activity.this, data, Toast.LENGTH_SHORT).show();
            }
        });
    }
});

JS调用Native方法

JavaApi提供了给JS调用的handler,通过注册这些方法,实现JS端对Native端端调用。
编写一个Api,需要实现IXGBridgeHandler接口,该接口需要传入一个方法名(对应JS中调用的方法名),以及一个handler来执行相应的处理,比如提供JS关于Android系统的版本号等信息的IXGBridgeHandler:

public static IXGBridgeHandler getNativeInfo(final Context context){
    return new IXGBridgeHandler() {
        @Override
        public String getMethodName() {
            return "getNativeInfo";
        }

        @Override
        public void handler(String data, CallBackFunction function) {
            Toast.makeText(context, data, Toast.LENGTH_SHORT).show();
            function.onCallBack(android.os.Build.VERSION.RELEASE);
        }
    };
}

然后在需要使用的Activity中进行注册:

//注册Java的handler,用于JS调用
mXGBridge.registerXGBridgeHandler(JavaApi.getNativeInfo(this));
mXGBridge.registerXGBridgeHandler(JavaApi.setTitle(this));

总结

根据以上整理总结,对于Native调用JS的几个方法对比如下:

方法 发送数据 回调接口 兼容性
loadUrl
evaluateJavascript 仅4.4以上支持
JsBridge

JS调用Native的对比:

方法 发送数据 回调接口 安全性
addJavascriptInterface 4.2以下存在严重的漏洞
shouldOverrideUrlLoading
onJsPrompt
JsBridge

相关参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容