首先咱們來了解一下什麼是JSBridge和爲何要使用JSBridge?javascript
在開發中,爲了追求開發的效率以及移植的便利性,一些展現性強的頁面咱們會偏向於使用h5來完成,功能性強的頁面咱們會偏向於使用native來完成,而一旦使用了h5,爲了在h5中儘量的獲得native的體驗,咱們native層須要暴露一些方法給js調用,好比,彈Toast提醒,彈Dialog,分享等等,有時候甚至把h5的網絡請求放到native去完成。前端
JSBridge作得好的一個典型就是微信,微信給開發者提供了JSSDK,該SDK中暴露了不少微信native層的方法,好比支付,定位等。java
本文將對js和Native的通訊原理和實現方法的一些探討。web
Android中的JSBridge是H5與Native通訊的橋樑,其做用是實現H5與Native間的雙向通訊。要實現H5與Native的雙向通訊,解決以下四個問題便可:面試
一、Java如何調用JavaScriptjson
二、JavaScript如何調用Java安全
三、方法參數以及回調如何處理性能優化
四、通訊協議的制定微信
下面從以上問題依次開始討論網絡
在WebView中,若是java要調用js的方法,是很是容易作到的,使用WebView.loadUrl(「javascript:function()」)便可,這樣,就作到了JSBridge的native層調用h5層的單向通訊
WebView.loadUrl("javascript:function()");
JavaScript如何調用Java
js調用Android的方法有如下四種:
一、WebView 的 andJavascriptInterface
二、WebViewClient.shouldOverrideUrlLoading()
三、WebChromeClient.onConsoleMessage()
四、WebChromeClient.onJsPrompt()、onJsAlert()、onJsConfirm()
咱們先對此四種方案進行一個詳細的描述,最後選擇一個方案便可。本文章中採用了第四種方案。
JavascriptInterface是Android官方提供的js和Native通訊方案。其實現以下:
一、實現一個java類,供js調用
public class MyJavascriptInterface { @JavascriptInterface public void showToast(String toast) { Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show(); } }
二、在webView中註冊這個類
webView.addJavascriptInterface(new MyJavascriptInterface(), "javascriptInterface");
三、在js中直接調用這個接口:
function showToast(text){ window.javascriptInterface.showToast(text); }
四、總結
大多數人都知道WebView存在一個漏洞,見WebView中接口隱患與手機掛馬利用,雖然該漏洞已經在Android 4.2上修復了(即便用@JavascriptInterface代替addJavascriptInterface),可是因爲兼容性和安全性問題,基本上咱們不會再利用Android系統爲咱們提供的addJavascriptInterface方法或者@JavascriptInterface註解來實現,因此咱們只能另闢蹊徑,去尋找既安全,又能實現兼容Android各個版本的方案。
這個方法是攔截全部webView的跳轉,頁面能夠構造一個特殊格式的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); } }
在js中執行console.log(), 會進入Android的WebChromeClient.consoleMessage()回調。
public class CustomWebChromeClient extends WebChromeClient { @Override public boolean onConsoleMessage(ConsoleMessage consoleMessage) { super.onConsoleMessage(consoleMessage); String msg = consoleMessage.message();//Javascript輸入的Log內容 } }
一、在WebView有一個方法,叫setWebChromeClient,能夠設置WebChromeClient對象,而這個對象中有三個方法,分別是onJsAlert,onJsConfirm,onJsPrompt,當js調用window對象的對應的方法,即window.alert,window.confirm,window.prompt,WebChromeClient對象中的三個方法對應的就會被觸發,那這三個方法到底要使用哪一個呢?
二、這三個方法的區別,能夠詳見w3c JavaScript 消息框 。
三、通常來講,咱們是不會使用onJsAlert的,爲何呢?由於js中alert使用的頻率仍是很是高的,一旦咱們佔用了這個通道,alert的正常使用就會受到影響,而confirm和prompt的使用頻率相對alert來講,則更低一點。
四、那麼究竟是選擇confirm仍是prompt呢,其實confirm的使用頻率也是不低的,好比你點一個連接下載一個文件,這時候若是須要彈出一個提示進行確認,點擊確認就會下載,點取消便不會下載,相似這種場景仍是不少的,所以不能佔用confirm。
五、而prompt則不同,在Android中,幾乎不會使用到這個方法,就是用,也會進行自定義,因此咱們徹底可使用這個方法。該方法就是彈出一個輸入框,而後讓你輸入,輸入完成後返回輸入框中的內容。所以,佔用prompt是再完美不過了。
public class CustomWebChromeClient extends WebChromeClient { @Override public boolean onJsPrompt() { super.onJsPrompt(); ... } }
myWebView.setWebChromClient(new CustomWebChromeClient());
一、任何IPC通訊都涉及到參數序列化的問題,同理,Java與JavaScript之間只能傳遞基礎類型(包括基本類型和字符串),不包括其餘對象或者函數。因此能夠採用json格式來傳遞數據。
二、爲了實現異步返回結果,因此JavaScript與Java相互調用不能直接獲取返回值,只能經過回調的方式來獲取返回結果。
要進行正常的通訊,通訊協議的制定是必不可少的。咱們回想一下熟悉的http請求url的組成部分。形如http://host:port/path?param=value, 咱們參考http,制定JSBridge的組成部分
jsbridge://className:callbackAddress/methodName?jsonObj // className: 表示java的類名 // callbackAddress: js回調的標識 // methodName: java中的方法名 // jsonObj: 接口數據
調用流程:
一、在js中,能夠採用以下方法調用java方法
var JSBridge = { call: function(className, method, params, callback) { var uri = 'jsbridge://' + className + ':' + callback + '/' + method + '?' + params; window.prompt(uri, ""); } } // 下面會調用java中的 bridge.showToast方法 JSBridge.call('bridge', 'showToast', {'msg':'Hello JSBridge'}, function(res) { alert(JSON.stringify(res)) });
二、在java中, 能夠以下實現:
// 進入prompt回調 public class JSBridgeWebChromeClient extends WebChromeClient { @Override public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { result.confirm(JSBridge.callJava(view, message)); return true; } } // 調用java邏輯 public class JSBridge { ... public static String callJava(WebView webView, String uriString) { String methodName = ""; String className = ""; String param = "{}"; String port = ""; if (!TextUtils.isEmpty(uriString) && uriString.startsWith("JSBridge")) { Uri uri = Uri.parse(uriString); className = uri.getHost(); param = uri.getQuery(); port = uri.getPort() + ""; String path = uri.getPath(); if (!TextUtils.isEmpty(path)) { methodName = path.replace("/", ""); } } // 基於上面的className、methodName和port path調用對應類的方法 if (exposedMethods.containsKey(className)) { HashMap<String, Method> methodHashMap = exposedMethods.get(className); if (methodHashMap != null && methodHashMap.size() != 0 && methodHashMap.containsKey(methodName)) { Method method = methodHashMap.get(methodName); if (method != null) { try { method.invoke(null, webView, new JSONObject(param), new Callback(webView, port)); } catch (Exception e) { e.printStackTrace(); } } } } return null; } } // 直接進入showToast函數的實現 public static void showToast(WebView webView, JSONObject param, final Callback callback) { String message = param.optString("msg"); Toast.makeText(webView.getContext(), message, Toast.LENGTH_SHORT).show(); if (null != callback) { try { JSONObject object = new JSONObject(); object.put("key", "value"); object.put("key1", "value1"); callback.apply(getJSONObject(0, "ok", object)); } catch (Exception e) { e.printStackTrace(); } } } // 上述程序的callback.apply方法實現以下: 即經過webView.loadUrl實現java調用js的方法 public class Callback { private static Handler mHandler = new Handler(Looper.getMainLooper()); private static final String CALLBACK_JS_FORMAT = "javascript:JSBridge.onFinish('%s', %s);"; private String mPort; private WeakReference<WebView> mWebViewRef; public Callback(WebView view, String port) { mWebViewRef = new WeakReference<>(view); mPort = port; } public void apply(JSONObject jsonObject) { final String execJs = String.format(CALLBACK_JS_FORMAT, mPort, String.valueOf(jsonObject)); if (mWebViewRef != null && mWebViewRef.get() != null) { mHandler.post(new Runnable() { @Override public void run() { mWebViewRef.get().loadUrl(execJs); } }); } } }
JSBridge類管理暴露給前端方法,前端調用的方法應該在此類中註冊纔可以使用。register的實現是從Map中查找key是否存在,不存在則反射取得對應class中的全部方法,具體方法是在BridgeImpl中定義的,方法包括三個參數分別爲WebView、JSONObject、CallBack。若是知足條件,則將全部知足條件的方法put到map中。
private static Map<String, HashMap<String, Method>> exposedMethods = new HashMap<>(); public static void register(String exposedName, Class<? extends IBridge> clazz) { if (!exposedMethods.containsKey(exposedName)) { try { exposedMethods.put(exposedName, getAllMethod(clazz)); } catch (Exception e) { e.printStackTrace(); } } }
JSBridge類中的callJava方法就是將js傳遞過來的URL解析,根據將要調用的類名從剛剛創建的Map中找出,根據方法名調用具體的方法,並將解析出的三個參數傳遞進去。
public static String callJava(WebView webView, String uriString) { String methodName = ""; String className = ""; String param = "{}"; String port = ""; if (!TextUtils.isEmpty(uriString) && uriString.startsWith("JSBridge")) { Uri uri = Uri.parse(uriString); className = uri.getHost(); param = uri.getQuery(); port = uri.getPort() + ""; String path = uri.getPath(); if (!TextUtils.isEmpty(path)) { methodName = path.replace("/", ""); } } if (exposedMethods.containsKey(className)) { HashMap<String, Method> methodHashMap = exposedMethods.get(className); if (methodHashMap != null && methodHashMap.size() != 0 && methodHashMap.containsKey(methodName)) { Method method = methodHashMap.get(methodName); if (method != null) { try { method.invoke(null, webView, new JSONObject(param), new Callback(webView, port)); } catch (Exception e) { e.printStackTrace(); } } } } return null; }
CallBack類是用來回調js中回調方法的Java對應類。Java層處理好的返回結果是經過CallBack類來實現的。在這個回調類中傳遞的參數是JSONObject(返回結果)、WebView和port,port應與js傳遞過來的port相對應。
private static Handler mHandler = new Handler(Looper.getMainLooper()); private static final String CALLBACK_JS_FORMAT = "javascript:JSBridge.onFinish('%s', %s);"; private String mPort; private WeakReference<WebView> mWebViewRef; public Callback(WebView view, String port) { mWebViewRef = new WeakReference<>(view); mPort = port; } public void apply(JSONObject jsonObject) { final String execJs = String.format(CALLBACK_JS_FORMAT, mPort, String.valueOf(jsonObject)); if (mWebViewRef != null && mWebViewRef.get() != null) { mHandler.post(new Runnable() { @Override public void run() { mWebViewRef.get().loadUrl(execJs); } }); } }
在java層的JSBridge中註冊方法,例如
JSBridge.register("bridge", BridgeImpl.class);