Android JSBridge原理與實現


在Android中,JSBridge已經不是什么新鮮的事物了,各家的實現方式也略有差異。大多數人都知道WebView存在一個漏洞,詳細信息見你不知道的 Android WebView 使用漏洞,雖然該漏洞已經在Android 4.2上修復了,即使用@JavascriptInterface代替addJavascriptInterface,但是由於兼容性和安全性問題,基本上我們不會再利用Android系統為我們提供的addJavascriptInterface方法或者@JavascriptInterface注解來實現,所以我們只能另辟蹊徑,去尋找既安全,又能實現兼容Android各個版本的方案。

 

首先我們來了解一下為什么要使用JSBridge,在開發中,為了追求開發的效率以及移植的便利性,一些展示性強和性能要求不是很高的頁面,我們會偏向於使用h5來完成,功能性強的頁面我們會偏向於使用native來完成,而一旦使用了h5,為了在h5中盡可能的得到native的體驗,我們native層需要暴露一些方法給js調用,比如,彈Toast提醒,彈Dialog,分享等等,有時候甚至把h5的網絡請求放着native去完成,而JSBridge做得好的一個典型就是微信,微信給開發者提供了JSSDK,該SDK中暴露了很多微信native層的方法,比如支付,定位等。

那么,怎么去實現一個兼容Android各版本又具有一定安全性的JSBridge呢?我們知道,在WebView中,如果java要調用js的方法,是非常容易做到的,Android4.4以前使用WebView.loadUrl(“javascript:function()”)。Android4.4以后,使用以下方式

webView.evaluateJavascript(“javascript:function()”, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
Toast.makeText(MainActivity.this, "onReceiveValue Java To JS", Toast.LENGTH_SHORT).show();
}
});

,這樣,就做到了JSBridge的native層調用h5層的通信,但是h5層如何調native層呢,我們需要尋找這么一個通道,仔細回憶一下,WebView有一個方法,叫setWebChromeClient,可以設置WebChromeClient對象,而這個對象中有三個方法,分別是onJsAlert,onJsConfirm,onJsPrompt,當js調用window對象的對應的方法,即window.alert,window.confirm,window.prompt,WebChromeClient對象中的三個方法對應的就會被觸發,我們是不是可以利用這個機制,自己做一些處理呢?答案是肯定的。

 

至於js這三個方法的區別,可以詳見w3c JavaScript 消息框 。一般來說,我們是不會使用onJsAlert的,為什么呢?因為js中alert使用的頻率還是非常高的,一旦我們占用了這個通道,alert的正常使用就會受到影響,而confirm和prompt的使用頻率相對alert來說,則更低一點。那么到底是選擇confirm還是prompt呢,其實confirm的使用頻率也是不低的,比如你點一個鏈接下載一個文件,這時候如果需要彈出一個提示進行確認,點擊確認就會下載,點取消便不會下載,類似這種場景還是很多的,因此不能占用confirm。而prompt則不一樣,在Android中,幾乎不會使用到這個方法,就是用,也會進行自定義,所以我們完全可以使用這個方法。該方法就是彈出一個輸入框,然后讓你輸入,輸入完成后返回輸入框中的內容。因此,占用prompt是再完美不過了。

到這一步,我們已經找到了JSBridge雙向通信的一個通道了,接下來就是如何實現的問題了。本文中實現的只是一個簡單的demo,如果要在正式項目中下使用,還需要自己做一層封裝。

要進行正常的通信,通信協議的制定是必不可少的。我們回想一下熟悉的http請求url的組成部分。形如http://host:port/path?param=value,我們參考http,制定JSBridge的組成部分,我們的JSBridge需要傳遞給native什么信息,native層才能完成對應的功能,然后將結果返回呢?顯而易見我們native層要完成某個功能就需要調用某個類的某個方法,我們需要將這個類名和方法名傳遞過去,此外,還需要方法調用所需的參數,為了通信方便,native方法所需的參數我們規定為json對象,我們在js中傳遞這個json對象過去,native層拿到這個對象再進行解析即可。為了區別於http協議,我們的jsbridge使用jsbridge協議,為了簡單起見,問號后面不適用鍵值對,我們直接跟上我們的json字符串,於是就有了形如下面的這個uri

jsbridge://className:port/methodName?jsonObj

假設我們需要調用native層的Logger類的log方法,當然這個類以及方法肯定是遵循某種規范的,不是所有的java類都可以調用,不然就跟文章開頭的WebView漏洞一樣了,參數是msg,執行完成后js層要有一個回調,那么地址就如下

sbridge://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];
}
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM