前端网页Javascript和Native互相调用在手机应用中越来越常见,JsBridge是最常用的解决方案。

在Android开发中,能实现Javascript与Native代码通信的,有4种途径:

1.JavascriptInterface

2.WebViewClient.shouldOverrideUrlLoading()

3.WebChromeClient.onConsoleMessage()

4.WebChromeClient.onJsPrompt()

JavascriptInterface

这是Android提供的Javascript与Native通信的官方解决方案。

首先Java代码要实现这么一个类,它的作用是提供给Javascript调用。

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

在Javascript代码中就能直接通过“javascriptInterface”直接调用了该Native的类的方法。

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

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

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

WebChromeClient.onConsoleMessage()

这是Android提供给Javascript调试在Native代码里面打印日志信息的API,同时这也成了其中一种Javascript与Native代码通信的方法。

在Javascript代码中调用console.log('xxx')方法。

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

就会在Native代码的WebChromeClient.consoleMessage()中得到回调。

consoleMessage.message()获得的正是Javascript代码console.log('xxx')的内容。

public class CustomWebChromeClient extends WebChromeClient {

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

WebChromeClient.onJsPrompt()

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

Javascript中调用

window.prompt(message, value)

WebChromeClient.onJsPrompt()就会受到回调。

onJsPrompt()方法的message参数的值正是Javascript的方法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;
}
}

JsBridge

前文提到的4种通信方式都是Javascript通信Native的Java,而反过来,Java通信Javascript只有一种方式。

那就是调用WebView.loadUrl()去执行一个预先定义好的Javascript方法。

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

接下来会结合JsBridge这个开源组件来讲解一下JsBridge的原理。

1. WebView加载html页面

webView.registerHandler("submitFromWeb", ...);这是Java层注册了一个叫"submitFromWeb"的接口方法,目的是提供给Javascript来调用。这个"submitFromWeb"的接口方法的回调就是BridgeHandler.handler()。

webView.callHandler("functionInJs", ..., new CallBackFunction());这是Java层主动调用Javascript的"functionInJs"方法。

package com.github.lzyzsd.jsbridge.example;

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()方法,这里想解释下callbackId。

callbackId生成后不仅仅会被传到Javascript,而且会以key-value对的形式和responseCallback配对保存到responseCallbacks这个Map里面。

它的目的,就是为了等Javascript把处理结果回调给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这个Javascript方法。那这个Javascript的方法是哪里来的呢?

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. 页面加载完成后会加在一段Javascript。

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

我们看看WebViewJavascriptBridge.js的代码,就能找到function _handleMessageFromNative()这个Javascript方法了。

3. 接下来分析下WebViewJavascriptBridge.js

_handleMessageFromNative()方法里面会调用_dispatchMessageFromNative()方法。

当处理来自Java层的主动调用时候会走“直接发送”的else分支。

message.callbackId会被取出来,实例化一个responseCallback,而它是用来Javascript处理完成后把结果数据回调给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);
}
}
}
});

4. 页面注册的"functionInJs"方法,提供给Java调用Javascript的。

延续上面的分析,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>

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()首先去执行Javascript的_flushQueue()方法,并附带着CallBackFunction。

Javascript的_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后,分析起来有点绕,但原理是不变的。

Javascript调用Java是通过WebViewClient.shouldOverrideUrlLoading()。当然,还有在文章开头介绍另外3种方式。

Java调用Javascript是通过WebView.loadUrl("javascript:xxxx")。

参考:

好好和h5沟通!几种常见的hybrid通信方式

Android 利用WebViewJavascriptBridge 实现js和java的交互

GitHub:lzyzsd/JsBridge

JsBridge实现Javascript和Java的互相调用的更多相关文章

  1. javascript与java的相互调用,纯java的javascript引擎rhino(转载)

    1.下载Rhino安装包,下载地址:官网http://www.mozilla.org/rhino. 2.rhino环境配置,把解压出来的js.jar文件加入到系统的环境变量classpath 3.在命 ...

  2. 在Java中直接调用js代码(转载)

    http://blog.csdn.net/xzyxuanyuan/article/details/8062887 JDK1.6版添加了新的ScriptEngine类,允许用户直接执行js代码. 在Ja ...

  3. 在Java中直接调用js代码

    JDK1.6版添加了新的ScriptEngine类,允许用户直接执行js代码. 在Java中直接调用js代码 不能调用浏览器中定义的js函数,会抛出异常提示ReferenceError: “alert ...

  4. 关于JavaScript和Java的区别和联系

    转载自: Javascript和Java除了名字和语法有点像,其他没有任何的关系. 做个比较是为了让大家更好的理解Javascript,事实上,两种语言根本没有可比性,是完全不同的.   Javasc ...

  5. Java的传值调用

    (本文非引战或diss,只是说出自己的理解,欢迎摆正心态观看或探讨) 引子 之所以写这篇文章是因为前些天写了一篇<Java中真的只有值传递么?>探讨了网上关于Java只有值传递的说法,当时 ...

  6. JavaScript与java语法区别

    网页中各种技术的作用 感谢大佬:https://blog.csdn.net/RookiexiaoMu_a/article/details/89052768 HTML 制作网页的结构 CSS 美化网页 ...

  7. 【历史】JavaScript和Java没啥关系!————JavaScript简史

    文章的开始先上张图: 图片拍摄自北京图书大厦,代表着现在国内应该是绝大部分书店的现状--Javascript书籍放在Java类当中.甚至很多业内人也一直认为Javascript是Java语言在浏览器内 ...

  8. JavaScript和Java之间的关系

    今天来简单而又详细地说说JavaScript和Java的关系. 开门见山总结性一句话,它们之间的关系 = 雷锋和雷峰塔之间的关系,换句话说:它们之间没什么关系. 但往往有不少初学者甚至中级者认为它们之 ...

  9. 在网页程序或Java程序中调用接口实现短信猫收发短信的解决方案

    方案特点: 在网页程序或Java程序中调用接口实现短信猫收发短信的解决方案,简化软件开发流程,减少各应用系统相同模块的重复开发工作,提高系统稳定性和可靠性. 基于HTTP协议的开发接口 使用特点在网页 ...

随机推荐

  1. [转]Redis之七种武器

    长生剑.孔雀翎.碧玉刀.多情环.离别钩.霸王枪.拳头是古龙笔下的七种武器,而本文打算将Redis的几种使用方式 Strings.Hashs.Lists.Sets.Sorted Sets.Pub/Sub ...

  2. 基于VC的ACM音频编程接口压缩Wave音频(三)

    (三)音 频 数 据 的 压 缩 下 面 说 明 使 用 CODEC 实 现 音 频 压 缩 的 过 程:假 设 源 信 号 为8K 采 样.16bits PCM 编 码. 单 声 道. 长 度 为1 ...

  3. android5.0 aosp编译记录(由于机器硬件原因,改为4.4.2编译通过)

    编译环境必须是64位系统啊,妥妥的又装了64位的ubuntu,虚拟机推荐server版本,不带x省性能…… 接着要装openjdk 1.7,记得更新一下系统的源,下面这个不错 deb http://m ...

  4. 运维自动化工具---Puppet

    案例环境:-----------------------------------------------------------------主机  操作系统   IP地址  主要软件--------- ...

  5. ios图片添加文字或者水印

    在项目中,我们会对图片做一些处理,但是我们要记住,一般在客户端做图片处理的数量不宜太多,因为受设备性能的限制,如果批量的处理图片,将会带来交互体验性上的一些问题.首先让我们来看看在图片上添加文字的方法 ...

  6. oracle数据库常用查询

    一.数据库信息 1.数据库时间 select to_char(sysdate, 'yyyy-mm-dd hh24:mi:ss') AS dbtime from dual; 2.主机OS类型 SELEC ...

  7. wpa_supplicant代码走读

    wpa_supplicant_add_iface wpa_supplicant_init_iface wpa_supplicant_set_driver wpa_config_read wpa_sup ...

  8. Excel VBA自定义函数编写(UDF, User-Defined Function)

    虽然知道Microsoft Office Excel可以支持用VB语言来进行复杂的编程和自定义函数的编写,但是一直以来都没有这个需求. 这次遇到的问题是要根据一列数组计算出一个值,但计算过程又比较复杂 ...

  9. STC12C5A60S2笔记6(中断)

    1. 基本特性 1) 中断源 STC12C5A60S2共有十个中断源,每个中断源可设置4类优先级:当相同优先级下各中断优先级由高到低依次如下: 1.1)INT0(外部中断0) 中断向量地址 0003H ...

  10. 【读书笔记】Programming Entity Framework CodeFirst -- 初步认识

    以下是书<Programming Entity Framework Code First>的学习整理,主要是一个整体梳理. 一.模型属性映射约定 1.通过 System.Component ...