我們知道在 WebView
的使用中,WebView
與 Js 代碼的交互是非常重要的一部分,在上篇文章 WebView 的使用 中,簡單介紹了 WebView
與 Js 代碼的交互。JsBridge 是一個 WebView
與 Js 代碼交互的庫,封裝的非常完善,但是源碼也不算難,在 GitHub 上有 3000+ Star,值得分析一下。
1. WebView 與 Js 交互回顧
在 WebView 的使用 中已經對 WebView
與 Js 代碼進行了簡單的介紹:
-
Js 調用
WebView
的方法- 設置
WebView
支持 JavaScript - 定義用于交互的類,并用
@JavascriptInterface
關鍵字注解其中可以被 Js 調用的方法 - 使用
WebView
的addJavascriptInterface(Object object, String name)
方法將該類的方法傳入WebView
中,Js 便可以通過name
調用該對象
- 設置
-
WebView
調用 Js 代碼- 設置
WebView
支持 JavaScript - 使用
WebView
的loadUrl(String url)
便可以調用 Js 中的方法,如:webView.loadUrl("javascript:testConfirm()")
,testConfirm()
便是一個 Js 函數。
- 設置
2. JsBridge 源碼解析
JsBridge 使用起來很簡單,在 JsBridge 的 README 中有詳細的介紹,這里就不多做介紹,直接分析其源碼。
在 JsBridge 中有兩個非常重要的類:BridgeWebView 和 BridgeWebViewClient
2.1 Android 調用 Js 代碼
首先看一下,在 JsBridge 中,Android 是如何調用 Js 代碼的
-
在 Js 代碼中,聲明 Android 可以調用的方法,如下所示:
WebViewJavascriptBridge.registerHandler("functionInJs", function(data, responseCallback) { document.getElementById("show").innerHTML = ("data from Java: = " + data); var responseData = "Javascript Says Right back aka!"; responseCallback(responseData); }); bridge.init(function(message, responseCallback) { console.log('JS got a message', message); var data = { 'Javascript Responds': 'Wee!' }; console.log('JS responding with', data); responseCallback(data); });
-
Java 調用此 JavaScript 函數的方法如下所示:
webView.callHandler("functionInJs", new Gson().toJson(user), new CallBackFunction() { @Override public void onCallBack(String data) { } });
* 同時在 Android 中可以使用如下方法,向 JavaScript 發送數據:
``` Java
webView.send("hello");
接下來分析一下 JsBridge 中是如何實現 Java 調用 Js 方法的
@SuppressLint("SetJavaScriptEnabled")
public class BridgeWebView extends WebView implements WebViewJavascriptBridge {
......
Map<String, CallBackFunction> responseCallbacks = new HashMap<>();
private List<Message> startupMessage = new ArrayList<>();
public List<Message> getStartupMessage() {
return startupMessage;
}
public void setStartupMessage(List<Message> startupMessage) {
this.startupMessage = startupMessage;
}
private void init() {
this.setVerticalScrollBarEnabled(false);
this.setHorizontalScrollBarEnabled(false);
this.getSettings().setJavaScriptEnabled(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
WebView.setWebContentsDebuggingEnabled(true);
}
this.setWebViewClient(generateBridgeWebViewClient());
}
protected BridgeWebViewClient generateBridgeWebViewClient() {
return new BridgeWebViewClient(this);
}
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;
}
}
@Override
public void send(String data) {
send(data, null);
}
@Override
public void send(String data, CallBackFunction responseCallback) {
doSend(null, data, 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);
}
private void queueMessage(Message m) {
if (startupMessage != null) {
startupMessage.add(m);
} else {
dispatchMessage(m);
}
}
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);
}
}
public void loadUrl(String jsUrl, CallBackFunction returnCallback) {
this.loadUrl(jsUrl);
responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl), returnCallback);
}
/**
* call javascript registered handler
*
* @param handlerName
* @param data
* @param callBack
*/
public void callHandler(String handlerName, String data, CallBackFunction callBack) {
doSend(handlerName, data, callBack);
}
}
- 從上面 BridgeWebView 源碼中可以看到,不論是通過
callHandler(String handlerName, String data, CallBackFunction callBack)
方法還是通過send(String data, CallBackFunction responseCallback)
方法,最后都走到queueMessage(Message m)
中。 - 在
queueMessage(Message m)
方法中
- 如果
startupMessage
為空,則直接通過dispatchMessage(Message m)
方法,在其中生成調用 Js 方法的命令行,并通過WebView.loadUrl(String url)
方法調用 Js 中的方法 - 若
startupMessage
不為空,則將該Message
對象加入到startupMessage
消息列表中,那么消息列表是在何處調用呢?
- BridgeWebViewClient 的源碼如下:
public class BridgeWebViewClient extends WebViewClient {
private BridgeWebView webView;
public BridgeWebViewClient(BridgeWebView webView) {
this.webView = webView;
}
@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);
}
}
@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);
}
}
}
可以看到,在 BridgeWebViewClient
的 onPageFinished(WebView view, String url)
方法中,會將 BridgeWebView
中的 List<Message> startupMessage
對象中的消息都通過 dispatchMessage(Message m)
將消息發送出去,也就是 Java 調用 Js 中的方法。
- 在
BridgeWebView.doSend(String handlerName, String data, CallBackFunction responseCallback)
方法中會將CallBackFunction responseCallback
方法加入Map<String, CallBackFunction> responseCallbacks
回調集合中,在BridgeWebViewClient.shouldOverrideUrlLoading(WebView view, String url)
攔截傳遞進來的String url
數據,并進行判斷,若是 Java 調用 Js 方法處理的結果,則找到對應的CallBackFunction
對象將結果返回。
可以看到,在 JsBridge 中還是通過 WebView.loadUrl(String url)
的方法實現 Java 調用 Js 方法的功能的。
2.2 Js 調用 Android 方法
看一下 Js 代碼如何調用 Android 中的方法
在 Java 中注冊 Js 可以調用的方法,如下所示:
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");
}
});
并在 Js 中進行調用:
WebViewJavascriptBridge.callHandler(
'submitFromWeb'
, {'param': str1}
, function(responseData) {
document.getElementById("show").innerHTML = "send get responseData from java, data = " + responseData
}
);
- 首先將回調方法
BridgeHandler handler
對象,通過registerHandler(String handlerName, BridgeHandler handler)
方法添加到BridgeWebView
中的Map<String, BridgeHandler> messageHandlers
集合中 - 當 Js 代碼調用 Java 方法時,通過
BridgeWebViewClient.shouldOverrideUrlLoading(WebView view, String url)
方法進行攔截,并通過BridgeWebView.flushMessageQueue()
方法去主動獲取 Js 中的消息隊列,再通過BridgeWebViewClient.shouldOverrideUrlLoading(WebView view, String url)
攔截得到獲取到的數據 - 得到獲取到的數據之后,找到
Map<String, BridgeHandler> messageHandlers
中的回調方法,再將結果通過BridgeHandler handler
對象回調
3.總結
其實,在 Js 和 WebView
交互的過程中,主要實現兩個方向可以通信即可。
-
WebView
向 Js 傳遞數據是通過WebView.loadUrl(String url)
實現的 - 在
WebView
中接收 Js 傳遞的數據是通過WebViewClient
中的shouldOverrideUrlLoading(WebView view, String url)
攔截加載鏈接String url
參數實現的。
參考資料: