1、交互方式总结
对于 Android 调用 JS 代码的方法有 2 种:
a. 通过 WebView 的 loadUrl()
b. 通过 WebView 的 evaluateJavascript()
对于 JS 调用 Android 代码的方法有 3 种:
a. 通过 WebView 的 addJavascriptInterface() 进行对象映射
b. 通过 WebViewClient 的 shouldOverrideUrlLoading() 方法回调拦截 url
c. 通过 WebChromeClient 的 onJsAlert()、onJsConfirm()、onJsPrompt() 方法回调拦截 JS 对话框 alert()、confirm()、prompt() 消息
2、Android 调用 JS
- 方式 1:通过 WebView 的 loadUrl()
JS 代码:
// JS 代码
<script>
// Android 需要调用的方法
function callJS() {
alert("Android 调用了 JS 的 callJS 方法");
}
</script>
Android 代码:
// 注意调用的 JS 方法名要对应上
// 调用 javascript 的 callJS() 方法
mWv.loadUrl("javascript:callJs('" + mMsg + "')");
完整代码:
public class MainActivity extends AppCompatActivity {
WebView mWebView;
Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = mWebView.getSettings();
// 设置与 Js 交互的权限
webSettings.setJavaScriptEnabled(true);
// 设置允许 JS 弹窗
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
// 先载入 JS 代码
// 格式规定为:file:///android_asset/文件名.html
mWebView.loadUrl("file:///android_asset/javascript.html");
button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 通过 Handler 发送消息
mWebView.post(new Runnable() {
@Override
public void run() {
// 注意调用的 JS 方法名要对应上
// 调用 javascript 的 callJS() 方法
mWebView.loadUrl("javascript:callJS()");
}
});
}
});
// 由于设置了弹窗检验调用结果,所以需要支持 js 对话框
// webview 只是载体,内容的渲染需要使用 webviewChromClient 类去实现
// 通过设置 WebChromeClient 对象处理 JavaScript 的对话框
// 设置响应 js 的 Alert() 函数
mWebView.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
AlertDialog.Builder b = new AlertDialog.Builder(MainActivity.this);
b.setTitle("Alert");
b.setMessage(message);
b.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
result.confirm();
}
});
b.setCancelable(false);
b.create().show();
return true;
}
});
}
}
特别注意:JS 代码调用一定要在 onPageFinished() 回调之后才能调用,否则不会调用。
- 方式2:通过 WebView 的 evaluateJavascript()
优点:该方法比第一种方法效率更高、使用更简洁。
因为该方法的执行不会使页面刷新,而第一种方法(loadUrl )的执行则会。
Android 4.4 后才可使用
// 只需要将第一种方法的 loadUrl() 换成下面该方法即可
mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
//此处为 js 返回的结果
}
});
}
使用建议
两种方法混合使用,即 Android 4.4 以下使用方法 1,Android 4.4 以上方法 2
// Android 版本变量
final int version = Build.VERSION.SDK_INT;
// 因为该方法在 Android 4.4 版本才可使用,所以使用时需进行版本判断
if (version < 18) {
mWebView.loadUrl("javascript:callJS()");
} else {
mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
//此处为 js 返回的结果
}
});
}
3、JS 通过 WebView 调用 Android 代码
- 方式 1:通过 WebView 的 addJavascriptInterface() 进行对象映射
步骤 1:定义一个与 JS 对象映射关系的 Android 类:AndroidtoJs
// 继承自 Object 类
public class AndroidtoJs extends Object {
// 定义 JS 需要调用的方法
// 被 JS 调用的方法必须加入 @JavascriptInterface 注解
@JavascriptInterface
public void hello(String msg) {
System.out.println("JS 调用了 Android 的 hello 方法");
}
}
步骤 2:将需要调用的 JS 代码以 .html 格式放到 src/main/assets 文件夹里
<script>
function callAndroid() {
// 由于对象映射,所以调用 test 对象等于调用 Android 映射的对象
window.test.hello("js 调用了 android 中的 hello 方法");
}
</script>
步骤 3:在 Android 里通过 WebView 设置 Android 类与 JS 代码的映射
mWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = mWebView.getSettings();
// 设置与 Js 交互的权限
webSettings.setJavaScriptEnabled(true);
// 通过 addJavascriptInterface() 将 Java 对象映射到 JS 对象
// 参数 1:Javascript 对象名
// 参数 2:Java 对象名
mWebView.addJavascriptInterface(new AndroidtoJs(), "test"); // AndroidtoJS 类对象映射到 js 的 test 对象
// 加载 JS 代码
// 格式规定为:file:///android_asset/文件名.html
mWebView.loadUrl("file:///android_asset/javascript.html");
特点
优点:使用简单
缺点:存在严重的漏洞问题(Android 4.2 之后函数以 @JavascriptInterface 进行注解从而避免漏洞攻击)
- 方式 2:通过 WebViewClient 的方法 shouldOverrideUrlLoading() 回调拦截 url
具体原理:
(1) Android 通过 WebViewClient 的回调方法 shouldOverrideUrlLoading() 拦截 url
(2) 解析该 url 的协议
(3) 如果检测到是预先约定好的协议,就调用相应方法
具体使用:
步骤 1:在 JS 约定所需要的 Url 协议
<script>
function callAndroid(){
/*约定的 url 协议为:js://webview?arg1=111&arg2=222*/
document.location = "js://webview?arg1=111&arg2=222";
}
</script>
当该 JS 通过 Android 的 mWebView.loadUrl("file:///android_asset/javascript.html") 加载后,就会回调 shouldOverrideUrlLoading()
步骤 2:在 Android 通过 WebViewClient 复写 shouldOverrideUrlLoading()
// 复写 WebViewClient 类的 shouldOverrideUrlLoading 方法
mWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
// 步骤 2:根据协议的参数,判断是否是所需要的 url
// 一般根据 scheme(协议格式)& authority(协议名)判断(前两个参数)
// 假定传入进来的 url = "js://webview?arg1=111&arg2=222"(同时也是约定好的需要拦截的)
Uri uri = Uri.parse(url);
// 如果 url 的协议 = 预先约定的 js 协议
// 就解析往下解析参数
if (uri.getScheme().equals("js")) {
// 如果 authority = 预先约定协议里的 webview,即代表都符合约定的协议
// 所以拦截url,下面JS开始调用Android需要的方法
if (uri.getAuthority().equals("webview")) {
// 步骤 3:
// 执行 JS 所需要调用的逻辑
System.out.println("js调用了Android的方法");
// 可以在协议上带有参数并传递到 Android 上
Set<String> collection = uri.getQueryParameterNames();
Iterator<String> iterator = collection.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
String value = uri.getQueryParameter(next);
System.out.println("key:" + next + ",value:" + value);
}
}
return true;
}
return super.shouldOverrideUrlLoading(view, url);
}
}
);
特点
优点:不存在方式 1 的漏洞;
缺点:JS 获取 Android 方法的返回值复杂。
如果 JS 想要得到 Android 方法的返回值,只能通过 WebView 的 loadUrl() 去执行 JS 方法把返回值传递回去,相关的代码如下:
// Android:MainActivity.java
mWebView.loadUrl("javascript:returnResult(" + result + ")");
// JS:javascript.html
function returnResult(result) {
alert("result is" + result);
}
-
方式 3:通过 WebChromeClient 的 onJsAlert()、onJsConfirm()、onJsPrompt() 方法回调拦截 JS 对话框 alert()、confirm()、prompt() 消息
在 JS 中,有三个常用的对话框方法:
方式 3 的原理:Android 通过 WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt() 方法回调分别拦截 JS 对话框 (即上述三个方法),得到他们的消息内容,然后解析即可。
下面的例子将用拦截 JS 的输入框(即 prompt() 方法)说明 :
a.常用的拦截是:拦截 JS 的输入框(即 prompt() 方法)
b.因为只有 prompt() 可以返回任意类型的值,操作最全面方便、更加灵活;而 alert() 对话框没有返回值;confirm() 对话框只能返回两种状态(确定 / 取消)两个值
步骤 1:加载 JS 代码,如下:
<script>
function clickprompt() {
// 调用 prompt()
var result=prompt("js://demo?arg1=111&arg2=222");
alert("demo " + result);
}
</script>
当使用 mWebView.loadUrl("file:///android_asset/javascript.html") 加载了上述 JS 代码后,就会触发回调 onJsPrompt(),具体如下:
如果是拦截警告框(即 alert() ),则触发回调 onJsAlert();
如果是拦截确认框(即 confirm() ),则触发回调onJsConfirm();
步骤 2:在 Android 通过 WebChromeClient 复写 onJsPrompt()
mWebView.setWebChromeClient(new WebChromeClient() {
// 拦截输入框(原理同方式 2)
// 参数 message:代表 promt() 的内容(不是 url)
// 参数 result:代表输入框的返回值
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
// 根据协议的参数,判断是否是所需要的 url(原理同方式 2)
// 一般根据 scheme(协议格式)& authority(协议名)判断(前两个参数)
// 假定传入进来的 url = "js://webview?arg1=111&arg2=222"(同时也是约定好的需要拦截的)
Uri uri = Uri.parse(message);
// 如果 url 的协议 = 预先约定的 js 协议
// 就解析往下解析参数
if (uri.getScheme().equals("js")) {
// 如果 authority = 预先约定协议里的 webview,即代表都符合约定的协议
// 所以拦截 url,下面 JS 开始调用 Android 需要的方法
if (uri.getAuthority().equals("demo")) {
// 执行 JS 所需要调用的逻辑
System.out.println("js 调用了 Android 的方法");
// 可以在协议上带有参数并传递到 Android 上
HashMap<String, String> params = new HashMap<>();
Set<String> collection = uri.getQueryParameterNames();
// 参数 result:代表消息框的返回值(输入值)
result.confirm("js调用了Android的方法成功啦");
}
return true;
}
return super.onJsPrompt(view, url, message, defaultValue, result);
}
// 通过 alert() 和 confirm() 拦截的原理相同,此处不作过多讲述
// 拦截 JS 的警告框
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
return super.onJsAlert(view, url, message, result);
}
// 拦截 JS 的确认框
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
return super.onJsConfirm(view, url, message, result);
}
}
);
4、总结
5、WebView 的坑
(1) API 16 之前版本存在远程代码执行漏洞,该漏洞源自于程序没有正确限制使用 WebView.addJavascriptInterface 方法,攻击者可以使用 Java Reflection API 利用该漏洞执行任意 Java 对象和方法。
(2) WebView 的销毁和内存泄漏问题。WebView 的完全销毁是件麻烦事,一旦销毁流程不正确,极易容易导致内存泄漏。
原因:WebView 会关联一个 Activity,WebView 执行的操作是在新线程当中回收,时间 Activity 没有办法确认,Activity 的生命周期和 WebView 线程生命周期不一致导致 WebView 一直执行,因为 WebView 内部持有 Activity 的引用,导致Activity 一直不能被回收,原理类似于匿名内部类持有外部类的引用一样。那么如何解决呢?解决方案如下:
a、使含 WebView 的界面在独立进程(销毁时 System.exit(0)),简单暴力,涉及到进程间通信。(开发过程中常用)
b、动态添加 WebView,对传入 WebView 中使用 Context 弱引用,动态添加 WebView 意思在布局中创建一个 ViewGroup 用来放置 WebView,Activity 创建add 进来,Activity 停止时 remove 掉。
c、从根源解决
前面的方法都没有解决我内存泄漏的问题,然后我看到了一篇文章是从源码角度分析了 webview 内存泄漏的原因,最后按作者的方法解决了问题,后面会贴上原文地址。这里简单说一下:
原文里说的 webview 引起的内存泄漏主要是因为org.chromium.android_webview.AwContents 类中注册了 component callbacks,但是未正常反注册而导致的。
org.chromium.android_webview.AwContents 类中有这两个方法 onAttachedToWindow 和 onDetachedFromWindow;系统会在 attach 和 detach 处进行注册和反注册 component callback;
在 onDetachedFromWindow() 方法的第一行中:
if (isDestroyed()) return;
如果 isDestroyed() 返回 true 的话,那么后续的逻辑就不能正常走到,所以就不会执行 unregister 的操作;我们的 activity 退出的时候,都会主动调用 WebView.destroy() 方法,这会导致 isDestroyed() 返回 true;destroy() 的执行时间又在 onDetachedFromWindow 之前,所以就会导致不能正常进行 unregister()。
然后解决方法就是:让 onDetachedFromWindow 先走,在主动调用 destroy() 之前,把 webview 从它的 parent 上面移除掉。
完整的 activity 的 onDestroy() 方法:
@Override
protected void onDestroy() {
if (mWebView != null) {
// 如果先调用 destroy() 方法,则会命中 if (isDestroyed()) return; 这一行代码,
// 需要先 onDetachedFromWindow(),再 destory()
ViewParent parent = mWebView.getParent();
if (parent != null) {
((ViewGroup) parent).removeView(mWebView);
}
mWebView.stopLoading();
// 退出时调用此方法,移除绑定的服务,否则某些特定系统会报错
mWebView.getSettings().setJavaScriptEnabled(false);
mWebView.clearHistory();
mWebView.clearView();
mWebView.removeAllViews();
mWebView.destroy();
}
super.on Destroy();
}
(3)、jsbridge
通过 javascript 构建一个桥,桥的两端一个端是客户端,一端是服务端,它可以让本地端调用远端的 web 代码,也可以让远端调用本地的代码。
(4)、WebViewClient.onPageFinished(不靠谱)–> WebChromeClient.onProgressChanged(靠谱)
(5)、后台耗电(性能优化)
WebView 开启网页时会自己开启线程,如果没有合理的销毁,那么残余线程就会一直运行,so 这会非常耗电的,解决方案:有一种暴力方式就是 Activity 的 onDestroy 中调用 System.exit() 方法把虚拟机关闭,也可以结合自己应用的 WebView 的情况设计出一个温柔的方案。
(6)、WebView 硬件加速导致的页面渲染问题
WebView 硬件加速偶尔导致页面白块,页面闪烁,但是加载速度比较快,解决方案:关闭硬件加速。
例子
Android studio 中创建 html 文件
//www.greatytc.com/p/9a74d4700e31
1、先创建一个 assets 文件夹,在文件夹里创建一个 test.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>我是个水哥</title>
</head>
<body>
<input type="text" value="我是输入框" id="edit">
<button id="btn1" onclick="callAndroid()">调用 Android 方式 1</button>
<button id="btn2" onclick="callAndroid2()">调用 Android 方式 2</button>
<button id="btn3" onclick="callAndroid3()">调用 Android 方式 3</button>
<script>
// Android 调用 Js
function callJs(msg) {
//想要传递过来的内容显示在输入框中
var edit = document.getElementById("edit");
edit.value = msg;
}
// Js 调用 Android -- 方法 1,addJavaScriptInterface
function callAndroid() {
// android 要通过对象调用方法,所以需要将 java 的对象映射过来
// 由于对象映射,所以调用 test 对象等于调用 Android 映射的对象
window.zhangsan.callAndroid("js 调用了 android 中的方法");
}
// Js 调用 Android -- 方法 2,shouldOverrideUrlLoading
function callAndroid2() {
/*约定的 url 协议为:js://webview?arg1=111&arg2=222*/
document.location = "js://webview?arg1=111&arg2=222";
}
// Js 调用 Android -- 方法 3,WebChromeClient 的 onJsAlert()、onJsConfirm()、onJsPrompt()
function callAndroid3() {
// 调用 prompt()
var result=prompt("js://demo?arg1=3333&arg2=4444");
// 弹窗
alert(result);
}
</script>
</body>
</html>
2、xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical">
<Button
android:id="@+id/btn1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Android 调用 JS ----- 1"/>
<Button
android:id="@+id/btn2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Android 调用 JS ----- 2"/>
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent">
</WebView>
</LinearLayout>
3、AndroidJs(一个与 JS 对象映射关系的 Android 类)(JS 通过 WebView 调用 Android 代码时方式 1 时使用)
import android.webkit.JavascriptInterface;
// 继承自 Object 类
public class AndroidJs extends Object {
// 定义 JS 需要调用的方法
// 被 JS 调用的方法必须加入 @JavascriptInterface 注解
@JavascriptInterface
public void hello(String msg) {
System.out.println("JS 调用了 Android 的 hello 方法");
}
}
4、Activity
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.webkit.JsPromptResult;
import android.webkit.JsResult;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.Toast;
import java.util.Iterator;
import java.util.Set;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button btn1;
private Button btn2;
private WebView webview;
private String msg = "哈哈哈哈哈哈哈哈哈";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
private void initView() {
btn1 = (Button) findViewById(R.id.btn1);
btn2 = (Button) findViewById(R.id.btn2);
webview = (WebView) findViewById(R.id.webview);
// 设置与 Js 交互的权限
WebSettings settings = webview.getSettings();
// 设置允许 JS 弹窗
settings.setJavaScriptEnabled(true);
// 先载入 JS 代码
// 格式规定为:file:///android_asset/文件名.html
webview.loadUrl("file:///android_asset/test.html");
// Js 调用 Android 方法 1
// 参数 1:桥梁类的对象
// 参数 2:对象的名字
// AndroidJs zhangsan = new AndroidJs();
webview.addJavascriptInterface(new AndroidJs(), "zhangsan");
// Js 调用 Android -- 方法 2,shouldOverrideUrlLoading
// 复写 WebViewClient 类的 shouldOverrideUrlLoading 方法
webview.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
// 步骤 2:根据协议的参数,判断是否是所需要的 url
// 一般根据 scheme(协议格式)& authority(协议名)判断(前两个参数)
// 假定传入进来的 url = "js://webview?arg1=111&arg2=222"(同时也是约定好的需要拦截的)
Uri uri = Uri.parse(url);
// 如果 url 的协议 = 预先约定的 js 协议
// 就解析往下解析参数
if (uri.getScheme().equals("js")) {
// 如果 authority = 预先约定协议里的 webview,即代表都符合约定的协议
// 所以拦截 url,下面 JS 开始调用 Android 需要的方法
if (uri.getAuthority().equals("webview")) {
// 步骤3:
// 执行 JS 所需要调用的逻辑
System.out.println("js 调用了 Android 的方法");
// 可以在协议上带有参数并传递到 Android 上
Set<String> collection = uri.getQueryParameterNames();
Iterator<String> iterator = collection.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
String value = uri.getQueryParameter(next);
System.out.println("key:" + next + ",value:" + value);
}
}
return true;
}
return super.shouldOverrideUrlLoading(view, url);
}
});
// Js 调用 Android -- 方法3,WebChromeClient 的 onJsAlert()、onJsConfirm()、onJsPrompt()
webview.setWebChromeClient(new WebChromeClient() {
// 拦截输入框(原理同方式 2)
// 参数message:代表 promt() 的内容(不是 url)
// 参数 result:代表输入框的返回值
@Override
public boolean onJsPrompt(WebView view, String url, String message,
String defaultValue, JsPromptResult result) {
// 根据协议的参数,判断是否是所需要的 url(原理同方式 2)
// 一般根据 scheme(协议格式)& authority(协议名)判断(前两个参数)
// 假定传入进来的 url = "js://webview?arg1=111&arg2=222"(同时也是约定好的需要拦截的)
Uri uri = Uri.parse(message);
// 如果 url 的协议 = 预先约定的 js 协议
// 就解析往下解析参数
if (uri.getScheme().equals("js")) {
// 如果 authority = 预先约定协议里的 webview,即代表都符合约定的协议
// 所以拦截 url,下面 JS 开始调用 Android 需要的方法
if (uri.getAuthority().equals("demo")) {
// 执行 JS 所需要调用的逻辑
System.out.println("js 调用了 Android 的方法");
// 可以在协议上带有参数并传递到 Android 上
Set<String> collection = uri.getQueryParameterNames();
Iterator<String> iterator = collection.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
String value = uri.getQueryParameter(next);
System.out.println("key:" + next + ",value:" + value);
}
// 参数 result:代表消息框的返回值(输入值)
result.confirm("第三种方法的 Android 的返回值");
}
return true;
}
return super.onJsPrompt(view, url, message, defaultValue, result);
}
// 通过 alert() 和 confirm() 拦截的原理相同,此处不作过多讲述
// 拦截 JS 的警告框
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
return super.onJsAlert(view, url, message, result);
}
// 拦截 JS 的确认框
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
return super.onJsConfirm(view, url, message, result);
}
});
btn1.setOnClickListener(this);
btn2.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn1:
webview.loadUrl("javascript:callJs('" + msg + "')");
break;
case R.id.btn2:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
webview.evaluateJavascript("javascript:callJs('" + msg + "')", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
// 接收返回值用的
Toast.makeText(MainActivity.this, value, Toast.LENGTH_SHORT).show();
}
});
}
break;
}
}
}
通过 WebView 选择图片、视频、文件时可以查询
https://www.apiref.com/android-zh/android/webkit/WebChromeClient.FileChooserParams.html