JsBridge实现JavaScript和Java的互相调用

前端网页JavaScript(下文简称Js)和Java互相调用在手机应用中越来越常见,JsBridge是最常用的解决方案。

1. Js调用Java,Java调用Js

在Android开发中,能实现Js调用Java,有4种方法: 1.JavascriptInterface 2.WebViewClient.shouldOverrideUrlLoading() 3.WebChromeClient.onConsoleMessage() 4.WebChromeClient.onJsPrompt()

1.1 JavascriptInterface

这是Android提供的Js与Native通信的官方解决方案。 首先Java代码要实现这么一个类,它的作用是提供给Js调用。

public class JavascriptInterface {

    @JavascriptInterface
    public void showToast(String toast) {
        Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show();
    }
}

然后把这个类添加到WebView的JavascriptInterface中。webView.addJavascriptInterface(new JavascriptInterface(), “javascriptInterface”); 在Js代码中就能直接通过“javascriptInterface”直接调用了该Native的类的方法。

function showToast(toast) {
    javascript:javascriptInterface.showToast(toast);
}

但是这个官方提供的解决方案在Android4.2之前存在严重的安全漏洞。在Android4.2之后,加入了@JavascriptInterface才得到解决。所以考虑到兼容低版本的系统,JavascriptInterface并不适合。

1.2 WebViewClient.shouldOverrideUrlLoading()

这个方法的作用是拦截所有WebView的Url跳转。页面可以构造一个特殊格式的Url跳转,shouldOverrideUrlLoading拦截Url后判断其格式,然后Native就能执行自身的逻辑了。

public class CustomWebViewClient extends WebViewClient {

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        if (isJsBridgeUrl(url)) {
            // JSbridge的处理逻辑
            return true;
        }
        return super.shouldOverrideUrlLoading(view, url);
    }
}

1.3 WebChromeClient.onConsoleMessage()

这是Android提供给Js调试在Native代码里面打印日志信息的API,同时这也成了其中一种Js与Native代码通信的方法。在Js代码中调用console.log(‘xxx’)方法。

console.log('log message that is going to native code')

就会在Native代码的WebChromeClient.consoleMessage()中得到回调。consoleMessage.message()获得的正是Js代码console.log(‘xxx’)的内容。

public class CustomWebChromeClient extends WebChromeClient {

    @Override
    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
        super.onConsoleMessage(consoleMessage);
        String msg = consoleMessage.message();//JavaScript输入的Log内容
    }
}

1.4 WebChromeClient.onJsPrompt()

其实除了WebChromeClient.onJsPrompt(),还有WebChromeClient.onJsAlert()和WebChromeClient.onJsConfirm()。顾名思义,这三个Js给Native代码的回调接口的作用分别是展示提示信息,展示警告信息和展示确认信息。鉴于,alert和confirm在Js的使用率很高,所以JSBridge的解决方案中都倾向于选用onJsPrompt()。 Js中调用

window.prompt(message, value)

WebChromeClient.onJsPrompt()就会受到回调。onJsPrompt()方法的message参数的值正是Js的方法window.prompt()的message的值。

public class CustomWebChromeClient extends WebChromeClient {

    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        // 处理JS 的调用逻辑
        result.confirm();
        return true;
    }
}

1.5 Java调用Js

前文提到的4种通信方式都是Js通信Native的Java,而反过来,Java通信Js只有一种方式。那就是调用WebView.loadUrl()去执行一个预先定义好的Js方法。

webView.loadUrl(String.format("javascript:WebViewJavascriptBridge._handleMessageFromNative(%s)", data));

2. JsBridge

接下来会结合JsBridge这个开源组件(https://github.com/lzyzsd/JsBridge)来讲解一下JsBridge的原理

2.1 Java调用Js的functionInJs方法的流程图

2.2 JsBridge的UML图

2.3 JsBridge工作步骤讲解

2.3.1 WebView加载html页面

webView.registerHandler(“submitFromWeb”, …);这是Java层注册了一个叫”submitFromWeb”的接口方法,目的是提供给Js来调用。这个”submitFromWeb”的接口方法的回调就是BridgeHandler.handler()。webView.callHandler(“functionInJs”, …, new CallBackFunction());这是Java层主动调用Js的”functionInJs”方法。

public class MainActivity extends Activity implements OnClickListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        webView = (BridgeWebView) findViewById(R.id.webView);
        webView.loadUrl("file:///android_asset/demo.html");
        webView.registerHandler("submitFromWeb", new BridgeHandler() {

            @Override
            public void handler(String data, CallBackFunction function) {
                Log.i(TAG, "handler = submitFromWeb, data from web = " + data);
                function.onCallBack("submitFromWeb exe, response data 中文 from Java");
            }

        });

        webView.callHandler("functionInJs", new Gson().toJson(user), new CallBackFunction() {
            @Override
            public void onCallBack(String data) {

            }
        });
    }
}

我们一层层深入callHandler()方法的实现。这其中会调用到doSend()方法,这里想解释下m.setCallbackId(callbackStr)方法的作用。该方法设置的callbackId生成后不仅仅会被传到Js,而且会以key-value对的形式和responseCallback配对保存到responseCallbacks这个Map里面。它的目的,就是为了等Js把处理结果回调给Java层后,Java层能根据callbackId找到对应的responseCallback,做后续的回调处理。

private void doSend(String handlerName, String data, CallBackFunction responseCallback) {
        Message m = new Message();
        if (!TextUtils.isEmpty(data)) {
            m.setData(data);
        }
        if (responseCallback != null) {
            String callbackStr = String.format(BridgeUtil.CALLBACK_ID_FORMAT, ++uniqueId + (BridgeUtil.UNDERLINE_STR + SystemClock.currentThreadTimeMillis()));
            responseCallbacks.put(callbackStr, responseCallback);
            m.setCallbackId(callbackStr);
        }
        if (!TextUtils.isEmpty(handlerName)) {
            m.setHandlerName(handlerName);
        }
        queueMessage(m);
    }

最终可以看到是BridgeWebView.dispatchMessage(Message m)方法调用的是this.loadUrl(),调用了_handleMessageFromNative这个Js方法。那这个Js的方法是哪里来的呢?

final static String JS_HANDLE_MESSAGE_FROM_JAVA = "javascript:WebViewJavascriptBridge._handleMessageFromNative('%s');";

void dispatchMessage(Message m) {
        String messageJson = m.toJson();
        //escape special characters for json string
        messageJson = messageJson.replaceAll("(\\\\)([^utrn])", "\\\\\\\\$1$2");
        messageJson = messageJson.replaceAll("(?<=[^\\\\])(\")", "\\\\\"");
        String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);
        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            this.loadUrl(javascriptCommand);
        }
    }

2.3.2 加载WebViewJavascriptBridge.js

在WebViewClient.onPageFinished()里面的BridgeUtil.webViewLoadLocalJs(view, BridgeWebView.toLoadJs)。正是把保存在assert/WebViewJavascriptBridge.js加载到WebView中。

package com.github.lzyzsd.jsbridge;

public class BridgeWebViewClient extends WebViewClient {
  
    @Override
    public void onPageFinished(WebView view, String url) {
        super.onPageFinished(view, url);

        if (BridgeWebView.toLoadJs != null) {
            BridgeUtil.webViewLoadLocalJs(view, BridgeWebView.toLoadJs);
        }

        //
        if (webView.getStartupMessage() != null) {
            for (Message m : webView.getStartupMessage()) {
                webView.dispatchMessage(m);
            }
            webView.setStartupMessage(null);
        }
    }
}

2.3.3 分析WebViewJavascriptBridge.js

我们看看WebViewJavascriptBridge.js的代码,就能找到function _handleMessageFromNative()这个Js方法了。_handleMessageFromNative()方法里面会调用_dispatchMessageFromNative()方法。当处理来自Java层的主动调用时候会走“直接发送”的else分支。message.callbackId会被取出来,实例化一个responseCallback,而它是用来Js处理完成后把结果数据回调给Java层代码的。接着会根据message.handleName(在这个分析例子中,handleName的值就是”functionInJs”)在messageHandlers这个Map去获取handler,最后交给handler去处理。

function _dispatchMessageFromNative(messageJSON) {
    setTimeout(function() {
        var message = JSON.parse(messageJSON);
        var responseCallback;
        //java call finished, now need to call js callback function
        if (message.responseId) {
            ...
        } else {
            //直接发送
            if (message.callbackId) {
                var callbackResponseId = message.callbackId;
                responseCallback = function(responseData) {
                    _doSend({
                        responseId: callbackResponseId,
                        responseData: responseData
                    });
                };
            }

            var handler = WebViewJavascriptBridge._messageHandler;
            if (message.handlerName) {
                handler = messageHandlers[message.handlerName];
            }
            //查找指定handler
            try {
                handler(message.data, responseCallback);
            } catch (exception) {
                if (typeof console != 'undefined') {
                    console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception);
                }
            }
        }
    });
}

2.3.4 页面Html注册”functionInJs”方法

延续上面的分析,messageHandler是哪里设置的呢。答案就在当初webView.loadUrl(“file:///android_asset/demo.html”);加载的这个demo.html中。bridge.registerHandler(“functionInJs”, …)这里注册了”functionInJs”。

<html>
    <head>
    ...
    </head>
    <body>
    ...
    </body>
    <script>
        ...

        connectWebViewJavascriptBridge(function(bridge) {
            bridge.init(function(message, responseCallback) {
                console.log('JS got a message', message);
                var data = {
                    'Javascript Responds': '测试中文!'
                };
                console.log('JS responding with', data);
                responseCallback(data);
            });

            bridge.registerHandler("functionInJs", function(data, responseCallback) {
                document.getElementById("show").innerHTML = ("data from Java: = " + data);
                var responseData = "Javascript Says Right back aka!";
                responseCallback(responseData);
            });
        })
    </script>
</html>

2.3.5 “functionInJs”执行结果回传Java

“funciontInJs”执行完毕后调用的responseCallback正是_dispatchMessageFromNative()实例化的,而它实际会调用_doSend()方法。_doSend()方法会先把Message推送到sendMessageQueue中。然后修改messagingIframe.src,这里会出发Java层的WebViewClient.shouldOverrideUrlLoading()的回调。

function _doSend(message, responseCallback) {
    if (responseCallback) {
        var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
        responseCallbacks[callbackId] = responseCallback;
        message.callbackId = callbackId;
    }

    sendMessageQueue.push(message);
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
    在BridgeWebViewClient.shouldOverrideUrlLoading()里面,会先执行webView.flushMessageQueue()的分支。

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    try {
        url = URLDecoder.decode(url, "UTF-8");
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    }

    if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) { // 如果是返回数据
        webView.handlerReturnData(url);
        return true;
    } else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { //
        webView.flushMessageQueue();
        return true;
    } else {
        return super.shouldOverrideUrlLoading(view, url);
    }
}

webView.flushMessageQueue()首先去执行Js的_flushQueue()方法,并附带着CallBackFunction。Js的_flushQueue()方法会把sendMessageQueue中的所有message都回传给Java层。CallBackFunction就是把messageQueue解析出来后一个一个Message在for循环中处理,也正是在for循环中,”functionInJs”的Java层回调方法被执行了。

void flushMessageQueue() {
    if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
        loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {

            @Override
            public void onCallBack(String data) {
                // deserializeMessage
                List<Message> list = null;
                try {
                    list = Message.toArrayList(data);
                } catch (Exception e) {
                    e.printStackTrace();
                    return;
                }
                if (list == null || list.size() == 0) {
                    return;
                }
                for (int i = 0; i < list.size(); i++) {
                    ...
                }
            }
        });
    }
}

到此,JsBridge的调用流程就分析完毕了。虽然JsBridge使用了MessageQueue后,分析起来有点绕。但原理是不变的,Js调用Java是通过WebViewClient.shouldOverrideUrlLoading()。当然,还有在文章开头介绍另外3种方式。Java调用Js是通过WebView.loadUrl(“javascript:xxxx”)。

参考:

好好和h5沟通!几种常见的hybrid通信方式; Android 利用WebViewJavascriptBridge 实现js和java的交互; GitHub:lzyzsd/JsBridge;

原文发布于微信公众号 - QQ音乐技术团队(gh_287053a877e6)

原文发表时间:2016-05-12

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏潇涧技术专栏

Pury Project Analysis

Pury的源码:https://github.com/NikitaKozlov/Pury

912
来自专栏西二旗一哥

iOS - autoreleasepool and @autoreleasepool

+ 在一个自动引用计数的环境中(并不是垃圾回收机制),一个包含了多个对象的 NSAutoreleasePool 对象能够接收 autorelease 消息并且...

1534
来自专栏刘望舒

Android资源动态加载以及相关原理分析

思考 一般情况下,我们在设计一个插件化框架的时候,要解决的无非是下面几个问题: 四大组件的动态注册 组件相关的类的加载 资源的动态加载 实际上从目前的主流插件...

3268
来自专栏Android干货园

Android 拦截WebView加载URL,控制其加载CSS、JS资源

版权声明:本文为博主原创文章,转载请标明出处。 https://blog.csdn.net/lyhhj/article/details/49...

6902
来自专栏Android开发经验

webview调用android代码

1706
来自专栏Android先生

RxJava2 实战知识梳理(6) - 基于错误类型的重试请求

在网络请求时,有时候会出现需要进行重试的情况,重试的时候,有以下几点需要注意:

1141
来自专栏青蛙要fly的专栏

项目需求讨论 - 动态权限申请分析及相关第三方库源码分析

在具体项目开发中,关于Android的动态申请权限的功能,我想大家都见怪不怪了。很多人开发的app中也都使用过这块需求。

1594
来自专栏技术小黑屋

Android中WebView拦截替换网络请求数据

Android中处理网页时我们必然用到WebView,这里我们有这样一个需求,我们想让WebView在处理网络请求的时候将某些请求拦截替换成某些特殊的资源。具体...

1972
来自专栏Java帮帮-微信公众号-技术文章全总结

struts2+spring+hibernate整合步骤(1)

struts2、hibernate、spring所需jar包 struts-core-2.x.x.jar ----struts核心包 xwork-cor...

3484
来自专栏开发之途

Android IPC机制(2)-AIDL

1624

扫码关注云+社区

领取腾讯云代金券