1. 交互方式总结
Android 调用 JS 代码的方法有 2 种:
- 通过 WebView 的 loadUrl();
- 通过 WebView 的 evaluateJavascript();
JS 调用 Android 代码的方法有 3 种:
- 通过 WebView 的 addJavascriptInterface() 进行对象映射;
- 通过 WebViewClient 的 shouldOverrideUrlLoading() 方法回调拦截 url;
- 通过 WebChromeClient 的 onJsAlert()、onJsConfirm()、onJsPrompt() 方法回调拦截 JS 对话框的alert()、confirm()、prompt() 消息;
二者沟通的桥梁是 WebView。
2. Android 调用 JS 代码
2.1 通过 WebView 的 loadUrl()
需要加载的 html 文件------javascript.html,把该文件放到 assets 目录下。
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS Interaction</title>
This is a Web View
<script>
function callJS(){
console.log("Android 调用了 JS 的 callJS() 方法");
}
</script>
</head>
</html>
MainActivity.java:
java
package com.example.webviewtest;
import android.os.Bundle;
import android.view.View;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WebView mWebView = findViewById(R.id.webview);
WebSettings webSettings = mWebView.getSettings();
// 允许在 WebView 中执行 JS
webSettings.setJavaScriptEnabled(true);
// 载入 url
mWebView.loadUrl("file:///android_asset/javascript.html");
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 调用 JS 的 callJS() 方法
mWebView.loadUrl("javascript:callJS()");
}
});
}
}
activity_main.xml:
xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="300dp"/>
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Call JS"
android:textAllCaps="false"
android:layout_below="@+id/webview"
android:layout_marginTop="10dp" />
</RelativeLayout>
这里直接通过 WebView 的 loadUrl() 方法传入字符串 javascript:callJS() 即可调用 JS 中的 callJS() 方法,但是 loadUrl() 会使页面刷新,并且该方法拿不到 JS 的返回值。
2.2 通过 WebView 的 evaluateJavascript()
该方法与第一种方法的区别是:
- 该方法比第一种方法执行效率更高,因为该方法的执行不会使页面刷新;
- 该方法在 Android 4.4 及以后才可以使用;
- 该方法可以拿到 JS 的返回值;
把第一种方法中的 JS 改成下面这样:
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS Interaction</title>
This is a Web View
<script>
function callJS(){
return "Android 调用了 JS 的 callJS() 方法";
}
</script>
</head>
</html>
MainActivity.java 修改如下:
java
package com.example.webviewtest;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.view.View;
import android.webkit.ValueCallback;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WebView mWebView = findViewById(R.id.webview);
WebSettings webSettings = mWebView.getSettings();
webSettings.setJavaScriptEnabled(true);
mWebView.loadUrl("file:///android_asset/javascript.html");
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
// values 是 JS 的返回
// evaluateJavascript() 方法返回的值会被封装成一个 JSON 格式的字符串,
// 如果 JS 函数返回 "Hello from JavaScript!",
// Android 端会接收到 "\"Hello from JavaScript!"\" 这样的结果,
// 所以需要转换
String result = value != null ? value.replace("\"", "") : "";
AlertDialog.Builder b = new AlertDialog.Builder(MainActivity.this);
b.setTitle("Alert");
b.setMessage(result);
b.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
b.setCancelable(false);
b.create().show();
}
});
}
});
}
}
evaluateJavascript() 方法有两个参数,第一个参数是要执行的 JS 方法,第二个参数是 JS 执行后触发的回调,在这里可以拿到 JS 方法的返回值。
建议在 Android 4.4 以下使用第一种方法,Android 4.4 及以上使用第二种方法。
java
final int version = Build.VERSION.SDK_INT;
if (version < Build.VERSION_CODES.KITKAT) {
mWebView.loadUrl("javascript:callJS()");
} else {
mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
}
});
}
注意,我这里都是在点击事件中调用 JS 的代码,最好等 WebView 页面加载完,在在 WebViewClient 的 onPageFinished() 方法中调用 JS 的代码。
2.3 两种方法的对比
| 调用方式 | 优点 | 缺点 | 使用建议 |
|---|---|---|---|
| loadUrl() | 简单 | 执行效率低,获取返回值麻烦 | Android 4.4 以下用这个方法 |
| evaluateJavascript() | 执行效率高 | 向下兼容性差(Android 4.4 以上可用) | Android 4.4 以上用这个方法 |
3. JS 调用 Android 代码
3.1 通过 WebView 的 addJavascriptInterface() 进行对象映射
步骤 1 :定义一个要注入到 JS 中的类:
java
package com.example.webviewtest;
import android.webkit.JavascriptInterface;
public class JsObject {
// 对于 Build.VERSION_CODES.JELLY_BEAN_MR1 及以上的版本,
// 只有添加了 @JavascriptInterface 注解的 public 方法才能从 JavaScript 中访问
@JavascriptInterface
public void hello(String msg) {
System.out.println(" JS 调用了 Android 的 hello 方法");
}
}
步骤 2 :在 assets 目录下添加需要加载的 JS 代码:javascript.html
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS Interaction</title>
<script>
function callAndroid(){
// 由于对象映射,所以调用 test 对象等于调用 Android 中的对象
test.hello("js调用了android中的hello方法");
}
</script>
</head>
<body>
<button type="button" id="button1" onclick="callAndroid()">点击调用 Android 代码</button>
</body>
</html>
步骤 3 :在 Android 中通过 WebView 的 addJavascriptInterface() 方法将 Java 对象注入 WebView 中的 JavaScript。
java
package com.example.webviewtest;
import android.os.Bundle;
import android.webkit.WebSettings;
import android.webkit.WebView;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
WebView mWebView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mWebView = findViewById(R.id.webview);
WebSettings webSettings = mWebView.getSettings();
webSettings.setJavaScriptEnabled(true);
// 通过 addJavascriptInterface() 方法将 Java 对象注入 WebView 中的 JavaScript
// 参数1:注入的 Java 对象
// 参数2:暴露在 JS 中对应的实例的名字
mWebView.addJavascriptInterface(new JsObject(), "test");
mWebView.loadUrl("file:///android_asset/javascript.html");
}
}
activity_main.xml:
xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</RelativeLayout>
这样点击 html 中的 button 会调用 callAndroid() 方法,然后会调用 test 实例的 hello() 方法,相应会调用 JsObject 实例的 hello() 方法。
这个方法的优点是简单,缺点是存在安全漏洞。Google 在 Android 4.2 及以上的版本中规定对被调用的函数要以 @JavascriptInterface 进行注解从而避免漏洞攻击,在 Android 4.2 以下 @JavascriptInterface 是无效的,存在安全漏洞。在 Android 4.2 以下需要采用拦截 prompt() 的方式进行漏洞修复。
3.2 通过 WebViewClient 的 shouldOverrideUrlLoading() 方法拦截 url
javascript.html:
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS Interaction</title>
<script>
function callAndroid(){
document.location = "js://webview?arg1=111&arg2=222";
}
</script>
</head>
<body>
<button type="button" id="button1" onclick="callAndroid()">点击调用 Android 代码</button>
</body>
</html>
MainActivity.java:
java
package com.example.webviewtest;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.appcompat.app.AppCompatActivity;
import java.util.Set;
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WebView mWebView = findViewById(R.id.webview);
WebSettings webSettings = mWebView.getSettings();
webSettings.setJavaScriptEnabled(true);
mWebView.loadUrl("file:///android_asset/javascript.html");
mWebView.setWebViewClient(new WebViewClient() {
//该重载方法不建议使用了,Android 7.0 以上已经摒弃了
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return super.shouldOverrideUrlLoading(view, url);
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
// 传入进来的 url = "js://webview?arg1=111&arg2=222"
Uri uri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
uri = request.getUrl();
} else {
uri = Uri.parse(request.toString());
}
// 先判断 scheme
if ("js".equals(uri.getScheme())) {
// 再判断 authority
if ("webview".equals(uri.getAuthority())) {
// 执行对应的代码
Log.d(TAG, "JS 调用了 Android 的方法");
Set<String> sets = uri.getQueryParameterNames();
for (String key : sets) {
Log.d(TAG, "param:" + uri.getQueryParameter(key));
}
return true;
}
}
return super.shouldOverrideUrlLoading(view, request);
}
});
}
}
原理就是通过解析 url 的协议调用相应的代码。
该方式的优点是不存在方式一的漏洞,缺点是如果 JS 想获取 Android 方法的返回值会比较麻烦。
如果 JS 想要拿到 Android 方法的返回值,只能通过 WebView 的 loadUrl() 去执行 JS 代码中的方法把返回值传递给 JS,代码如下:
java
// Android:MainActivity.java
mWebView.loadUrl("javascript:returnResult(" + result + ")");
// JS:javascript.html
function returnResult(result){
alert("result is" + result);
}
3.3 通过 WebChromeClient 的 onJsAlert()、onJsConfirm()、onJsPrompt() 方法回调拦截
在 JS 中,有三个常用的弹出对话框的方法:
- alert(),用于弹出警告框,没有返回值,在文本中加入 \n 可以换行;
- confirm(),用于弹出确认框,有两个返回值,返回 true 表示点击了确认按钮,false 表示点击了取消按钮;
- prompt(),用于弹出输入框,可以任意设置返回值,点击确认会返回输入框中的字符串,点击取消会返回 null;
这 3 个方法分别会触发 WebChromeClient 的 onJsAlert()、onJsConfirm()、onJsPrompt() 方法。
最常用的是使用 JS 的 prompt() 方法,因为只有 prompt() 可以返回任意类型的值,看下面的示例:
javascript.html:
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS Interactiion</title>
<script>
function callPrompt(){
var result = prompt("js://webview?arg1=8&arg2=9");
console.log("result is:" + result);
}
</script>
</head>
<body>
<button type="button" id="button1" onclick="callPrompt()">点击调用 Android 代码</button>
</body>
</html>
MainActivity.java:
java
package com.example.webviewtest;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.webkit.JsPromptResult;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import androidx.appcompat.app.AppCompatActivity;
import java.util.Set;
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WebView mWebView = findViewById(R.id.webview);
WebSettings webSettings = mWebView.getSettings();
webSettings.setJavaScriptEnabled(true);
mWebView.loadUrl("file:///android_asset/javascript.html");
mWebView.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
// 传入进来的 url = "js://webview?arg1=8&arg2=9"
Uri uri = Uri.parse(message);
if (uri.getScheme().equals("js")) {
if (uri.getAuthority().equals("webview")) {
// 执行对应的代码
Log.d(TAG, "JS 调用了 Android 的方法");
Set<String> sets = uri.getQueryParameterNames();
for (String key : sets) {
Log.d(TAG, "param:" + uri.getQueryParameter(key));
}
// JsPromptResult 用于把结果返回给 JS
result.confirm("JS 调用 Android 的方法成功啦!");
}
return true;
}
return super.onJsPrompt(view, url, message, defaultValue, result);
}
});
}
}
3.4 三种方法的对比
| 调用方式 | 优点 | 缺点 | 使用场景 |
|---|---|---|---|
| 通过 WebView 的 addJavascriptInterface() 进行对象映射 | 简单 | Android 4.2 以下存在漏洞 | Android 4.2 以上用这个方法 |
| 通过 WebViewClient 的 shouldOverrideUrlLoading() 方法拦截 url | 没有漏洞问题 | 相对复杂,需要解析 url | 不需要 Android 端的方法的返回值的场景 |
| 通过 WebChromeClient 的 onJsAlert()、onJsConfirm()、onJsPrompt() 方法回调拦截 | 没有漏洞问题 | 相对复杂,需要解析 url | 能满足大多数场景 |