0x00 背景
近年來,由于開發(fā)成本,開發(fā)效率础芍,用戶需求等原因灸叼,對于移動 App 的開發(fā)方案已經(jīng)從原生開發(fā)趨向于混合(Hybrid)開發(fā)的方式神汹,甚至于說直接基于一些大的 App 平臺提供的 JS SD K直接開發(fā) Web 頁面,例如微信古今、手機QQ等超級 App邻耕。最近氨淌,就在我寫這篇文章的時候,在微信公開課 Pro 活動上,張小龍?zhí)岢隽宋⑿抨P(guān)于“應(yīng)用號”的規(guī)劃富弦,具體請看這篇文章預(yù)埋兩年的線索,傳說會干掉 App 的微信應(yīng)用號是什么虫埂?贷盲,可見混合開發(fā)這種開發(fā)方式的重要性。
基于混合開發(fā)方式的優(yōu)勢是非常明顯的:它既能使用原生的一些手機特性拟逮,而且又擁有隨時發(fā)布的能力撬统。前者是通過提供相關(guān) JS API 使 Web 頁面具有一些原生的功能,而后者是 Web 頁面天生具有的特性敦迄。也就是說:我們在開發(fā)原生應(yīng)用的基礎(chǔ)上嵌入 WebView宪摧,但是整體的架構(gòu)使用原生應(yīng)用提供。關(guān)于這種開發(fā)方式颅崩,如果你想要更進(jìn)一步了解几于,請參考這篇《Hybrid App 開發(fā)實戰(zhàn)》。
而本文的目的就是想要知道這些 JS API 是如何實現(xiàn)的沿后,或者更直白一點:Android 中 Java 與 JavaScript 是怎么交互/通信的沿彭?你要知道 JavaScript 是運行在瀏覽器環(huán)境下的腳本語言。當(dāng)然尖滚,網(wǎng)上關(guān)于這方面的資料非常多喉刘,但是我這里還是想總結(jié)與實踐一下瞧柔,因為之前的項目需求開發(fā)接觸 Hybrid 開發(fā)這種方式,在 Android 上寫了一些 JS API睦裳,后來又接觸了前端開發(fā)造锅,開始使用這些 JS API,所以很想了解一下其中的相關(guān)原理廉邑。
0x01 兩類交互方式
在進(jìn)入主題之前哥蔚,還需要提到一點:本文主要是涉及 JavaScript 如何調(diào)用 Java?而反過來蛛蒙,Java 調(diào)用 JavaScript 因為比較簡單一點糙箍,我這里稍微提下,直接上代碼:
String url = "javascript:" + methodName + "(" + jsonParams + ");void(0);"
webView.loadUrl(url);
就是這么簡單牵祟,直接調(diào)用 WebView 的loadUrl(url)
方法深夯,當(dāng)然參數(shù) url 是比較特殊,前面的javascript:
偽協(xié)議讓我們可以通過一個鏈接來調(diào)用 JavaScript 函數(shù)诺苹,中間methodName是 JavaScript 中實現(xiàn)的函數(shù)咕晋,jsonParams是傳入的參數(shù)。關(guān)于后面的void(0);
收奔,可以參考這篇文檔《void operator》中的說明掌呜。Java 調(diào)用 JavaScript 的方式就說到這里,下面我們繼續(xù)討論 JavaScript 是如何調(diào)用 Java 的筹淫,實現(xiàn)的方法有很多種站辉,我把它歸為兩類:
Android WebView api 本身就支持的方式
addJavascriptInterface
;通過偽協(xié)議攔截頁面的“請求”损姜,即需要 JavaScript 與 Java 端(native)事先約定饰剥,方法有
shouldOverrideUrlLoading
、window.prompt
摧阅、Console.log
和alert
等汰蓉,我們通常稱這種方式為JsBridge。
addJavascriptInterface
首先棒卷,我們來看第一類addJavascriptInterface
顾孽,其方法聲明如下所示:
public void addJavascriptInterface(Object object, String name)
該方法將參數(shù)中提供的 Java 對象(object)注入到 WebView 中。該對象會被注入到頁面主框架(main frame)的 Javascript 上下文中比规,通過參數(shù)中提供的名稱(name)訪問若厚。具體的使用方式,Android 官方文檔有給出:
class JsObject {
@JavascriptInterface
public String toString() {
return "injectedObject";
}
}
webView.addJavascriptInterface(new JsObject(), "injectedObject"); // 只有頁面再加載蜒什,該對象才可見
webView.loadData("", "text/html", null);
webView.loadUrl("javascript:alert(injectedObject.toString())");
這個例子大家一看就很明了测秸,addJavascriptInterface
這種方式非常簡單好用。但是這種方式在 Android 4.2之前的版本中存在安全問題:在4.2之前被注入的對象的所有公共方法(包括從父類繼承過來的方法)都可以被訪問到;在4.2以后霎冯,只有通過@JavascriptInterface注解的公共方法才能被訪問铃拇。具體請看這篇WebView中接口隱患與手機掛馬利用
除此之外,對于該方法還需要注意的是:
在該方式下沈撞,JavaScript 調(diào)用 Java 通過 WebView 的一個私有后臺線程慷荔,所以,需要我們需要注意線程安全缠俺;
Java對象的域是不可訪問的显晶;
在 Android 5.0及以上,被注入對象的方法可被 JavaScript 枚舉晋修。
下面吧碾,我們來看第二類方法凰盔,這類方法的特點是:JS 端與 Native 端存在一個偽協(xié)議墓卦,Native 端口根據(jù)這個協(xié)議去偵聽/截獲頁面的相關(guān)行為。所以户敬,我們首先需要定義一個協(xié)議(可參考上面的javascript:
偽協(xié)議):協(xié)議名+方法名+相關(guān)參數(shù)
落剪。在本文中,我們假定該協(xié)議格式為:"jsbridge://" + "method" + "jsonParams"
尿庐,整個協(xié)議就是個特殊的字符串忠怖。之后我們要做的工作是把這個字符串從 JS 端傳到 Native 端,然后 Native 去解析這個字符串并執(zhí)行相關(guān)代碼抄瑟。這其中的關(guān)鍵就是如何傳這個字符串凡泣,方法有很多,我們一個一個來看:
shouldOverrideUrlLoading
shouldOverrideUrlLoading
是類WebViewClient中的一個方法皮假。它的作用是控制當(dāng)前 Webview 加載新 url 的相關(guān)行為鞋拟。在默認(rèn)情況下,Webview 沒有設(shè)置 WebViewClient惹资,所以它會請求 Activity Manager 來處理該 url (一般就是調(diào)用相關(guān)瀏覽器應(yīng)用)贺纲。該方法的方法聲明如下:
public boolean shouldOverrideUrlLoading(Webview view, String url)
從方法聲明可知,我們將通過參數(shù)String url
來傳遞我們協(xié)議字符串褪测,所以在 Native 端我們創(chuàng)建設(shè)置 WebViewClient 子類猴誊,該子類覆寫shouldOverrideUrlLoading
方法,這個就可以攔截 Webview 加載新 url 了侮措。那么在 JS 端該如何生成這個 url 呢懈叹?一般我們可以創(chuàng)建一個 iframe,設(shè)置它的 src 屬性分扎,并將其添加到頁面的文檔流中澄成,或者直接設(shè)置window.location.href
。相關(guān)代碼如下:
// 方式(1) 直接設(shè)置window.location.href
window.location.href = "jsbridge://toast?{msg:jstojava}";
// 方式(2) 在需要js調(diào)用native api的時候,js在頁面中創(chuàng)建一個不可見的iframe,設(shè)置這個iframe的地址
var iframe = document.createElement("iframe");
iframe.style.display = "none";
document.documentElement.appendChild(iframe);
iframe.src = "jsbridge://toast?{msg:jstojava}";
prompt环揽,console.log略荡,alert
這部分我們要講的三個方法(原理同上),都是瀏覽器實現(xiàn)的API接口:
prompt:默認(rèn)顯示一個對話框歉胶,對話框中包含一條文字信息汛兜,用來提示用戶輸入文字;
console.log:默認(rèn)向web控制臺輸出一條消息通今;
alert:默認(rèn)用于顯示帶有一條指定消息和一個 OK 按鈕的警告框粥谬。
對于上述三個方法的默認(rèn)行為,大家可通過 chrome 的開發(fā)者工具試試辫塌,調(diào)用方式非常簡單漏策。所以,我們只要能攔截這三個方法的默認(rèn)行為并獲得其中的參數(shù)即可臼氨。而 Android 中的類WebChromeClient確實存在相對應(yīng)的方法來處理掺喻,只要覆寫 WebChromeClient 中相對應(yīng)的三個方法,并設(shè)置 Webview储矩。下面是這三個方法的方法聲明感耙,要注意的是這三個方法的參數(shù)差異是有點大,具體使用那個參數(shù)可能需要與 JS 端配合:
class WebChromeClientImp extends WebChromeClient {
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
}
@Override
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
}
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
}
}
0x02 相關(guān)代碼實現(xiàn)
在上面一節(jié)主要介紹了 JS 與 Java 互相調(diào)用的方法持隧,在這一節(jié)主要是如何這些方法即硼。雖然以上這些還是很簡單的,但寫個 demo 實踐一下還是必要的屡拨。這個 demo 已上傳至 github 只酥,有興趣的同學(xué),請點這里 jsbridgeDemo呀狼。這個demo的功能如下:
利用上述的兩種方式實現(xiàn) JS 調(diào)用 Native 端的 toast 功能裂允;
Native 端調(diào)用 JS 端的方法實現(xiàn)修改頁面背景色的功能。
在這個 demo 實現(xiàn)比較簡單赠潦,我這里就稍微說明幾點:
第一叫胖,這個 demo 項目需要一個頁面來承載,這個頁面可以發(fā)布在外網(wǎng)上或者它就寫在本地項目中她奥。本文使用了后面這種方式瓮增,因為比較方便,具體操作方式是:在 Android 項目的根目錄下創(chuàng)建assets
目錄(如果該目錄不存在的話)哩俭,并創(chuàng)建頁面jsdemo.html
在該目錄下绷跑,代碼中加載頁面的方式為webView.loadUrl("file:///android_asset/jsdemo.html")
;
第二凡资,JS 端調(diào)用prompt()
與alert()
后砸捏,Native 端必須給 JS 端回調(diào)確認(rèn)谬运,否則會有問題,因為兩者都是會彈框垦藏,需要給響應(yīng):
result.confirm();
第三梆暖,在 Native 代碼需要設(shè)置 Webview 啟用WJavaScript:
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
第四,目前實現(xiàn)只是對偽協(xié)議做了字符串比較掂骏,最好的方式當(dāng)然是在 JS 和 Native 端各自封裝相對模塊來處理相關(guān)邏輯轰驳,后續(xù)有時間我會做下修改。
0x03 總結(jié)
本文主要介紹了 Java 與 JavaScript 相互調(diào)用的方式弟灼,特別是 JavaScript 調(diào)用 Java 的幾種方法:當(dāng)然级解,Android 原生提供的方式由于安全的問題是不被推薦的,但是隨著 Android 4.2及之后版本的普及田绑,這未必不是一種好的方式勤哗;關(guān)于其他幾種方法應(yīng)該都是可以使用的,但都需要自己做一定封裝掩驱;還有就是這幾種方法的相關(guān)調(diào)用性能估計是不一樣芒划,大家在選擇的時候需要做下對比,本文暫時沒有涉及到昙篙。