前端網頁Javascript和Native互相調用在手機應用中愈來愈常見,JsBridge是最經常使用的解決方案。javascript
在Android開發中,能實現Javascript與Native代碼通訊的,有4種途徑:html
1.JavascriptInterface前端
2.WebViewClient.shouldOverrideUrlLoading()java
3.WebChromeClient.onConsoleMessage()android
4.WebChromeClient.onJsPrompt()git
這是Android提供的Javascript與Native通訊的官方解決方案。github
首先Java代碼要實現這麼一個類,它的做用是提供給Javascript調用。web
public class JavascriptInterface { @JavascriptInterface public void showToast(String toast) { Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show(); } }
而後把這個類添加到WebView的JavascriptInterface中。json
webView.addJavascriptInterface(new JavascriptInterface(), "javascriptInterface");
在Javascript代碼中就能直接經過「javascriptInterface」直接調用了該Native的類的方法。安全
function showToast(toast) { javascript:javascriptInterface.showToast(toast); }
可是這個官方提供的解決方案在Android4.2以前存在安全漏洞。在Android4.2以後,加入了@JavascriptInterface才獲得解決。因此考慮到兼容低版本的系統,JavascriptInterface並不適合。
這個方法的做用是攔截全部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); } }
這是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.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; } }
前文提到的4種通訊方式都是Javascript通訊Native的Java,而反過來,Java通訊Javascript只有一種方式。
那就是調用WebView.loadUrl()去執行一個預先定義好的Javascript方法。
webView.loadUrl(String.format("javascript:WebViewJavascriptBridge._handleMessageFromNative(%s)", data));
接下來會結合JsBridge這個開源組件來說解一下JsBridge的原理。
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); } }
在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方法了。
_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); } } } }); }
延續上面的分析,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>
"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")。