JsBridge
簡單介紹
Android JsBridge 就是用來在 Android app的原生 java 代碼與 javascript 代碼中架設通信(調用)橋梁的輔助工具。
有問題請聯系 xesam
原理概述
Javascript 運行在 WebView 中,而 WebView 僅僅是 Javascript 運行引擎與頁面渲染引擎的一個包裝而已。
因為這樣的天然的隔離效應,我們能夠將這樣的情況與 IPC 進行類比,將 Java 與 Javascript 的每次互調都看做一次 IPC 調用。
如此一來,我們能夠模仿各種已有的 IPC 方式來進行設計。比方 RPC。本文模仿 Android 的 Binder 機制來實現一個 JsBridge。
首先回想一一下基於 Binder 的經典 RPC 調用:
當然,client 與 server 僅僅是用來區分通信兩方責任的叫法而已。並非一成不變的。
對於 java 與 javascript 互調的情況,當 java 主動調用 javascript 的時候,java 充當 client 角色,javascript 則扮演 server 的角色,
javascript 中的函數運行完成后回調 java 方法,這個時候。javascript 充當 client 角色,而 javascript 則承擔 server 的責任。
剩下的問題就是怎么來實現這個機制了。大致有這么幾個須要解決的問題:
- java 怎樣調用 Javascript
- Javascript 怎樣調用 java
- 方法參數以及回調怎樣處理
- 通信的數據格式是怎樣的
以下逐個討論這些問題:
1. java 怎樣調用 Javascript
要實現 Java 與 Javascript 的相互調用。有兩條途徑能夠考慮:
- 集成一個定制化的 Javascript 與 Html 渲染引擎,java 通過引擎底層與 Javascript 交互。這樣能夠獲得全然的控制權。
- 使用 Android Sdk 提供的交互方法。
對於第一種途徑,代價比較大,並且技術方案比較復雜,一般僅僅有基於 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 線程里面運行的。
2. Javascript 怎樣調用 java
要實現 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 的運行方法比較怪異,所以,我們須要將概念統一化。
- 假設須要運行的方法比較多。那么,代理對象上也須要定義非常多的方法。我們須要將各種方法定義統一起來管理。
所以。我們先將 Javascript 的運行包裝成相似 java 一樣的代理對象,然后通過在各自的 stub 上注冊回調來添加功能支持。
比方。假設 java 想添加 getPackageName 方法,那么。直接在 JavaProxy 上注冊就可以:
javaProxy.register("getPackageName", new JavaHandler(){
@Override
public void handle(Object value){
//xxxxx
}
})
如圖:
3. 方法參數以及回調怎樣處理
非常顯然。不論什么 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);
});
反之亦然。
4. 通信的數據格式是怎樣的
問題都處理了,僅僅須要設計相應的協議就可以。
依照上面的討論,
在 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 共同決定回調的具體對象
須要注意的問題
1. 回調函數須要及時刪除。不然會引起內存泄漏。
因為我們使用一 Hash 來保存各自環境中的回調函數。假設某個回調因為某種原因沒有被觸發。那么,這個引用的對象就永遠不會被回收。
針對這樣的問題。處理方案例如以下:
在 Java 環境中:
假設 WebView 被銷毀了,應該手動移除全部的回調,然后禁用 javascript 。
另外。一個 WebView 可能載入多個 Html 頁面,假設頁面的 URL 發生了改變,這個時候也應該清理全部的回調,因為 Html 頁面是無狀態的。也不會傳遞相互數據。
這里有一點須要注意的是,假設 javascript 端是一個單頁面應用,應該忽略 url 中 fragment (也就是 # 后面的部分) 的變化。因為並沒有發生傳統意義上的頁面跳轉,
全部單應用的 Page 之間是可能有交互的。
在 javascript 環境中:
javascript 端情況好非常多,因為 WebView 會自己管理每一個頁面的資源回收問題。
使用
必要配置
請在相應的 html 頁面中引入
<script src="js-bridge.js"></script>
Java 環境
初始化 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 的靈活性比較高。所以要簡單一些:
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 project