在Android中。JSBridge已經不是什麼新奇的事物了,各家的實現方式也略有差別。javascript
大多數人都知道WebView存在一個漏洞。見WebView中接口隱患與手機掛馬利用,儘管該漏洞已經在Android 4.2上修復了,即便用@JavascriptInterface取代addJavascriptInterface,但是因爲兼容性和安全性問題,基本上咱們不會再利用Android系統爲咱們提供的addJavascriptInterface方法或者@JavascriptInterface註解來實現。因此咱們僅僅能另闢蹊徑,去尋找既安全,又能實現兼容Android各個版本號的方案。php
首先咱們來了解一下爲何要使用JSBridge,在開發中。爲了追求開發的效率以及移植的便利性,一些展現性強的頁面咱們會偏向於使用h5來完畢。功能性強的頁面咱們會偏向於使用native來完畢。而一旦使用了h5,爲了在h5中儘量的獲得native的體驗,咱們native層需要暴露一些方法給js調用,比方,彈Toast提醒。彈Dialog,分享等等,有時候甚至把h5的網絡請求放着native去完畢,而JSBridge作得好的一個典型就是微信。微信給開發人員提供了JSSDK,該SDK中暴露了很是多微信native層的方法,比方支付,定位等。css
那麼。怎麼去實現一個兼容Android各版本號又具備必定安全性的JSBridge呢?咱們知道。在WebView中,假設java要調用js的方法。是很是easy作到的,使用WebView.loadUrl(「javascript:function()」)就能夠,這樣。就作到了JSBridge的native層調用h5層的單向通訊,但是h5層怎樣調native層呢,咱們需要尋找這麼一個通道。細緻回顧一下,WebView有一個方法,叫setWebChromeClient,可以設置WebChromeClient對象,而這個對象中有三個方法。各自是onJsAlert,onJsConfirm,onJsPrompt。當js調用window對象的相應的方法,即window.alert,window.confirm,window.prompt,WebChromeClient對象中的三個方法相應的就會被觸發,咱們是否是可以利用這個機制,本身作一些處理呢?答案是確定的。html
至於js這三個方法的差異,可以詳見w3c JavaScript 消息框 。通常來講,咱們是不會使用onJsAlert的,爲何呢?因爲js中alert使用的頻率仍是很是高的,一旦咱們佔用了這個通道,alert的正常使用就會受到影響。而confirm和prompt的使用頻率相對alert來講,則更低一點。那麼到底是選擇confirm仍是prompt呢,事實上confirm的使用頻率也是不低的,比方你點一個連接下載一個文件,這時候假設需要彈出一個提示進行確認。點擊確認就會下載。點取消便不會下載,類似這種場景仍是很是多的,所以不能佔用confirm。而prompt則不同,在Android中。差點兒不會使用到這種方法,就是用。也會進行本身定義。因此咱們全然可以使用這種方法。該方法就是彈出一個輸入框。而後讓你輸入,輸入完畢後返回輸入框中的內容。所以。佔用prompt是再完美只是了。java
到這一步,咱們已經找到了JSBridge雙向通訊的一個通道了。接下來就是怎樣實現的問題了。本文中實現的僅僅是一個簡單的demo,假設要在生產環境下使用。還需要本身作一層封裝。android
要進行正常的通訊,通訊協議的制定是不可缺乏的。git
咱們回憶一下熟悉的http請求url的組成部分。github
形如http://host:port/path?param=value。咱們參考http,制定JSBridge的組成部分,咱們的JSBridge需要傳遞給native什麼信息,native層才幹完畢相應的功能,而後將結果返回呢?顯而易見咱們native層要完畢某個功能就需要調用某個類的某個方法,咱們需要將這個類名和方法名傳遞過去。此外,還需要方法調用所需的參數,爲了通訊方便。native方法所需的參數咱們規定爲json對象。咱們在js中傳遞這個json對象過去。native層拿到這個對象再進行解析就能夠。爲了差異於http協議,咱們的jsbridge使用jsbridge協議,爲了簡單起見,問號後面不適用鍵值對。咱們直接跟上咱們的json字符串,因而就有了形如如下的這個uriweb
json
jsbridge://className:port/methodName?jsonObj
有人會問,這個port用來幹嗎,事實上js層調用native層方法後,native需要將運行結果返回給js層。只是你會認爲經過WebChromeClient對象的onJsPrompt方法將返回值返回給js不就行了嗎,事實上否則,假設這麼作,那麼這個過程就是同步的。假設native運行異步操做的話,返回值怎麼返回呢?這時候port就發揮了它應有的做用。咱們在js中調用native方法的時候,在js中註冊一個callback,而後將該callback在指定的位置上緩存起來,而後native層運行完畢相應方法後經過WebView.loadUrl調用js中的方法,回調相應的callback。那麼js怎麼知道調用哪一個callback呢?因而咱們需要將callback的一個存儲位置傳遞過去,那麼就需要native層調用js中的方法的時候將存儲位置回傳給js,js再調用相應存儲位置上的callback,進行回調。
因而,完整的協議定義例如如下:
jsbridge://className:callbackAddress/methodName?jsonObj
假設咱們需要調用native層的Logger類的log方法。固然這個類以及方法確定是遵循某種規範的,不是所有的java類都可以調用。否則就跟文章開頭的WebView漏洞同樣了,參數是msg。運行完畢後js層要有一個回調。那麼地址就例如如下
jsbridge://Logger:callbackAddress/log?{"msg":"native log"}
至於這個callback對象的地址。可以存儲到js中的window對象中去。至於怎麼存儲,後文會慢慢倒來。
上面是js向native的通訊協議,那麼另外一方面,native向js的通訊協議也需要制定,一個不可缺乏的元素就是返回值,這個返回值和js的參數作法同樣。經過json對象進行傳遞。該json對象中有狀態碼code,提示信息msg,以及返回結果result。假設code爲非0,則運行過程當中發生了錯誤,錯誤信息在msg中,返回結果result爲null。假設運行成功,返回的json對象在result中。如下是兩個樣例。一個成功調用,一個調用失敗。
{
"code":500,
"msg":"method is not exist",
"result":null }
{
"code":0,
"msg":"ok",
"result":{ "key1":"returnValue1", "key2":"returnValue2", "key3":{ "nestedKey":"nestedValue" "nestedArray":["value1","value2"] } } }
那麼這個結果怎樣返回呢。native調用js暴露的方法就能夠。而後將js層傳給native層的port一併帶上,進行調用就能夠,調用的方式就是經過WebView.loadUrl方式來完畢,例如如下。
mWebView.loadUrl("javascript:JSBridge.onFinish(port,jsonObj);");
關於JsBridge.onFinish方法的實現。後面再敘述。前面咱們提到了native層的方法必須遵循某種規範。否則就很是不安全了。在native中,咱們需要一個JSBridge統一管理這些暴露給js的類和方法,並且能實時增長,這時候就需要這麼一個方法
JSBridge.register("jsName",javaClass.class)
這個javaClass就是知足某種規範的類,該類中有知足規範的方法,咱們規定這個類需要實現一個空接口,爲何呢?主要做用就混淆的時候不會錯誤發生,另外一個做用就是約束JSBridge.register方法第二個參數必須是該接口的實現類。那麼咱們定義這個接口
public interface IBridge{
}
類規定好了。類中的方法咱們還需要規定,爲了調用方便,咱們規定類中的方法必須是static的,這樣直接依據類而沒必要新建對象進行調用了(還要是public的)。而後該方法不具備返回值,因爲返回值咱們在回調中返回,既然有回調,參數列表就確定有一個callback。除了callback,固然還有前文提到的js傳來的方法調用所需的參數,是一個json對象,在java層中咱們定義成JSONObject對象;方法的運行結果需要經過callback傳遞回去。而java運行js方法需要一個WebView對象。因而,知足某種規範的方法原型就出來了。
public static void methodName(WebView web view,JSONObject jsonObj,Callback callback){
}
js層除了上文說到的JSBridge.onFinish(port,jsonObj);方法用於回調。應該另外一個方法提供調用native方法的功能,該函數的原型例如如下
JSBridge.call(className,methodName,params,callback)
在call方法中再將參數組合成形如如下這個格式的uri
jsbridge://className:callbackAddress/methodName?jsonObj
而後調用window.prompt方法將uri傳遞過去,這時候java層就會收到這個uri,再進一步解析就能夠。
萬事具有了,僅僅欠怎樣編碼了,別急,如下咱們一步一步的來實現,先完畢js的兩個方法。新建一個文件,命名爲JSBridge.js
(function (win) { var hasOwnProperty = Object.prototype.hasOwnProperty; var JSBridge = win.JSBridge || (win.JSBridge = {}); var JSBRIDGE_PROTOCOL = 'JSBridge'; var Inner = { callbacks: {}, call: function (obj, method, params, callback) { console.log(obj+" "+method+" "+params+" "+callback); var port = Util.getPort(); console.log(port); this.callbacks[port] = callback; var uri=Util.getUri(obj,method,params,port); console.log(uri); window.prompt(uri, ""); }, onFinish: function (port, jsonObj){ var callback = this.callbacks[port]; callback && callback(jsonObj); delete this.callbacks[port]; }, }; var Util = { getPort: function () { return Math.floor(Math.random() * (1 << 30)); }, getUri:function(obj, method, params, port){ params = this.getParam(params); var uri = JSBRIDGE_PROTOCOL + '://' + obj + ':' + port + '/' + method + '?' + params; return uri; }, getParam:function(obj){ if (obj && typeof obj === 'object') { return JSON.stringify(obj); } else { return obj || ''; } } }; for (var key in Inner) { if (!hasOwnProperty.call(JSBridge, key)) { JSBridge[key] = Inner[key]; } } })(window);
可以看到。咱們裏面有一個Util類,裏面有三個方法。getPort()用於隨機生成port,getParam()用於生成json字符串。getUri()用於生成native需要的協議uri,裏面主要作字符串拼接的工做,而後有一個Inner類,裏面有咱們的call和onFinish方法,在call方法中,咱們調用Util.getPort()得到了port值,而後將callback對象存儲在了callbacks中的port位置,接着調用Util.getUri()將參數傳遞過去。將返回結果賦值給uri。調用window.prompt(uri, 「」)將uri傳遞到native層。而onFinish()方法接受native回傳的port值和運行結果,依據port值從callbacks中獲得原始的callback函數,運行callback函數,以後從callbacks中刪除。最後將Inner類中的函數暴露給外部的JSBrige對象。經過一個for循環一一賦值就能夠。
固然這個實現是最最簡單的實現了。實際狀況要考慮的因素太多,因爲本人不是很是精通js,因此僅僅能以java的思想去寫js,沒有考慮到的因素姑且忽略吧。比方內存的回收等等機制。
這樣,js層的編碼就完畢了,接下來實現java層的編碼。
上文說到java層有一個空接口來進行約束暴露給js的類和方法,同一時候也便於混淆
public interface IBridge {
}
首先咱們要將js傳來的uri獲取到,編寫一個WebChromeClient子類。
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;
}
}
以後不要忘記了將該對象設置給WebView
WebView mWebView = (WebView) findViewById(R.id.webview);
WebSettings settings = mWebView.getSettings();
settings.setJavaScriptEnabled(true);
mWebView.setWebChromeClient(new JSBridgeWebChromeClient());
mWebView.loadUrl("file:///android_asset/index.html");
核心的內容來了。就是JSBridgeWebChromeClient中調用的JSBridge類的實現。
前文提到該類中有這麼一個方法提供註冊暴露給js的類和方法
JSBridge.register("jsName",javaClass.class)
該方法的實現事實上很是easy,從一個Map中查找key是否是存在,不存在則反射拿到相應的Class中的所有方法。將方法是public static void 類型的。並且參數是三個參數,各自是Webview,JSONObject。Callback類型的,假設知足條件。則將所有知足條件的方法put進去,整個實現例如如下
public class JSBridge {
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();
}
}
}
private static HashMap<String, Method> getAllMethod(Class injectedCls) throws Exception {
HashMap<String, Method> mMethodsMap = new HashMap<>();
Method[] methods = injectedCls.getDeclaredMethods();
for (Method method : methods) {
String name;
if (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || (name = method.getName()) == null) {
continue;
}
Class[] parameters = method.getParameterTypes();
if (null != parameters && parameters.length == 3) {
if (parameters[0] == WebView.class && parameters[1] == JSONObject.class && parameters[2] == Callback.class) {
mMethodsMap.put(name, method);
}
}
}
return mMethodsMap;
}
}
而至於JSBridge類中的callJava方法,就是將js傳來的uri進行解析,而後依據調用的類名別名從剛剛的map中查找是否是存在。存在的話拿到該類所有方法的methodMap。而後依據方法名從methodMap拿到方法,反射調用。並將參數傳進去。參數就是前文說的知足條件的三個參數,即WebView,JSONObject。Callback。
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;
}
看到該方法中使用了 new Callback(webView, port)進行新建對象。該對象就是用來回調js中回調方法的java相應的類。這個類你需要將js傳來的port傳進來以外,還需要將WebView的引用傳進來,因爲要使用到WebView的loadUrl方法,爲了防止內存泄露,這裏使用弱引用。假設你需要回調js的callback,在相應的方法裏調用一下callback.apply()方法將返回數據傳入就能夠,
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);
}
});
}
}
}
惟一需要注意的是apply方法我把它扔在主線程運行了,爲何呢,因爲暴露給js的方法可能會在子線程中調用這個callback,這種話就會報錯,因此我在方法內部將其切回主線程。
編碼完畢的差點兒相同了,那麼就剩實現IBridge就能夠了,咱們來個簡單的。就來顯示Toast爲例好了,顯示完給js回調。儘管這個回調沒有什麼意義。
public class BridgeImpl implements IBridge {
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();
}
}
}
private static JSONObject getJSONObject(int code, String msg, JSONObject result) {
JSONObject object = new JSONObject();
try {
object.put("code", code);
object.put("msg", msg);
object.putOpt("result", result);
return object;
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}
}
你可以往該類中扔你需要的方法。但是必須是public static void且參數列表知足條件,這樣才幹找到該方法。
不要忘記將該類註冊進去
JSBridge.register("bridge", BridgeImpl.class);
進行一下簡單的測試,將以前實現好的JSBridge.js文件扔到assets文件夾下,而後新建index.html。輸入
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>JSBridge</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1, user-scalable=no"/>
<script src="file:///android_asset/JSBridge.js" type="text/javascript"></script>
<script type="text/javascript"> </script>
<style> </style>
</head>
<body>
<div>
<h3>JSBridge 測試</h3>
</div>
<ul class="list">
<li>
<div>
<button onclick="JSBridge.call('bridge','showToast',{'msg':'Hello JSBridge'},function(res){alert(JSON.stringify(res))})">
測試showToast
</button>
</div>
</li>
<br/>
</ul>
</body>
</html>
很是easy,就是按鈕點擊時調用JSBridge.call()方法,回調函數是alert出返回的結果。
接着就是使用WebView將該index.html文件load進來測試了
mWebView.loadUrl("file:///android_asset/index.html");
效果例如如下圖所看到的
可以看到整個過程都走通了,而後咱們測試下子線程回調,在BridgeImpl中增長測試方法
public static void testThread(WebView webView, JSONObject param, final Callback callback) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
JSONObject object = new JSONObject();
object.put("key", "value");
callback.apply(getJSONObject(0, "ok", object));
} catch (InterruptedException e) {
e.printStackTrace();
} catch (JSONException e) {
e.printStackTrace();
}
}
}).start();
}
在index.html中增長
<ul class="list">
<li>
<div>
<button onclick="JSBridge.call('bridge','testThread',{},function(res){alert(JSON.stringify(res))})">
測試子線程回調
</button>
</div>
</li>
<br/>
</ul>
理想的效果應該是3秒鐘以後回調彈出alert顯示
很是完美,代碼也很少,就實現了功能。假設你需要使用到生成環境中去,上面的代碼你必定要再本身封裝一下,因爲我僅僅是簡單的實現了功能。其它因素並無考慮太多。
固然你也可以參考一個開源的實現
Safe Java-JS WebView Bridge
最後仍是慣例,貼上代碼