本篇文章已授權微信公眾號 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的過程類似:
是不是感覺反而更復雜了???其實只要捉住主要的三點,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