Android JsBridge 就是用來在 Android app的原生 java 代碼與 javascript 代碼中架設通訊(調用)橋樑的輔助工具。javascript
原文地址點這裏html
github點這裏java
使用方式戳這裏android
有問題請聯繫 xesamgit
Javascript 運行在 WebView 中,而 WebView 只是 Javascript 執行引擎與頁面渲染引擎的一個包裝而已。github
因爲這種自然的隔離效應,咱們能夠將這種狀況與 IPC 進行類比,將 Java 與 Javascript 的每次互調都看作一次 IPC 調用。
如此一來,咱們能夠模仿各類已有的 IPC 方式來進行設計,好比 RPC。本文模仿 Android 的 Binder 機制來實現一個 JsBridge。web
首先回顧一一下基於 Binder 的經典 RPC 調用:json
固然,client 與 server 只是用來區分通訊雙方責任的叫法而已,並非一成不變的。
對於 java 與 javascript 互調的狀況,當 java 主動調用 javascript 的時候,java 充當 client 角色,javascript 則扮演 server 的角色,
javascript 中的函數執行完畢後回調 java 方法,這個時候,javascript 充當 client 角色,而 javascript 則承擔 server 的責任。安全
剩下的問題就是怎麼來實現這個機制了,大體有這麼幾個須要解決的問題:app
下面逐個討論這些問題:
要實現 Java 與 Javascript 的相互調用,有兩條途徑能夠考慮:
對於第一種途徑,代價比較大,並且技術方案比較複雜,通常只有基於 Javascript 的跨平臺開發方案纔會這麼作。
因此,如今着重考查第二種途徑。
Android 的默認 Sdk 中, Java 與 Javascript 的一切交互都是依託於 WebView 的,大體有如下幾個可用方法:
第一:
webView.loadUrl("javascript:scriptString"); //其中 scriptString 爲 Javascript 代碼
第二,在 KITKAT 以後,又新增了一個方法:
webView.evaluateJavascript(scriptString, new ValueCallback<String>() { @Override public void onReceiveValue(String value) { } });//其中 scriptString 爲 Javascript 代碼,ValueCallback 的用來獲取 Javascript 的執行結果。這是一個異步掉用。
這個調用看起比上面的正常,並且更像是一個方法調用。
須要注意的是,ValueCallback 並非在 UI 線程裏面執行的。
要實現 Javascript 調用 java 方法,須要先在 Javascript 環境中注入一個 Java 代理:
class JavaProxy{ @JavascriptInterface //注意這裏的註解。出於安全的考慮,4.2 以後強制要求,否則沒法從 Javascript 中發起調用 public void javaFn(){ //xxxxxx }; } webView.addJavascriptInterface(new JavaProxy();, "java_proxy");
而後在 Javascript 環境中直接調用 obj_proxy 代理上的方法便可。
java_proxy.javaFn();
這裏有兩個方面須要統一:
因此,咱們先將 Javascript 的執行包裝成相似 java 同樣的代理對象,而後經過在各自的 stub 上註冊回調來增長功能支持。
好比,若是 java 想增長 getPackageName 方法,那麼,直接在 JavaProxy 上註冊便可:
javaProxy.register("getPackageName", new JavaHandler(){ @Override public void handle(Object value){ //xxxxx } })
如圖:
很顯然,任何 IPC 通訊都涉及到參數序列化的問題, 同理 java 與 Javascript 之間只能傳遞基礎類型(注意,不單純是基本類型),包括基本類型與字符串,不包括其餘對象或者函數。
因爲只涉及到簡單的相互調用,這裏就能夠考慮採用 JSON 格式來傳遞各類數據,輕量而簡潔。
Java 調用 Javascript 沒有返回值(這裏指 loadUrl 形式的調用),所以若是 java 端想從 Javascript 中獲取返回值,只能使用回調的形式。
可是在執行完畢以後如何找到正確的回調方法信息,這是一個重要的問題。好比有下面的例子:
在 java 環境中,JavaProxy 對象有一個無參數的 getPackageName 方法用來獲取當前應用的 PackageName。
獲取到 packageName 以後,傳遞給 Javascript 調用者的對應回調中。
在 Javascript 環境中,獲取當前應用的 PackageName 的大體調用以下:
bridge.invoke('getPackageName', null, function(packageName){ console.log(packageName); });
顯然
function(packageName){ console.log(packageName); }
這個 Javascript 函數是沒法傳遞到 java 環境中的,因此,能夠採起的一個策略就是,
在 Javascript 環境中將全部回調統一管理起來,而只是將回調的 id 傳遞到 java 環境去,java 方法執行完畢以後,
將回調參數以及對應的回調 id 返回給 Javascript 環境,由 Javascript 來負責執行正確的回調。
這樣,咱們就能夠實現一個簡單的回調機制:
在 java 環境中
class JavaProxy{ public void onTransact(String jsonInvoke, String jsonParam){ json = new Json(jsonInvoke); invokeName = json.getInvokeName(); // getPackageName callbackId = json.getCallbackId(); // 12345678xx invokeParam = new Param(jsonParam);// null ... ... JsProxy.invoke(callbackId, callbackParam); //發起 Javascript 調用,讓 Javascript 去執行對應的回調 } }
在 javascript 環境中
bridge.invoke = function(name, param, callback){ var callbackId = new Date().getTime(); _callbacks[callbackId] = callback; var invoke = { "invokeName" : name, "callbackId" : callbackId }; JavaProxy.onTransact(JSON.stringify(invoke), JSON.stringify(param)); } bridge.invoke('getPackageName', null, function(packageName){ console.log(packageName); });
反之亦然。
問題都處理了,只須要設計對應的協議便可。
按照上面的討論,
在 client 端,咱們使用:
Proxy.transact(invoke, callback);
來調用 server 端註冊的方法。
在 server 端,咱們使用:
Stub.register(name, handler);
來註冊新功能,使用
Stub.onTransact(invoke, handler);
來處理接收到的 client 端調用。
其中,invoke 包含所要執行的方法以及回調的信息,所以,invoke 的設計以下:
{ _invoke_id : 1234, _invoke_name : "xxx", _callback_id : 5678, _callback_name : "xxx" }
注意 _invoke_id 與 _invoke_name 的區別:
若是當前 invoke 是一個直接方法調用,那麼 _invoke_id 應該是無效的。 若是當前 invoke 是一個回調,那麼 _invoke_id + _invoke_name 共同決定回調的具體對象
因爲咱們使用一 Hash 來保存各自環境中的回調函數。若是某個回調因爲某種緣由沒有被觸發,那麼,這個引用的對象就永遠不會被回收。
針對這種問題,處理方案以下:
在 Java 環境中:
若是 WebView 被銷燬了,應該手動移除全部的回調,而後禁用 javascript 。
另外,一個 WebView 可能加載多個 Html 頁面,若是頁面的 URL 發生了改變,這個時候也應該清理全部的回調,由於 Html 頁面是無狀態的,也不會傳遞相互數據。
這裏有一點須要注意的是,若是 javascript 端是一個單頁面應用,應該忽略 url 中 fragment (也就是 # 後面的部分) 的變化,由於並無發生傳統意義上的頁面跳轉,
全部單應用的 Page 之間是可能有交互的。
在 javascript 環境中:
javascript 端狀況好不少,由於 WebView 會本身管理每一個頁面的資源回收問題。
請在對應的 html 頁面中引入
<script src="js-bridge.js"></script>
初始化 JsBridge:
jsBridge = new JsBridge(vWebView);
加入 url 監控:
vWebView.setWebViewClient(new WebViewClient() { @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); Log.e("onPageFinished", url); jsBridge.monitor(url); } });
Java 註冊處理方法:
jsBridge.register(new SimpleServerHandler("showPackageName") { @Override public void handle(String param, ServerCallback serverCallback) { new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { String packageName = getPackageName(); Tip.showTip(getApplicationContext(), "showPackageName:" + packageName); } }); } });
Java 在處理方法中回調 Javascript:
@Override public void handle(final String param, final ServerCallback serverCallback) { new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { User user = getUser(); Map<String, String> map = new Gson().fromJson(param, Map.class); String prefix = map.get("name_prefix"); Tip.showTip(mContext, "user.getName():" + prefix + "/" + user.getName()); if ("standard_error".equals(prefix)) { Map<String, String> map1 = new HashMap<>(); map1.put("msg", "get user failed"); String userMarshalling = new Gson().toJson(map1); serverCallback.invoke("fail", new MarshallableObject(userMarshalling)); } else { String userMarshalling = new Gson().toJson(user); serverCallback.invoke("success", new MarshallableObject(userMarshalling)); } } }); }
Java 執行 Js 函數:
jsBridge.invoke("jsFn4", new MarshallableString("yellow"), new ClientCallback<String>() { @Override public void onReceiveResult(String invokeName, final String invokeParam) { if ("success".equals(invokeName)) { new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { Tip.showTip(getApplicationContext(), invokeParam); } }); } } @Override public String getResult(String param) { return param; } });
銷燬 JsBridge
@Override protected void onDestroy() { super.onDestroy(); jsBridge.destroy(); }
Javascript 的靈活性比較高,因此要簡單一些:
Javascript 註冊處理函數:
window.JavaBridge.serverRegister('jsFn4', function (transactInfo, color) { log("jsFn4:" + color); title.style.background = color; log("jsFn4:callback"); transactInfo.triggerCallback('success', 'background change to ' + color); });
Javascript 執行 Java 方法:
var sdk = { getUser: function (params) { var _invokeName = 'getUser'; var _invokeParam = params; var _clientCallback = params; window.JavaBridge.invoke(_invokeName, _invokeParam, _clientCallback); } }; sdk.getUser({ "name_prefix": "standard_error", "success": function (user) { log('sdk.getUser,success:' + user.name); }, "fail": function (error) { log('sdk.getUser,fail:' + error.msg); } })
詳細 Demo 請參見 js-bridge-demo 工程