隨著H5性能的提升,在我們移動(dòng)應(yīng)用開發(fā)的過程中,我們會(huì)越來越多的在我們的App頁面內(nèi)嵌入H5頁面,使得App變的更加動(dòng)態(tài)靈活。而H5頁面往往并不是獨(dú)立,很多時(shí)候需要和native進(jìn)行交互,調(diào)用native的一些方法,或者Web中的一些方法被native所調(diào)用。
現(xiàn)在有很多開源的解決方案,比如JSBridge,可以很方便的讓我們進(jìn)行web頁面和native的交互,其實(shí)現(xiàn)是在WebView原有提供的Web和Native通信基礎(chǔ)上做了封裝,由于最近接手工作中用到了JSBridge,借此機(jī)會(huì)學(xué)習(xí)了一下,本文先從系統(tǒng)提供的一些WebView和Native交互接口講起,然后對(duì)于一個(gè)簡單的開源JSBridge 的剖析。
WebView的使用
WebView webView= (WebView) findViewById(R.id.webview);
webView.loadUrl("file:///android_asset/index.html");
我們可以通過將WebView內(nèi)嵌在App界面中,來裝載網(wǎng)頁,通過loadUrl,給予一個(gè)本地或者遠(yuǎn)程的地址,程序執(zhí)行即可裝載出我們的界面。這里代碼演示的是在assets文件下一個(gè)index.html文件,然后通過我們的Webview裝載的。
Android中 JS和Java的交互方式
在進(jìn)行交互之前需要我們對(duì)WebView進(jìn)行設(shè)置開啟對(duì)JS的支持。
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
- Java調(diào)用JS
- 通過WebView的loadUrl()
- 通過WebView的evaluateJavascript()
- JS調(diào)用Java
- 通過WebView的JavascriptInterface
- 通過WebViewClient.shouldOverrideUrlLoading(),攔截加載信息
- 通過WebChromeClient.onConsoleMessage(),攔截控制臺(tái)信息
- 通過WebChromeClient.onJsPrompt(),onJsAlert()、onJsConfirm()攔截Web相應(yīng)彈框的事件
Java調(diào)用JS
在Java中調(diào)用JS的代碼有兩種方式,分別為通過loadurl和通過evaluateJavascript.
首先定義了一個(gè)html文件,然后將其放置在asset目錄下。通過WebView loadUrl裝載。
<html>
<head>
<title>我的頁面</title>
<meta charset=utf-8>
<script type="application/javascript">
function alertTest() {
alert("alerttest");
}
</script>
</head>
<body>
<p>Android Java JS 交互測試</p>
</body>
</html>
- 通過loadUrl來調(diào)用JS方法
mWebView.post(new Runnable() {
@Override
public void run() {
mWebView.loadUrl("javascript:alertTest()");
}
});
- 通過evaluateJavascript來調(diào)用JS方法
通過該方法,我們還可以得到JS方法的返回值,來進(jìn)行值的展示。
mWebView.evaluateJavascript("javascript:alertTest()", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
Toast.makeText(WebViewActivity.this, value, Toast.LENGTH_SHORT).show();
}
});
這兩個(gè)方法在開始調(diào)用的時(shí)候,出現(xiàn)的問題是報(bào)出錯(cuò)誤信息,錯(cuò)誤信息表示調(diào)用的JS方法未被定義,問題原因是因?yàn)樵趏ncreate或者onResume方法中調(diào)用的時(shí)候,其JavaScript文件未被完全加載完成,因此出現(xiàn)了該問題,可以通過監(jiān)聽WebView的裝載事件延遲調(diào)用來解決該問題。
JS調(diào)用Java
- JavascriptInterface
該種方式由于存在著缺陷,后來被棄用。具體問題將在下面介紹。這里先講一下其使用的方式。
1.定義和JS相關(guān)的交互類和方法,對(duì)于方法通過注解進(jìn)行標(biāo)注。
public class JSTest {
private Context mContext;
public JSTest(Context context) {
mContext = context;
}
@JavascriptInterface
public void showToast(String str) {
Toast.makeText(mContext, str, Toast.LENGTH_SHORT).show();
}
}
2.向WebView添加該JavaScriptInterface,同時(shí)為其指定一個(gè)名稱,該名稱將會(huì)在JS文件中使用。
mWebView.addJavascriptInterface(new JsTest(context), "JsTest");
- JS文件
function showToast() {
JsTest.showToast("來自Web調(diào)用");
}

- shouldOverrideUrlLoading
在WebViewClient中有一個(gè)方法shouldOverrideUrlLoading,該方法在每次有新的鏈接跳轉(zhuǎn)的時(shí)候,該函數(shù)都會(huì)被回調(diào),同時(shí)傳遞該次跳轉(zhuǎn)的url,所以我們可以根據(jù)自己的需求制定一個(gè)url的規(guī)則,在這里對(duì)于url進(jìn)行判斷,如果是我們協(xié)議內(nèi)的,則進(jìn)行攔截,解析我們的協(xié)議,然后進(jìn)行相應(yīng)的方法調(diào)用。
-
onConsoleMessage()
在WebChromeClient中,有一個(gè)函數(shù)回調(diào),當(dāng)我們有console消息的時(shí)候,該函數(shù)就會(huì)被回調(diào)到。因此,我們可以自己制定規(guī)則,然后觸發(fā)console消息,這個(gè)時(shí)候該函數(shù)就會(huì)被回調(diào),回調(diào)之后,根據(jù)我們的規(guī)則進(jìn)行解析,然后調(diào)用我們本地相應(yīng)的方法。
1.在WebChromeClient中定義相應(yīng)回調(diào)方法的攔截處理
@Override
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
String msg = consoleMessage.message();
if ("showToast".equals(msg)) {
Toast.makeText(mContext, "來自Web Toast測試", Toast.LENGTH_SHORT).show();
}
return super.onConsoleMessage(consoleMessage);
}
- JS文件,向控制臺(tái)輸出信息。
function consoleTest() {
console.log("showToast");
}

- onJsPrompt,onJsConfirm, onJsAlert
除了onConsoleMessage的回調(diào)之外,WebChromeClient還提供了onJsConfirm,onJsAlert,onJSPrompt等回調(diào),這些在web端有相應(yīng)的操作的時(shí)候,都會(huì)被回調(diào)到。對(duì)于其攔截,要對(duì)其中的result做判斷和處理,返回值為true,則表示不再執(zhí)行,如果返回值不是true,則會(huì)網(wǎng)頁上的操作繼續(xù)被執(zhí)行。我們可以通過該種消息的回調(diào)來傳遞一些信息,通過這個(gè)信息來實(shí)現(xiàn)JS和Java的交互。
對(duì)于三種回調(diào)的方式,onJsPrompt可以傳遞一個(gè)任意的值給web,而JsConfirm只能傳遞是否,onJsAlert則不能夠傳遞值,因此為了實(shí)現(xiàn)JS和Java的互相調(diào)用,onJsPrompt使用是最方便的。
1.JS文件
function promptTest {
var result = prompt("js://test?arg1=device");
alert("device: " + result);
}
2.WebChromeClient中做相應(yīng)的攔截,這里直接按照傳遞的數(shù)據(jù)做比對(duì)處理,沒有做協(xié)議的約束和解析,然后返回一個(gè)當(dāng)前的設(shè)備類型。
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
if ("js://test?arg1=device".equals(message)) {
result.confirm("模擬器");
}
return true;
}

JSBridge的實(shí)現(xiàn)
上面分析了JS和Java的交互的方式,但是如果只是通過上述的方式進(jìn)行通信,必然會(huì)使得代碼比較臃腫,也難以維護(hù),因此就出現(xiàn)了各種框架來對(duì)其進(jìn)行封裝。這里給出一個(gè)簡單地通信包裝。
我們的需求是實(shí)現(xiàn)JS和Java的互相調(diào)用,比如JS調(diào)用了Java的方法,執(zhí)行完成之后,能夠?qū)⒔Y(jié)果返回,同時(shí)使用返回的結(jié)果作為JS方法的參數(shù),執(zhí)行相應(yīng)的JS方法。這里采取的通信方式是通過對(duì)onJsPrompt的攔截解析,然后通過loadUrl的方式執(zhí)行JS方法。這里分析的是一個(gè)簡單開源JSBridge的實(shí)現(xiàn)。

- Java方法處理
對(duì)于JS可能會(huì)調(diào)用到的Java方法,進(jìn)行集中管理,對(duì)于每一個(gè)類,可以自定義名稱,方便在JS中的調(diào)用。可能會(huì)被調(diào)用到的Java方法,都要進(jìn)行注冊(cè)。
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();
}
}
}
....
}
除此之外,還提供了一個(gè)調(diào)用函數(shù),這個(gè)函數(shù)主要是對(duì)傳遞的數(shù)據(jù)根據(jù)我們制定的協(xié)議進(jìn)行解析,然后從注冊(cè)的函數(shù)中找到所要調(diào)用的函數(shù),執(zhí)行Java函數(shù)。
public static String invokeNative(WebView webView, String uriString) {
//協(xié)議解析
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("/", "");
}
}
//查找方法,執(zhí)行相應(yīng)函數(shù)
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;
}
從上面函數(shù)執(zhí)行的語句中,可以看到起傳遞的參數(shù)有WebView,JSonObject和一個(gè)Callback。
method.invoke(null, webView, new JSONObject(param), new Callback(webView, port));
這里用來給JS調(diào)用的Java方法,傳遞的值都是JsonObject的形式,Callback則是回調(diào)相應(yīng)的JS方法,當(dāng)我們的Java方法執(zhí)行完成之后,如果我們需要調(diào)用相應(yīng)的JS方法,我們可以通過callback提供的apply方法來傳遞一些數(shù)據(jù)。
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方法的實(shí)現(xiàn)。
private static final String CALLBACK_JS_FORMAT = "javascript:JSBridge.onFinish('%s', %s);";
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);
}
});
}
}
這里將需要傳回的數(shù)據(jù),傳遞給JSBridge中的onFinish方法。傳遞的數(shù)據(jù)中有一個(gè)端口號(hào),通過這個(gè)端口號(hào)作為標(biāo)示,來調(diào)用相應(yīng)的方法。
這里方法注冊(cè)的方式會(huì)局限在某一些類中,而且需要我們提前對(duì)所有的方法進(jìn)行注冊(cè),同時(shí)對(duì)于實(shí)例方法和靜態(tài)方法,在執(zhí)行上也會(huì)有一些區(qū)分,對(duì)于方法的處理,可以通過兩端的協(xié)商,規(guī)定好調(diào)用的方法之后,可以設(shè)置一個(gè)處理類,然后對(duì)于每一個(gè)持有WebView的Activity,為其設(shè)置一個(gè)回調(diào),當(dāng)JS對(duì)傳輸?shù)膮?shù)進(jìn)行解析的時(shí)候,解析返回的數(shù)據(jù),進(jìn)行相應(yīng)的處理即可。這樣就可以調(diào)用到我們當(dāng)前的類,但是對(duì)于方法調(diào)用部分就需要我們手動(dòng)去處理,而非在JSBridge中作為一個(gè)黑盒處理。
- JS方法處理
callbacks: {},
call: function (obj, method, params, callback) {
var port = Util.getPort();
this.callbacks[port] = callback;
var uri=Util.getUri(obj,method,params,port);
window.prompt(uri, "");
},
onFinish: function (port, jsonObj){
var callback = this.callbacks[port];
callback && callback(jsonObj);
delete this.callbacks[port];
},
JS文件提供了兩個(gè)方法,一個(gè)是call一個(gè)是finish,分別是web中被調(diào)用,另一個(gè)是在native中將會(huì)被調(diào)用,每一個(gè)web中調(diào)用我們native方法的時(shí)候,都會(huì)調(diào)用js文件中的oncall方法,同時(shí)也會(huì)傳遞一個(gè)函數(shù)作為回調(diào),js文件中會(huì)為該次調(diào)用隨機(jī)生成一個(gè)端口號(hào),同時(shí)將其回調(diào)保存在一個(gè)內(nèi)部列表callbacks中,根據(jù)協(xié)議規(guī)則,通過window.promt的方式將相應(yīng)的調(diào)用傳遞下去。
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
result.confirm(JSBridge.invokeNative(view, message));
return true;
}
在WebChromeClient的onJsPrompt方法中便可以得到相應(yīng)的信息,通過JSBridge方法對(duì)回傳數(shù)據(jù)進(jìn)行相應(yīng)的調(diào)用。
JS中調(diào)用Java方法的方式
JSBridge.call('bridge','showToast',{'msg':'Hello JSBridge'},function(res){alert(JSON.stringify(res))})
第一,二個(gè)參數(shù)表示調(diào)用的Java的方法,第三個(gè)參數(shù)為傳遞的參數(shù)。第四個(gè)參數(shù)為設(shè)置的回調(diào)函數(shù)。
至此,一個(gè)簡單的JSBridge實(shí)現(xiàn)了,JS只需要通過call方法傳遞相應(yīng)的參數(shù)即可調(diào)用Java方法,Java只需要將待調(diào)用方法進(jìn)行注冊(cè)即可。
交互中的安全漏洞問題
進(jìn)幾年和WebView遠(yuǎn)程代碼執(zhí)行相關(guān)的漏洞主要有CVE-2012-6336,CVE-2014-1939,CVE-2014-7224, 這些漏洞中最核心的漏洞是CVE-2012-6336,另外兩個(gè)CVE只是發(fā)現(xiàn)了幾個(gè)默認(rèn)存在的接口。
- CVE-2012-6636
Android API 16.0及之前的版本中存在安全漏洞,該漏洞源于程序沒有正確限制使用WebView.addJavascriptInterface方法。遠(yuǎn)程攻擊者可通過使用Java Reflection API利用該漏洞執(zhí)行任意Java對(duì)象的方法
Google Android <= 4.1.2 (API level 16) 受到此漏洞的影響。
- CVE-2014-1939
java/android/webkit/BrowserFrame.java 使用addJavascriptInterface API并創(chuàng)建了SearchBoxImpl類的對(duì)象。攻擊者可通過訪問searchBoxJavaBridge_接口利用該漏洞執(zhí)行任意Java代碼。
Google Android <= 4.3.1 受到此漏洞的影響
- CVE-2014-7224
香港理工大學(xué)的研究人員發(fā)現(xiàn)當(dāng)系統(tǒng)輔助功能中的任意一項(xiàng)服務(wù)被開啟后,所有由系統(tǒng)提供的WebView都會(huì)被加入兩個(gè)JS objects,分別為是accessibility和accessibilityTraversal。惡意攻擊者就可以使用accessibility和accessibilityTraversal這兩個(gè)Java Bridge來執(zhí)行遠(yuǎn)程攻擊代碼.
Google Android < 4.4 受到此漏洞的影響。
對(duì)于上述漏洞,其攻擊原理為得到了Java對(duì)象,通過反射的方式來執(zhí)行自己的惡意代碼。
解決方案
1.移除掉原有提供的JavaScript接口
private static final void removeJavascriptInterfaces11(WebView webView) {
try {
webView.removeJavascriptInterface("searchBoxJavaBridge_");
webView.removeJavascriptInterface("accessibility");
webView.removeJavascriptInterface("accessibilityTraversal");
} catch (Throwable tr) {
tr.printStackTrace();
}
}
2.升級(jí)系統(tǒng)API level 17后,只有顯示添加 @JavascriptInterface的方法才能被JavaScript調(diào)用,這樣反射就失去作用了。但對(duì)于更低版本則還是會(huì)存在,考慮采用其它方案,例如JSBridge實(shí)現(xiàn)交互。
參考資料
JsBridge 實(shí)現(xiàn) JavaScript 和 Java 的互相調(diào)用