在 Android WebView 中實現和 JavaScript 的互操作

前言

在 APP 中內嵌一個 H5 來實現特定的業務功能已經是非常成熟且常用的方案了。

雖然 H5 已經能夠實現大多數的需求,但是對于某些需求還是得依靠原生代碼來實現然后與 JavaScript 進行交互,例如我目前所負責的項目就是一個 “智能硬件” 設備,需要外接非常多的硬件或傳感器獲取特定的數據,并在實際業務中使用。此時如果直接使用 H5 是無法獲取到這些數據的,這就必須依賴于安卓原生提供相應的數據。

JavaScript 調用 Android 原生方法

webView.addJavascriptInterface()

簡介

webView.addJavascriptInterface() 有兩個參數 Object obj, String interfaceName

  1. 其中 object 即需要提供給 js 調用的對象。在 Android 4.1.2 (API 16) 以下時,js 可以調用該對象的所有公開方法;在 Android 4.2 (API 17)以上時, js 只能調用添加了 @JavascriptInterface 注解的公開方法。

之所以會有這樣的改動,是因為在 API 16 之前可以調用所有公開方法具有安全隱患,例如可以利用 jave 的反射機制實現任意命令的執行。

  1. interfaceName 即 js 調用時的接口名稱。

使用方法

首先,我們定義一個類用于給 js 調用:

class TestJsBridge {@JavascriptInterfacefun getCurrentTemperature(): String {val data = "37.5" // 模擬從傳感器獲取的數據return data}
}

然后,我們需要允許 WebView 的 js 支持:

val webSettings = webView.settings
webSettings.javaScriptEnabled = true

接下來,將第一步中定義的 TestJsBridge 對象通過 addJavascriptInterface 注入到 js 中:

webView.addJavascriptInterface(TestJsBridge(), "NativeBridge")

現在,我們就可以直接在 JavaScript 中調用這個方法了:

<script type="text/javascript">var temp = NativeBridge.getCurrentTemperature();
</script>

此時,在 js 中,temp 的值就是 37.5

另外需要注意的是,js 調用 java 的方法不是在主線程中調用的,而是在 webview 自己線程中調用的,所以在編寫某些涉及到 UI 的操作時需要先切換至主線程。

漏洞解析

對了,上文中說過在 API 16 以下的 addJavascriptInterface 有安全隱患,這里簡單舉一個例子演示如何通過反射在 js 中執行任意 sh。

首先,依舊是提供一個對象供 js 調用,這里我們直接給一個空對象:

class TestJsBridge {}

然后注入到 js 中:

webView.addJavascriptInterface(TestJsBridge(), "NativeBridge")

最后在 js 中這樣寫:

<script type="text/javascript">
for (var obj in window) {try {if ("getClass" in window[obj]) {try{ret= NativeBridge.getClass().forName("java.lang.Runtime").getMethod('getRuntime',null).invoke(null,null).exec(['echo', 'hello,equationl', '>', './sdcard/hack.txt']);} catch(e) { }}} catch(e) {}
}
</script>

這樣,即使我們注入 js 的對象什么方法都沒寫,還是會被執行 sh ,上述 sh 就是輸入一段字符串 “hello,equationl” 到 /sdcard/hack.txt 文件中。

shouldOverrideUrlLoading 攔截 URL

簡介

我們可以通過 webview 的 shouldOverrideUrlLoading 攔截到當前請求的 URL,并且可以修改以什么樣的方式去處理這個 URL。

換言之,我們可以在 js 中通過請求不同的 URL 來實現調用 java 代碼并且傳遞值。

使用方法

首先,我們需要自己規定一下哪種形式的 URL 會被認為是需要被攔截處理的。

這里我們就簡單的定為 “jsBridge://” 開頭的 URL 表示需要被攔截處理,而其后跟著的路徑表示調用哪個方法以及附帶的參數。

例如,“jsBridge://getNewMsg?id=monkey_fish” 表示需要調用 java 的 getNewMsg 方法,并且附帶參數 id 為 monkey_fish 。

接下來,我們覆寫 webview 的 shouldOverrideUrlLoading 方法,并在其中對 URL 進行處理。

webView.webViewClient = object : WebViewClient() {override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {// ------  對alipays:相關的scheme處理 -------val url = request.url.toString()if (url.startsWith("jsBridge://")) {// 解析參數等等等,然后調用安卓代碼,這里假設是跳轉到一個新的 Activity// ……val intent = Intent(this@WebViewHolderActivity, MsgActivity::class.java)startActivity(intent)return true}return super.shouldOverrideUrlLoading(view, request)}
}

shouldOverrideUrlLoading 方法中返回 true 表示當前 URL 已被攔截,webview 將取消繼續加載,false 則表示繼續使用 webview 加載。

那么,js 如何調用這個方法呢?其實也很簡單,只要重定向一下當前網址即可:

<script type="text/javascript">document.location = "jsBridge://getNewMsg?id=monkey_fish";
</script>

通過上面的例子我們可以看出,這個方式其實不太適合于 js 和 安卓原生的交互,反而更適合用于回調某些內容,并且這個內容不需要網頁繼續操作。

事實上,大多數情況下這個方法是用于網頁授權登錄或者網頁支付等場景的。

例如,業務中某項第三方授權登錄使用的是 webview 打開第三方授權網頁,在網頁上完成登錄后,該第三方網頁會重定向到特定的 URL,并在其中帶入 token,例如:“authorize:xxxxxxxxxxx”。

此時,我們只需要攔截具有上述規則的 URL,并跳轉到我們的登錄界面即可,也就是說,后續操作就沒有這個網頁什么事了。

攔截對話框

簡介

我們還可以通過覆寫 onJsAlertonJsConfirmonJsPrompt 實現對 js 中的 alert() confirm() prompt() 三種不同的對話框的攔截和修改,從而變相的達到 js 調用原生代碼的目的。

三個對話框都可以通過 message 參數向安卓傳遞參數。

第一個對話框不能返回數據給 js ; 第二個對話框只能返回一個 Boolean 值給 js ;最后一個對話框 onJsPrompt 可以返回一個字符串給 js,所以一般都是使用 onJsPrompt 來實現 js 和安卓的交互,因此我們接下來就只以 onJsPrompt 舉例。

使用方法

要覆寫 onJsPrompt 需要先創建一個類繼承自 WebChromeClient() ,然后在其中覆寫 onJsPrompt

    class MyWebChromeClient : WebChromeClient() {override fun onJsPrompt(view: WebView, url: String, message: String, defaultValue: String, result: JsPromptResult): Boolean {val resultMsg = getNext(message)result.confirm(resultMsg)return true}}

其中,result.confirm(resultMsg) 相當于我們點擊了這個對話框的確定按鈕,并且返回提供的值(resultMsg)。如果調用 result.cancel() 則相當于點擊了這個對話框的取消按鈕,此時返回值為 null 。

getNext 是我們的原生邏輯代碼,它會返回一個 String 的結果:

    fun getNext(id: String): String {// ……if (id == "fish") return "我多么想成為你的鹿"// ……return ""}

然后將該類設置到 webview 上:

webView.webChromeClient = MyWebChromeClient()

現在,我們只需要在 js 中如此調用即可:

<script type="text/javascript">var nextMsg = prompt("fish");
</script>

此時 js 中的 nextMsg 變量就是通過原生安卓拿到的 “我多么想成為你的鹿” 。

Android 調用 JavaScript 方法

evaluateJavascript()

要在 webview 中調用 js 代碼也非常簡單,官方給出的方案就是直接使用 evaluateJavascript()

evaluateJavascript() 接收兩個參數: scriptresultCallback ,其中 script 就是我們要執行的 js 代碼,可以執行任意 js 代碼;而 resultCallback 是執行結果回調,返回結果是 String 類型。

使用起來也十分簡單,例如我們想要調用 js 顯示一個 alert 彈框:

webView.evaluateJavascript("alert('hello, my fish, my monkey');") {println("執行結果: $it")
}

需要注意的是這里的返回結果是空的,因為 alert 本來就沒有返回值。

loadUrl()

另外一種在 webview 中調用 js 的代碼的方法就是使用 loadUrl(),其實顧名思義,loadUrl() 是用來加載 URL 的,但是它同樣可以用來執行 js ,就如同我們直接在瀏覽器地址欄中輸入一樣:

javascript:alert('hello, my deer');

1.png

在 webview 中使用也一樣:

webView.loadUrl("javascript:alert('hello, my deer');")

但是使用這種方式調用有一種顯而易見的缺點,那就是我們無法直接拿到 js 執行的結果。

實踐使用

上面已經簡要介紹了如何實現安卓原生和 H5 或者說和 js 的交互。

下面我就簡單說一下在實際中的應用。

還是以我負責的這個項目為例,在我這個項目中更多的是需要將硬件的能力或者說數據傳遞給 js 以供 H5 來使用,所以我基本都是在使用 webView.addJavascriptInterface()

另外在提供數據給 js 時還會涉及到兩種提供方式。

因為在這個項目中,所有硬件的數據都是實時輪詢后實時回報給安卓端 APP 的,所以在提供給 JS 時同樣需要提供兩種形式的數據:一是當前某個傳感器的瞬時數據;二是希望能夠實時提供某個傳感器的數據。

對于情況一非常好實現,這里以獲取溫度傳感器的瞬時值舉例。

首先先定義一些工具方法,用于將返回的數據格式化成固定格式:

    fun getCommonResponse(code: Int = WebViewCode.OK, message: String = "", data: String): String {return Gson().toJson(CommonResponse(code, message, data))}

然后定義需要注入 js 的接口:

class JsTemp {@JavascriptInterfacefun getCurrentTemp(): String {if (!TempManager.isConnected()) {return WebViewUtil.getCommonResponse(code = WebViewCode.TempNotConnect, message = "沒有連接溫度傳感器", data = "")}if (!TempManager.isDeviceExist()) {return WebViewUtil.getCommonResponse(code = WebViewCode.TempNotFound, message = "沒有可用的溫度傳感器", data = "")}return WebViewUtil.getCommonResponse(data = TempManager.currentTemp.toString())}
}

將其注入 webView:

val jsTemp by lazy { JsTemp() }
val JsTempObject = "NativeTemp"// ……webView.addJavascriptInterface(jsTemp, JsTempObject)

然后在 H5 中如此調用:

<html><head><title>test</title>
</head><body><div class="toast-div" id="currentTemp" onclick="getCurrentTemp()">獲取當前溫度</div><div id="temp">temp: null</div></body><script type="text/javascript">function getCurrentWeight() {document.getElementById("temp").innerHTML = "current temp: "+ NativeTemp.getCurrentTemp();}</script>
</html>

這樣即可在 H5 獲取當前溫度的瞬時值。

如果我們想要在 H5 中實時獲取溫度值的話,我們可以事先在 js 中定義好需要的回調函數,然后將函數傳遞給 webview,再由安卓原生在輪詢溫度值時通過 evaluateJavascript 將值回調給設置的 js 回調函數。

代碼如下,

首先,在 H5 中定義好用于接收溫度的值的回調函數,以及界面:

<!-- …… --><div class="toast-div" onclick="NativeTemp.addOnTempChangeListener('onTempChange')">添加溫度監聽</div><div class="toast-div" onclick="NativeTemp.removeOnTempChangeListener('onTempChange')">移除溫度監聽</div><!-- …… --><script type="text/javascript"><!-- …… -->function onTempChange(temp) {document.getElementById("temp").innerHTML = "temp callback: "+temp + " | " + Date.now();}<!-- …… --></script>

其中的 onTempChange 即為我們定義的用于接收回調的 js 函數名稱。

然后在安卓的接口類中:

// ……/*** 添加溫度改變時的監聽回調** @param callbackFunName JS 函數名,溫度改變時回調給哪個 JS 函數* @return 返回添加結果* */@JavascriptInterfacefun addOnTempChangeListener(callbackFunName: String): String {if (!TempManager.isConnected()) {return WebViewUtil.getCommonResponse(code = WebViewCode.TempNotConnect, message = "沒有連接溫度傳感器", data = "")}if (!TempManager.isDeviceExist()) {return WebViewUtil.getCommonResponse(code = WebViewCode.TempNotFound, message = "沒有可用的溫度傳感器", data = "")}val result = onTempChangeFunName.add(callbackFunName)return if (result) {WebViewUtil.getCommonResponse(data = "OK")} else {WebViewUtil.getCommonResponse(code = WebViewCode.CallBackAlreadyAdd, message = "$callbackFunName 已經添加", data = "")}}/*** 移除溫度改變時的監聽回調** @param callbackFunName JS 函數名,已添加的 JS 函數* @return 返回移除結果* */@JavascriptInterfacefun removeOnTempChangeListener(callbackFunName: String): String {val result = onTempChangeFunName.remove(callbackFunName)return if (result) {WebViewUtil.getCommonResponse(data = "OK")} else {WebViewUtil.getCommonResponse(code = WebViewCode.CallBackNotExist, message = "$callbackFunName 不存在", data = "")}}
// ……

其中的 onTempChangeFunName 是我們的定義的一個 Set ,用于存放當前設置的回調函數名稱:val onTempChangeFunName: MutableSet<String> = mutableSetOf()

最后,在輪詢溫度的地方調用:

while (true) {// ……val result = 36.5 // 模擬輪詢到溫度結果// ……if (onTempChangeFunName.isNotEmpty()) {val json = WebViewUtil.getCommonResponse(data = result.toString())for (function in onTempChangeFunName) {webView.evaluateJavascript("$function('$json');") {}}}delay(50)
}

自此,我們實時獲取溫度傳感器數值的目的也達成了。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/213576.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/213576.shtml
英文地址,請注明出處:http://en.pswp.cn/news/213576.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【PyTorch】卷積神經網絡

文章目錄 1. 理論介紹1.1. 從全連接層到卷積層1.1.1. 背景1.1.2. 從全連接層推導出卷積層 1.2. 卷積層1.2.1. 圖像卷積1.2.2. 填充和步幅1.2.3. 多通道 1.3. 池化層&#xff08;又稱匯聚層&#xff09;1.3.1. 背景1.3.2. 池化運算1.3.3. 填充和步幅1.3.4. 多通道 1.4. 卷積神經…

實現React18加TS,解決通用后臺管理系統,實戰方案落地有效實踐經驗

隨著前端技術的不斷發展和更新&#xff0c;使用React 18結合TypeScript&#xff08;TS&#xff09;來構建通用后臺管理系統已成為一種常見的選擇。本文將介紹如何在項目中應用React 18和TS&#xff0c;并分享一些實戰方案的有效實踐經驗。 一、搭建React 18 TS項目 首先&…

12.2每日一題(1無窮型冪指函數:二倍角公式+三部曲+等價無窮小代換(只有整體的因子不為0才能先算出來))

注意&#xff1a;求極限不能想先算哪里就先算哪里&#xff0c;只有整體的因子不為0才能先算出來&#xff0c;部分不為0不可以先算

外貿老業務也棘手的一個問題

這幾天有2個老業務都被一個類同的問題纏住了。 客戶定購了三臺車&#xff0c;由于是非常規要求所以我建議收取全款或者最少收50%的定金。但是業務員為了當月業績或者為了拿到就收了客戶20% 或者30% &#xff0c;定金收到了&#xff0c;我也不好再逼著業務員去加收定金。 訂單就…

記錄 | ubuntu上安裝fzf

在 ubuntu 上采用命令行安裝 fzf 的方式行不通 指的是采用下面的方式行不通&#xff1a; sudo apt install fzf # 行不通 sudo snap install fzf --classic # 行不通正確的安裝方式是&#xff1a; ● 到 fzf 的 git 倉庫&#xff1a;https://github.com/junegunn/fzf/re…

在高數據量中如何優化MySQL的Group by語句?

在實際開發環境中&#xff0c;MySQL的GROUP BY操作的優化需要結合具體的業務場景和數據特點。以下是一些建議&#xff0c;可以幫助你在實際開發中優化GROUP BY查詢&#xff1a; 使用合適的索引&#xff1a; 確保GROUP BY和ORDER BY中的列上存在索引。這有助于加速分組和排序操作…

計算機畢業設計 基于SpringBoot的電動車租賃系統的設計與實現 Java實戰項目 附源碼+文檔+視頻講解

博主介紹&#xff1a;?從事軟件開發10年之余&#xff0c;專注于Java技術領域、Python人工智能及數據挖掘、小程序項目開發和Android項目開發等。CSDN、掘金、華為云、InfoQ、阿里云等平臺優質作者? &#x1f345;文末獲取源碼聯系&#x1f345; &#x1f447;&#x1f3fb; 精…

場景示例:有贊商城 × 微盛企微管家,助力零售企業,實現私域運營自動化

1 場景描述 在零售行業內&#xff0c;線上渠道已經是零售行業的主要銷售渠道&#xff0c;大多數零售企業都會將產品上架到有贊商城&#xff0c;并使用微盛企微管家系統進行客戶管理和服務&#xff0c;希望能對客戶畫像進行精細化管理&#xff0c;以提升銷售和服務效率。 然而&a…

2023年最新prometheus + grafana搭建和使用+gmail郵箱告警配置

一、安裝prometheus 1.1 安裝 prometheus官網下載地址 sudo -i mkdir -p /opt/prometheus #移動解壓后的文件名到/opt/,并改名prometheus mv prometheus-2.45 /opt/prometheus/ #創建一個專門的prometheus用戶&#xff1a; -M 不創建家目錄&#xff0c; -s 不讓登錄 useradd…

女士內衣市場分析:預計2028年將達到643.08億美元

內衣 (英文名:Underwear)&#xff0c;是指貼身穿的衣物。內衣有保暖及污穢的危害作用&#xff0c;有時會被視為性征。女士內衣行業生產的主要原料是各類織布或無紡布&#xff0c;成分有海綿、邊、定型紗、骨膠、肩帶等&#xff0c;布面料在內衣企業的生產成本中所占比重較大。女…

Python基礎(四、探索迷宮游戲)

Python基礎&#xff08;四、探索迷宮游戲&#xff09; 游戲介紹游戲說明 游戲介紹 在這個游戲中&#xff0c;你將扮演一個勇敢的冒險者&#xff0c;進入了一個神秘的迷宮。你的任務是探索迷宮的每個房間&#xff0c;并最終找到隱藏在其中的寶藏。 游戲通過命令行界面進行交互…

web 前端之標簽練習+知識點

目錄 實現過程&#xff1a; 結果顯示 1、HTML語法 2、注釋標簽 3、常用標簽 4、新標簽 5、特殊標簽 6、在網頁中使用視頻和音頻、圖片 7、表格標簽 8、超鏈接標簽 使用HTML語言來實現該頁面 實現過程&#xff1a; <!DOCTYPE html> <html><head>…

泡沫包裝市場分析:預計2029年將達到659億元

泡沫包裝&#xff0c;簡單地講&#xff0c;就是用數學方法對無線電測量或光學測量所獲得的彈道數據進行檢驗、整理、校正、計算&#xff0c;減小或消除數據的誤差&#xff0c;得出反映運載火箭運動軌跡的精確彈道參數。通常所說的泡沫包裝&#xff0c;主要是指由可發性聚苯乙烯…

面試操作系統八股文五問五答第二期

面試操作系統八股文五問五答第二期 作者&#xff1a;程序員小白條&#xff0c;個人博客 相信看了本文后&#xff0c;對你的面試是有一定幫助的&#xff01; ?點贊?收藏?不迷路&#xff01;? 1.怎么解決死鎖&#xff1f; 1、預防死鎖&#xff1a;通過設置一些限制條件&am…

JAVA面試題8

1.Java中的線程是什么&#xff1f; 它有什么作用&#xff1f; 答案&#xff1a;線程是程序執行流的最小單位&#xff0c;用于實現多任務并發執行。Java中的線程可以實現并發編程&#xff0c;提高程序的性能和響應性。 2.什么是Java中的同步&#xff08;Synchronization&#x…

超靜音的兩相步進電機驅動芯片GC6609,GC6610的性能分析

兩相步進電機驅動芯片GC6609&#xff0c;GC6610它們是一款超靜音的兩相步進電機驅動芯片&#xff0c;內置最大 256 細分的步進驅動模式&#xff0c; 超靜音&#xff0c;低振動。芯片可以工作在 4~36V 的寬工作電壓范圍內&#xff0c;平均工作電流可以達到 2A和2.5A &#xff0c…

大數據機器學習算法項目——基于Django/協同過濾算法的房源可視化分析推薦系統的設計與實現

大數據機器學習算法項目——基于Django/協同過濾算法的房源可視化分析推薦系統的設計與實現 技術棧&#xff1a;大數據爬蟲/機器學習學習算法/數據分析與挖掘/大數據可視化/Django框架/Mysql數據庫 本項目基于 Django框架開發的房屋可視化分析推薦系統。這個系統結合了大數據…

STM32-01-認識單片機

文章目錄 一、單片機簡介二、Cortex-M系列介紹三、初識STM32四、STM32原理圖設計五、搭建開發環境六、STM32初體驗七、MDK5使用技巧 一、單片機簡介 單片機是什么&#xff1f; 單片機&#xff1a;Single-Chip Microcomputer&#xff0c;單片微型計算機&#xff0c;是一種集成電…

python獲得曲線峰值的個數

import numpy as np from scipy.signal import find_peaks import matplotlib.pyplot as plt# 生成示例數據 x np.linspace(0, 10, 100) y np.sin(x)# 查找峰值 peaks, _ find_peaks(y)# 繪制曲線和峰值點 plt.plot(x, y) plt.plot(x[peaks], y[peaks], ro)# 顯示峰值個數 n…

Golang channle(管道)基本介紹、快速入門

channel(管道)-基本介紹 為什么需要channel&#xff1f;前面使用全局變量加鎖同步來解決goroutine的通訊&#xff0c;但不完美 1)主線程在等待所有goroutine全部完成的時間很難確定&#xff0c;我們這里設置10秒&#xff0c;僅僅是估算。 2)如果主線程休眠時間長了&#xff0c…