WebView与JS交互

2019-05-13  本文已影响0人  沈溺_16e5

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

// 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() 回调之后才能调用,否则不会调用。
// 只需要将第一种方法的loadUrl()换成下面该方法即可
mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String value) {
            //此处为 js 返回的结果
        }
    });
}
0.5252186051721934.png

使用建议
两种方法混合使用,即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 代码

// 继承自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进行注解从而避免漏洞攻击)

<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);
}
<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);
     }
 }
);
0.3452358047020512.png

4、总结

0.894173224369111.png

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文件
https://www.jianshu.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;
        }
    }
}

上一篇下一篇

猜你喜欢

热点阅读