Android与JS交互

1. 交互方式总结

Android 调用 JS 代码的方法有 2 种:

  1. 通过 WebView 的 loadUrl();
  2. 通过 WebView 的 evaluateJavascript();

JS 调用 Android 代码的方法有 3 种:

  1. 通过 WebView 的 addJavascriptInterface() 进行对象映射;
  2. 通过 WebViewClient 的 shouldOverrideUrlLoading() 方法回调拦截 url;
  3. 通过 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()

该方法与第一种方法的区别是:

  1. 该方法比第一种方法执行效率更高,因为该方法的执行不会使页面刷新;
  2. 该方法在 Android 4.4 及以后才可以使用;
  3. 该方法可以拿到 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 能满足大多数场景
相关推荐
奶糖 肥晨7 分钟前
JS自动检测用户国家并显示电话前缀教程|vue uniapp react可用
javascript·vue.js·uni-app
啊花是条龙24 分钟前
《产品经理说“Tool 分组要一条会渐变的彩虹轴,还要能 zoom!”——我 3 步把它拆成 1024 个像素》
前端·javascript·echarts
青茶36040 分钟前
【js教程】如何用jq的js方法获取url链接上的参数值?
开发语言·前端·javascript
晴栀ay1 小时前
React性能优化三剑客:useMemo、memo与useCallback
前端·javascript·react.js
知其然亦知其所以然1 小时前
别再死记硬背了,一篇文章搞懂 JS 乘性操作符
前端·javascript·程序员
json{shen:"jing"}1 小时前
08_组件基础
前端·javascript·vue.js
菩提祖师_2 小时前
基于VR的虚拟会议系统设计
开发语言·javascript·c++·爬虫
hongkid2 小时前
React Native 如何打包正式apk
javascript·react native·react.js
菩提祖师_2 小时前
量子机器学习在时间序列预测中的应用
开发语言·javascript·爬虫·flutter
未来之窗软件服务3 小时前
幽冥大陆(九十二 )Gitee 自动化打包JS对接IDE —东方仙盟练气期
javascript·gitee·自动化·仙盟创梦ide·东方仙盟