Android使用WebView与Native交互的三种方式 ( 附源码 )

先附上assets目录中html的源代码文件内容,下面的demo都是使用这几个文件:

javascript.html:

DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Carsontitle>
    <script>
      function callAndroid(){
        object.hello("hello");
      }
    script>
head>
<body>
<button type="button" id="button1" onclick="callAndroid()">点击调用Android代码button>
body>
html>

jsToAndroid.html

DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Carson_Hotitle>
    <script>
      function callAndroid(){
        document.location = "js://webview?name=zxj&age=22";   //document.location 改变uri,使其被shouldOverrideUrlLoading捕获到
      }
      function callAndroid1(){
        document.location = "js://webview1?name=AdvanceDev&age=20"
      }
      function getAndroidReturn(result){
         alert("result is " + result)
         setTimeout(null,3000) //设置延时,这样android在使用loadUrl调用getAndroidReturn的时候,alert才能被捕获(因为界面加载完才可以捕获alert)
      }
      /**
      function showAlert(result){
         alert("result is " + result)
      }
      */
    script>
head>
<body>
<button type="button" id="button" onclick="callAndroid()">点击调用Android代码button>
<button type="button" id="button1" onclick="callAndroid1()">点击调用Android-1代码button>
body>
html>

jsToAndroid2.html

DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Carson_Hotitle>
    <script>
      function callAndroid(){
        var result=prompt("js://webView?name=ZXJ&age=22");
        alert("demo " + result);
      }
    script>
head>

<body>
<button type="button" id="button1" onclick="callAndroid()">CallAndroidbutton>
body>
html>

正文开始

1.JS与Native的交互

一.Android调用JS的方法

目前学习了俩种方法:1. 调用webview的loadUrl 2.调用webview的evaluateJavascript

方法说明: 1. webView.loadUrl(“javascript:callJS()”); 参数是一个字符串,说明调用了javascript中的 callJS方法

  1. webview.evaluateJavascript(arg1,arg2); 需要俩个参数,第一个参数是跟上面的loadUrl一样是个字符串,且格式一致。 第二个参数是一个回调,只有一个方法 onReceiveValue(String value),且该方法的参数value就是所调用的JS方法的返回值 (可以使用lambda)

evaluateJavascript 优势在于异步调用,还可以将执行JS代码的结果带回来。

DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>ZhengXJtitle>
    <body>
      <h3>测试--Android调用了JS--界面h3>
    body>
    <script>
       function callJS(){
          alert("Android调用了JS的callJS方法");
          return "方法结束"
       }
    script>
head>
html>

java代码:

public class MainActivity extends AppCompatActivity {
    private final String TAG = "MainActivity";
    private WebView webView;
    private Button button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        webView = findViewById(R.id.webView);
        button = findViewById(R.id.button);

        WebSettings webSettings = webView.getSettings();
        // 设置与JS交互的权限
        webSettings.setJavaScriptEnabled(true);
        webView.loadUrl("file:///android_asset/javascript.html");//必须要的,加载webview页面
        button.setOnClickListener(v -> {
            webView.post(() -> {
                // 调用javascript的callJS()方法
//                webView.loadUrl("javascript:callJS()");
                webView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
                    @Override
                    public void onReceiveValue(String value) {
                        //value是所调用的JS方法的返回值
                        Log.d(TAG, "onReceiveValue: " + value);
                    }
                });
            });
        });


        // 通过设置WebChromeClient对象处理JavaScript的对话框
        // 设置拦截js的Alert()函数
        webView.setWebChromeClient(new WebChromeClient() {
            //拦截JS中的alert()
            @Override
            public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
                AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
                builder.setTitle("Alert");
                builder.setMessage(message);
                builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        result.confirm();  //在onJsAlert返回值会true的时候需要加这个,表示将JS中的Alert也点击确定。否则JS方法不会return
                        //如何onJsAlert的返回值为false则不需要,因为false表示onJsAlert处理完逻辑后,还要交给JS中的Alert去操作,也就是说JS中的Alert也会出现
                    }
                });
                builder.setCancelable(false); //使android中,点击对话框外部或者按下返回键也取消不了对话框。  如果设置为true,则相反
                builder.create().show();

                return false;
            }
        });
    }
}

Android使用WebView与Native交互的三种方式 ( 附源码 )_第1张图片

点击:

Android使用WebView与Native交互的三种方式 ( 附源码 )_第2张图片

二. JS调用Android的代码:

目前学习了三种方法:

  1. 通过webView的addJavascriptInterface(Object object, String name)的方式,利用@JavascriptInterface来映射方法实现调用
  2. 通过webViewCilent的shouldOverrideUrlLoading来拦截Url的方式(需要配合javascript改变document.location的值来获取和拦截Url),解析url然后调用对应的方法
  3. 通过WebChromeClient的onJsAlert()、onJsConfirm()、onJsPrompt()方法回调拦截JS对话框alert()、confirm()、prompt() 消息, 然后解析对应的消息内容来调用对应的方法即可

1. webView的addJavascriptInterface方式

使用步骤:

·创建暴露方法的类,为要暴露的方法加上@JavascriptInterface注解

import android.webkit.JavascriptInterface;

public class AndroidJS extends Object {
    private final String TAG = "AndroidJS";
    @JavascriptInterface
    public void hello(String msg) {
        Log.d(TAG, Thread.currentThread().getId()  + "----" + Thread.currentThread().getName() + "----JS调用了Android的方法: " + msg);
        Log.d(TAG, "当前进程号: " + android.os.Process.myPid());
    }
}

上述log日志 只是用来验证 android通过webview的与JS交互的线程以及进程

结论是:进程号一致,线程不一样,线程是JavaBridge,非主线程main,因此不可以更新UI (多进程开启WebView的方式是解决webview内存泄漏的重要手段之一)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-84YI9O5D-1686711235235)(C:\Users\SW\AppData\Roaming\Typora\typora-user-images\image-20230530155856774.png)]

·调用webview的addJavascriptInterface

        // 通过addJavascriptInterface()将Java对象映射到JS对象
        // 参数1:Javascript对象名  参数2:Java对象名
        // AndroidJS类对象映射到js的object对象
        webView.addJavascriptInterface(new AndroidJS(), "object");

        webView.loadUrl("file:///android_asset/javascript.html"); 

测试用例:

javascript.html:放在了assets目录下

DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Carsontitle>
    <script>
      function callAndroid(){
        object.hello("hello");
      }
    script>
head>
<body>
<button type="button" id="button1" onclick="callAndroid()">点击调用Android代码button>
body>
html>

完整java代码:

public class MainActivity extends AppCompatActivity {
    private final String TAG = "AndroidJS";
    private WebView webView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d(TAG, "onCreate: " + Thread.currentThread().getId()  + "    " + Thread.currentThread().getName());
        Log.d(TAG,"当前主进程: " + android.os.Process.myPid() + "    ");
        webView = findViewById(R.id.webView);

        WebSettings webSettings = webView.getSettings();
        webSettings.setJavaScriptEnabled(true); // 设置与JS交互的权限

        // 通过addJavascriptInterface()将Java对象映射到JS对象
        // 参数1:Javascript对象名  参数2:Java对象名
        // AndroidJS类对象映射到js的object对象
        webView.addJavascriptInterface(new AndroidJS(), "object");
        webView.loadUrl("file:///android_asset/javascript.html"); //加载html页面

    }

}

Android使用WebView与Native交互的三种方式 ( 附源码 )_第3张图片

(按钮是在html页面中的)点击按钮后查看log:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MorZAhJX-1686711235237)(C:\Users\SW\AppData\Roaming\Typora\typora-user-images\image-20230530161235702.png)]

android的hello方法被js调用

2.webViewCilent的shouldOverrideUrlLoading拦截url的方式

使用步骤:

·为WebView添加webViewClient,并重写shouldOverrideUrlLoading方法

 webView.setWebViewClient(new WebViewClient() {
            @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
                // 步骤2:根据协议的参数,判断是否是所需的url
                // 一般根据scheme(协议格式)和authority(协议名)判断前两个参数
                // 假定传进来的 url="js://webview?arg1=111&arg2=222"
                Uri uri = Uri.parse(request.getUrl().toString());
                Log.d(TAG, "shouldOverrideUrlLoading: " + uri);
                // 如果authority=预先约定协议里的webview,即代表符合约定的协议
                // 所以拦截url,下面JS开始调用Android需要的方法
                if (uri.getScheme().equals("js")) {
                    if (uri.getAuthority().equals("webview")) {
                        // 步骤3:执行JS所需要的逻辑
                        // 可以在协议上带有参数并传递到Android上
                        HashMap<String, String> params = new HashMap<>();
                        Set<String> collection = uri.getQueryParameterNames();
                        for(String str : collection){
                            params.put(str,uri.getQueryParameter(str));
                        }
                        Log.d(TAG, "shouldOverrideUrlLoading: " + params);
                        String result = "'从Android返回给JS的result'";
                        view.loadUrl("javascript:getAndroidReturn("+ result+")");
//                        view.evaluateJavascript("getAndroidReturn('从Android返回给JS的result')", null);
                        /**
                         * shouldOverrideUrlLoading: {name=zxj, age=22}
                         * ----------------------------------------------
                         * 获取到参数和值之后,就可以灵活调用对应的方法了
                         */
                    } else if (uri.getAuthority().equals("webview1")) {
                        // 可以在协议上带有参数并传递到Android上
                        HashMap<String, String> params = new HashMap<>();
                        Set<String> collection = uri.getQueryParameterNames();
                        for(String str : collection){
                            params.put(str,uri.getQueryParameter(str));
                        }
                        Log.d(TAG, "shouldOverrideUrlLoading: " + params);
                        String result = "'从Android返回给JS的result'";
                        view.loadUrl("javascript:getAndroidReturn("+ result+")"); //不推荐这种去返回方法,而是通过evaluateJavascript 比较好。
                        /**
                         * 原因:如果调用的方法,上述是getAndroidReturn,需要处理内容然后弹出alert等,
                         * 如果执行太快(在webview界面加载完成前执行完调用方法(例如getAndroidReturn)),那么在onJsAlert就拦截不了窗口,就无法知道js做了什么
                         * 而evaluateJavascript需要操作完alert后才能获取到返回值,因此使用它去调用JS的方法传递返回值能拦截到Alert,会更好
                         */
//                        view.evaluateJavascript("getAndroidReturn('从Android返回给JS的result')", null);

                    }

                    return true;//一定要设置为true,表示我已经处理了该事件,否则webview会自行去处理(将跳转到修改后的URL,会导致找不到相关界面报错)
                }

                return super.shouldOverrideUrlLoading(view, request);
            }

        });

·而html的javaScript方法代码中要加上:

document.location = "js://webview?name=zxj&age=22";   
//document.location 改变uri,使其被shouldOverrideUrlLoading捕获到

示例:

jsToAndroid.html代码:

DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Carson_Hotitle>
    <script>
      function callAndroid(){
        document.location = "js://webview?name=zxj&age=22";   //document.location 改变uri,使其被shouldOverrideUrlLoading捕获到
      }
      function callAndroid1(){
        document.location = "js://webview1?name=AdvanceDev&age=20"
      }
      function getAndroidReturn(result){
         alert("result is " + result)
         setTimeout(null,3000) //设置延时,这样android在使用loadUrl调用getAndroidReturn的时候,alert才能被捕获(因为界面加载完才可以捕获alert)
      }
      /**
      function showAlert(result){
         alert("result is " + result)
      }
      */
    script>
head>
<body>
<button type="button" id="button" onclick="callAndroid()">点击调用Android代码button>
<button type="button" id="button1" onclick="callAndroid1()">点击调用Android-1代码button>
body>
html>

java调用代码:

public class MainActivity extends AppCompatActivity {
    private final String TAG = "AndroidJS";
    private WebView webView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d(TAG, "onCreate: " + Thread.currentThread().getId()  + "    " + Thread.currentThread().getName());
        Log.d(TAG,"当前主进程: " + android.os.Process.myPid() + "    ");
        webView = findViewById(R.id.webView);

        WebSettings webSettings = webView.getSettings();
        webSettings.setJavaScriptEnabled(true); // 设置与JS交互的权限
        webView.setWebViewClient(new WebViewClient() {
            @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
                // 步骤2:根据协议的参数,判断是否是所需的url
                // 一般根据scheme(协议格式)和authority(协议名)判断前两个参数
                // 假定传进来的 url="js://webview?arg1=111&arg2=222"
                Uri uri = Uri.parse(request.getUrl().toString());
                Log.d(TAG, "shouldOverrideUrlLoading: " + uri);
                // 如果authority=预先约定协议里的webview,即代表符合约定的协议
                // 所以拦截url,下面JS开始调用Android需要的方法
                if (uri.getScheme().equals("js")) {
                    if (uri.getAuthority().equals("webview")) {
                        // 步骤3:执行JS所需要的逻辑
                        // 可以在协议上带有参数并传递到Android上
                        HashMap<String, String> params = new HashMap<>();
                        Set<String> collection = uri.getQueryParameterNames();
                        for(String str : collection){
                            params.put(str,uri.getQueryParameter(str));
                        }
                        Log.d(TAG, "shouldOverrideUrlLoading: " + params);
                        String result = "'从Android返回给JS的result为 " + params + "'";
                        view.loadUrl("javascript:getAndroidReturn("+ result+")");
//                        view.evaluateJavascript("getAndroidReturn('从Android返回给JS的result')", null);
                        /**
                         * shouldOverrideUrlLoading: {name=zxj, age=22}
                         * ----------------------------------------------
                         * 获取到参数和值之后,就可以灵活调用对应的方法了
                         */
                    } else if (uri.getAuthority().equals("webview1")) {
                        // 可以在协议上带有参数并传递到Android上
                        HashMap<String, String> params = new HashMap<>();
                        Set<String> collection = uri.getQueryParameterNames();
                        for(String str : collection){
                            params.put(str,uri.getQueryParameter(str));
                        }
                        Log.d(TAG, "shouldOverrideUrlLoading: " + params);
                        String result = "'从Android返回给JS的result为 " + params + "'";
                        view.loadUrl("javascript:getAndroidReturn("+ result+")"); //不推荐这种去返回方法,而是通过evaluateJavascript 比较好。
                        /**
                         * 原因:如果调用的方法,上述是getAndroidReturn,需要处理内容然后弹出alert等,
                         * 如果执行太快(在webview界面加载完成前执行完调用方法(例如getAndroidReturn)),那么在onJsAlert就拦截不了窗口,就无法知道js做了什么
                         * 而evaluateJavascript需要操作完alert后才能获取到返回值,因此使用它去调用JS的方法传递返回值能拦截到Alert,还可以获取调用方法的返回值,会更好
                         */
//                        view.evaluateJavascript("getAndroidReturn('从Android返回给JS的result')", null);

                    }

                    return true;  //一定要设置为true,表示我已经处理了该事件,否则webview会自行去处理(将跳转到修改后的URL,会导致找不到相关界面报错)
                }

                return super.shouldOverrideUrlLoading(view, request);
            }

        });

        //拦截JS的Alert()
        webView.setWebChromeClient(new WebChromeClient(){
            @Override
            public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
                Log.d(TAG, "onJsAlert: " + url);
                AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
                builder.setTitle("Alert");
                builder.setMessage(message);
                builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        result.confirm();  //在onJsAlert返回值会true的时候需要加这个,表示将JS中的Alert也点击确定。否则JS方法不会return
                        //如何onJsAlert的返回值为false则不需要,因为false表示onJsAlert处理完逻辑后,还要交给JS中的Alert去操作,也就是说JS中的Alert也会出现
                    }
                });
                builder.setCancelable(false); //使android中,点击对话框外部或者按下返回键也取消不了对话框。  如果设置为true,则相反
                builder.create().show();

                return true;
            }

        });
        webView.loadUrl("file:///android_asset/jsToAndroid.html"); //此处是利用shouldOverrideUrlLoading 解析URL进行交互

    }

}

运行用例:

Android使用WebView与Native交互的三种方式 ( 附源码 )_第4张图片

点击第一个按钮:

Android使用WebView与Native交互的三种方式 ( 附源码 )_第5张图片

看log:

Android使用WebView与Native交互的三种方式 ( 附源码 )_第6张图片

点击第二个按钮:

Android使用WebView与Native交互的三种方式 ( 附源码 )_第7张图片

看log:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VHJHeQvf-1686711235241)(C:\Users\SW\AppData\Roaming\Typora\typora-user-images\image-20230530163153486.png)]

上述demo模拟JS调用Android的方法,并且将返回值返回给JS,然后JS将返回值以alert的形式展示出来,并被Android端拦截(跟前面的方式一样,用webchromclient的onJsAlert拦截JS中的alert方法)

PS:把返回值返回给JS,只能通过Android调用JS方法,并把返回值以参数的形式传递过去了。跟前面的方式一样,有俩种,loadUrl和evaluateJavaScript的方式。 经过测试,不建议使用loadUrl方式,如果使用loadUrl方式,调用的方法执行太快就结束了(在webview界面加载完成前执行完该调用方法(例如上述的getAndroidReturn)),那么在onJsAlert就拦截不了alert,就无法知道js做了什么。 而evaluateJavascript需要操作完alert后才能获取到返回值,因此使用它去调用JS的方法传递返回值能拦截到Alert,还可以获取调用方法的返回值,会更好

3.通过WebChromeClient的onJsAlert()、onJsConfirm()、onJsPrompt()方法回调拦截JS对话框alert()、confirm()、prompt() 消息, 然后解析对应的消息内容来调用对应的方法即可。

注意:对JS调用Android,在4.2以下存在的漏洞问题资料调研:

在Android 4.2及以下版本中,`addJavascriptInterface`存在一个重要的安全漏洞,即JavaScript可以通过调用Java对象的方法来执行任意代码。这个漏洞使得恶意网页有可能通过`WebView`对象中的Java对象执行任意的本地代码,可能导致用户数据的泄露或设备被控制。

这个漏洞的根本原因是,在Android 4.2及以下的版本中,`addJavascriptInterface`方法默认允许JavaScript访问Java对象的所有公共方法。这意味着,如果你在`WebView`中添加了一个Java对象,恶意网页可以通过调用该对象的任意公共方法来执行恶意代码,而无需用户的授权或知情。
可能导致WebView加载恶意网页的情况,例如:

1. 用户受到社会工程攻击:恶意行为者可能通过欺骗用户点击恶意链接或下载恶意应用程序等方式,让用户访问恶意网页。这可能是通过钓鱼邮件、欺诈性广告、恶意应用程序等方式实施的。
2. 代码注入:如果应用程序的WebView加载的内容依赖于用户提供的数据,例如从服务器或其他第三方来源动态加载内容,那么存在潜在的代码注入风险。恶意用户或攻击者可以在提供的数据中插入恶意代码,以此来加载恶意网页。
3. 未经授权的第三方库:如果应用程序使用了第三方库或服务来加载WebView内容,存在潜在的安全风险。如果这些第三方库存在漏洞或被攻击,可能会导致WebView加载恶意内容。

虽然开发人员可以通过合适的验证和过滤来确保WebView加载的内容是安全的,但不能排除所有的恶意行为。因此,用户也需要保持警惕,避免点击可疑的链接或下载未知来源的应用程序。
在Android 4.2及以上的版本中,`addJavascriptInterface`方法引入了一个新的注解`@JavascriptInterface`,用于标记供JavaScript调用的公共方法。只有被`@JavascriptInterface`注解标记的公共方法才会暴露给JavaScript使用,而未被注解标记的方法将不可见。

这个改变的目的是为了确保开发人员有意识地选择要暴露给JavaScript的方法,并提供了一种明确的方式来定义这些方法。通过使用`@JavascriptInterface`注解,开发人员可以明确指定哪些方法是安全的,以供JavaScript调用,从而减少了潜在的安全风险。

因此,在Android 4.2及以上的版本中,只有通过`@JavascriptInterface`注解标记的方法才能被JavaScript调用,其他未标记的方法将对JavaScript不可见。这种限制大大降低了潜在的安全漏洞,并提高了`addJavascriptInterface`的安全性。

总上所述:以addJavascriptInterface的方式来实现JS调研Android,还是要用4.2以上版本使用最好。记得使用@JavascriptInterface即可

2.使用WebView遇到的问题

·webview报错 net::ERR_UNKNOWN URLSCHEME

原因:

产生原因:webview重定向,其定义没有明确的官方解释,发生的原因是请求的链接(url)在加载完成后发生了变化 (eg.比如你的代码中设置webview加载的是网页A,打开后发现加载的是网页B); 关于 net::ERR_UNKNOWN_URL_SCHEME (如下图所示),因为webview只能识别http和https协议,遇到不属于"http(或https)/"开头的自定义协议时就无法识别,便会提示ERR_UNKNOWN_URL_SCHEME这样的错误。

Android使用WebView与Native交互的三种方式 ( 附源码 )_第8张图片

解决:

重写WebviewClient类中的 shouldOverriderUrlLoading 方法( 选取方法参数为(Webview view , String url )的那种,如下图),该方法可以对webview将要加载的url 进行处理,我们在此处对 会发生重定向的 url 和 不以 “http://”、“https//” 开头的自定义协议 进行拦截处理。该方法的返回值为boolean 类型,表示是否阻止webview继续加载url,默认值为 false。当返回false,表示不进行阻止,webview认为当前的url需要进行处理,会继续加载;返回 true,表示阻止webview继续加载url,等待我们进行处理。

             //加载的url是http/https协议地址
   if (url.startsWith("http://") || url.startsWith("https://")) { 
           view.loadUrl(url);
           return false; //返回false表示此url默认由系统处理,url未加载完成,会继续往下走
   } 

·页面百度点开图片无法加载图片问题:

产生原因不详,但分析可能是因为百度是个搜索引擎,搜到的图片都是来自各大网址中的内容。

解决: webView.getSettings().setDomStorageEnabled(true); 开启WebView的DOM存储功能即可。

注意:还需要 webView.getSettings().setBlockNetworkImage(false); 不阻塞加载网络图片

总结:webview遇到的问题大多都可以通过分析所加载的html页面的特性来配置对应的设置即可。

你可能感兴趣的:(Android基础,android,交互,webview,JS与Native的交互,JS与android原生的交互)