Android与JS之JsBridge使用与源码分析
本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
在Android开发中,由于Native开发的成本较高,H5页面的开发更灵活,修改成本更低,因此前端网页JavaScript(下面简称JS)与Java之间的互相调用越来越常见。
JsBridge就是一个简化Android与JS通信的框架,源码:https://github.com/lzyzsd/JsBridge
我们今天通过一个简单栗子来分析下开源框架JsBridge的源码。栗子的代码我也放在Github,有需要的可以seesee:
https://github.com/juexingzhe/Android_JS
栗子很简单,随便输入信息登陆,会加载一个H5页面,在H5界面点击按钮,Java执行getUserInfo()然后将UserInfo回传给JS,H5页面再显示UserInfo。
JS调用Android基本有下面三种方式
webView.addJavascriptInterface()
WebViewClient.shouldOverrideUrlLoading()
WebChromeClient.onJsAlert()/onJsConfirm()/onJsPrompt() 方法分别回调拦截JS对话框alert()、confirm()、prompt()消息
Android调用JS
webView.loadUrl();
webView.evaluateJavascript()
常用方法的使用后面栗子中会用到,更细节的介绍各位同学可以去网上搜搜看看。
1.JsBridge使用
我们先来看下Java层的代码
首先引入依赖和仓库
dependencies {
……
compile 'com.github.lzyzsd:jsbridge:1.0.4'
compile 'com.google.code.gson:gson:2.7'
}
repositories {
jcenter()
maven { url "https://jitpack.io" }
}
准备工作就是这样,下面可以开始撸代码,首先就是点击按钮登陆,这个简单:
Intent intent = new Intent(LoginActivity.this, WebActivity.class);
intent.putExtra("email", mEmailView.getText().toString());
startActivity(intent);
布局文件中要使用BridgeWebView:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.github.lzyzsd.jsbridge.BridgeWebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
在跳转后的页面,获取登陆信息并存储,再通过loadUrl加载H5页面:
Intent intent = this.getIntent();
String email = intent.getStringExtra("email");
mUserInfo = new UserInfo(email);
mBridgeWebView = (BridgeWebView) findViewById(R.id.web_view);
mBridgeWebView.setDefaultHandler(new DefaultHandler());
mBridgeWebView.loadUrl("file:///android_asset/getuserinfo.html");
registerHandler();
主要是要注册Handler,供JS调用,
getUserInfo就是注册供JS调用的Handler的id
data是JS传过来的参数
CallBackFunction 函数中需要把JS需要的response返回给JS
private void registerHandler() {
mBridgeWebView.registerHandler("getUserInfo", new BridgeHandler() {
@Override
public void handler(String data, CallBackFunction function) {
Log.i(TAG, "handler = getUserInfo, data from web = " + data);
function.onCallBack(new Gson().toJson(mUserInfo));
}
});
}
Java层的代码就这么简单,下面看下JS层工作:
首先需要一个js文件,我们写一个getuserinfo.html文件放在assets目录下,文件内容,不建议把js代码直接放在html文件中,我为了方便直接就写在这了。代码放了两个段落,一个类似于TextView用来显示用户信息,一个Button。点击按钮会调用callHandler,三个参数和Java层一一对应,在Java层返回的时候,会调用function(responseData)函数,显示用于信息。
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type">
<title>
js调用java
</title>
</head>
<body>
<p>
<xmp id="show">
</xmp>
</p>
<div align="center">
<p>
<input type="button" id="enter" value="获取用户信息" onclick="getUserInfo();"
/>
</p>
</div>
</body>
<script>
function getUserInfo(){
window.WebViewJavascriptBridge.callHandler(
'getUserInfo',
{'info': 'I am JS, want to get UserInfo from Java'},
function(responseData) {
document.getElementById("show").innerHTML = "repsonseData from java,\ndata = " + responseData;
}
)
}
</script>
</html>
使用基本就是这样了,可以看出来JsBridge通过封装,JS和Java之间的通信只需要实现两个步骤,使用起来很方便。
我们来看下源码是怎么个玩法,先来个华丽丽的分割线
2.JsBridge源码分析
分析之前我把JS调用Java画了个简易交互图,Java调用JS的过程类似:
4.png是不是感觉反而更复杂了???其实只要捉住主要的三点,JsBridge就原形毕露:
1.Android调用JS是通过loadUrl(url),url中可以拼接要传给JS的对象
2.JS调用Android是通过shouldOverrideUrlLoading
3.JsBridge将沟通数据封装成Message,然后放进Queue,再将Queue进行传输
接下来我们来一步一步跟踪上面栗子的调用过程:
- JS层点击按钮调用callHandler
handlerName,Java和JS要一致,
data是Java层handlerName函数执行的参数
responseCallback是Java执行完handlerName返回时,JS回调的接口,是JS执行
onclick="getUserInfo();"
function getUserInfo(){
window.WebViewJavascriptBridge.callHandler(
'getUserInfo',
{'info': 'I am JS, want to get UserInfo from Java'},
function(responseData) {
document.getElementById("show").innerHTML = "repsonseData from java,\ndata = " + responseData;
}
)
}
callHandler会调用_doSend
如果JS需要回调,就将回调的callbackId放进message中,Java执行完会传回callbackId,这里是cb_1_1495182409011
构造完message放进队列sendMessageQueue
通过iframe属性给Java发送通知消息,消息结构yy://QUEUE_MESSAGE/
function callHandler(handlerName, data, responseCallback) {
_doSend({
handlerName: handlerName,
data: data
}, responseCallback);
}
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;
}
- Java收到通知消息
WebView在shouldOverrideUrlLoading拦截到url:yy://QUEUE_MESSAGE/
然后会执行webView.flushMessageQueue(),在主线程执行loadUrl通知JS层推送队列到Java;
JS_FETCH_QUEUE_FROM_JAVA = "javascript:WebViewJavascriptBridge._fetchQueue();"
调用JS层的_fetchQueue,通知JS层发送队列到Java层
在responseCallbacks中注册回调接口,接口id是函数名_fetchQueue,在JS推送消息队列时进行回调
void flushMessageQueue() {
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {
@Override
public void onCallBack(String data) {
//
});
}
}
public void loadUrl(String jsUrl, CallBackFunction returnCallback) {
this.loadUrl(jsUrl);
responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl), returnCallback);
}
- JS 发送Request Queue
执行_fetchQueue
将sendMessageQueue转化成JSON
通过iframe属性给Java发送通知消息,消息结构:yy://return/_fetchQueue/消息队列的内容
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
//android can't read directly the return data, so we can reload iframe src to communicate with java
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
}
- Java收到调用通知,进行处理并发送Response Queue到JS
WebView在shouldOverrideUrlLoading会拦截到url:
yy://return/_fetchQueue/[{"handlerName":"getUserInfo","data":{"info":"I am JS, want to get UserInfo from Java"},"callbackId":"cb_1_1495180503779"}]
执行webView.handlerReturnData(url);
根据函数名_fetchQueue拿到之前注册的回调函数CallBackFunction returnCallback
执行回调函数,并且从注册中移除
void handlerReturnData(String url) {
String functionName = BridgeUtil.getFunctionFromReturnUrl(url);
CallBackFunction f = responseCallbacks.get(functionName);
String data = BridgeUtil.getDataFromReturnUrl(url);
if (f != null) {
f.onCallBack(data);
responseCallbacks.remove(functionName);
return;
}
}
接下来就是对Request Queue的解析然后找到JS希望调用Handler并且执行,代码中我写了注释,可以直接看:
//回调接口执行onCallBack函数
//其中data [{"handlerName":"getUserInfo","data":{"info":"I am JS, want to get UserInfo from Java"},"callbackId":"cb_1_1495180503779"}]
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 {
//将JSON数组转化成Java list
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++) {
//从list中取出Message
Message m = list.get(i);
//在我们的栗子中没有responseId,因此到else分支
String responseId = m.getResponseId();
// 是否是response
if (!TextUtils.isEmpty(responseId)) {
CallBackFunction function = responseCallbacks.get(responseId);
String responseData = m.getResponseData();
function.onCallBack(responseData);
responseCallbacks.remove(responseId);
} else {
CallBackFunction responseFunction = null;
// if had callbackId
//如果有callbackId就说明JS需要回调,因此Java层需要构造responseMsg
//从message中取出callbackId,放进responseMsg
final String callbackId = m.getCallbackId();
if (!TextUtils.isEmpty(callbackId)) {
responseFunction = new CallBackFunction() {
@Override
public void onCallBack(String data) {
Message responseMsg = new Message();
responseMsg.setResponseId(callbackId);
responseMsg.setResponseData(data);
queueMessage(responseMsg);
}
};
} else {
responseFunction = new CallBackFunction() {
@Override
public void onCallBack(String data) {
// do nothing
}
};
}
BridgeHandler handler;
//从message中取出Handler名字,再从messageHandlers中取
//如果没有就使用默认的Handler
if (!TextUtils.isEmpty(m.getHandlerName())) {
handler = messageHandlers.get(m.getHandlerName());
} else {
handler = defaultHandler;
}
if (handler != null){
//执行handler
handler.handler(m.getData(), responseFunction);
}
}
}
}
});
}
}
那么这个handler是什么?就是Java调用registerHandler注册的getUserInfo
private void registerHandler() {
mBridgeWebView.registerHandler("getUserInfo", new BridgeHandler() {
@Override
public void handler(String data, CallBackFunction function) {
Log.i(TAG, "handler = getUserInfo, data from web = " + data);
function.onCallBack(new Gson().toJson(mUserInfo));
}
}
上面的function就是在flushMessageQueue 解析时构造的responseFunction,在message中包括JS层需要回调的函数Id,然后就是getUserInfo执行的结果
调用queueMessage
responseFunction = new CallBackFunction() {
@Override
public void onCallBack(String data) {
Message responseMsg = new Message();
responseMsg.setResponseId(callbackId);
responseMsg.setResponseData(data);
queueMessage(responseMsg);
}
};
queueMessage调用dispatchMessage发送message给JS
通过构造String指令,然后loadUrl执行JS代码,注意对象也是通过这样方式传递过去的,就类似调用本地函数,不发起网络请求
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);
}
}
其中
BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA ="javascript:WebViewJavascriptBridge._handleMessageFromNative('%s');"
javascriptCommand = javascript:WebViewJavascriptBridge._handleMessageFromNative('{\"responseData\":\"{\\\"email\\\":\\\"jue@126.com\\\"}\",\"responseId\":\"cb_1_1495182558893\"}');
//data = {\"responseData\":\"{\\\"email\\\":\\\"jue@126.com\\\"}\",\"responseId\":\"cb_1_1495182409011\"}
- JS收到Response JSON
来到_handleMessageFromNative,
function _handleMessageFromNative(messageJSON) {
console.log(messageJSON);
if (receiveMessageQueue && receiveMessageQueue.length > 0) {
receiveMessageQueue.push(messageJSON);
} else {
_dispatchMessageFromNative(messageJSON);
}
}
最后都会调用到_dispatchMessageFromNative,由于是JS主动调用Java,因此有responseId,执行registerHandler时传入的CallBack,也就是显示用户信息。我在代码里加了注释,很容易看懂。
function _dispatchMessageFromNative(messageJSON) {
setTimeout(function() {
//将数据解析成JSON
var message = JSON.parse(messageJSON);
var responseCallback;
//java call finished, now need to call js callback function
//根据responseId:cb_1_1495182409011拿到responseCallback,就是我们前门注册的alert
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[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);
}
}
}
});
}
源码的分析就到这结束了,代码不多,但是封装的接口很是好用。最后再来个分割线~~
3.总结
最后总结下,使用上很方便主要两个步骤
被调用方注册Handler
registerHandler(String handlerName, BridgeHandler handler)
调用方调用Handler
callHandler(String handlerName, String data, CallBackFunction callBack)
原理上还是那三句话,请原谅我从上面直接copy过来:
1.Android调用JS是通过loadUrl(url),url中可以拼接要传给JS的对象
2.JS调用Android是通过shouldOverrideUrlLoading
3.JsBridge将沟通数据封装成Message,然后放进Queue,再将Queue进行传输
好了,今天我们JsBridge的使用和源码分析就到这了,谢谢!
文中栗子的链接:
https://github.com/juexingzhe/Android_JS
欢迎关注公众号:JueCode