老早之前就想总结下Webview相关的知识点了,因为互联网大潮中,很多APP都会使用到Webview,像那些不计其数的电商APP,无一例外的使用Webview;或者一些非电商APP中的像广告页面,注册协议页面都会用到;最后因为一些事情拖到现在才做,感觉事情真不能拖,越往后推越做不了,罪过罪过。
怎么总结Webview呢
1.简单介绍
2.WebView/WebViewClient/WebChromeClient api介绍
3.简单使用
4.JS调用Android本地
5.Android调用JS方法
6.缓存处理及性能优化
7.webview使用注意点
webview系列文章
Android之WebView/WebViewClient/WebChromeClient简介 API详述 【一】
Android之WebView/WebViewClient/WebChromeClient 使用样例 【二】
Android之WebView Android调用JS方法 JS调用Android方法 【三】
Android之WebView 使用注意点 JS注入漏洞问题 内存优化【五】
4.JS调用Android
方法主要有三种
1.通过Webview的addJavaScriptInterface方法注入java对象
2.通过WebViewClient的shouldOverrideUrlLoading方法拦截url,这个是用的最普遍的
3.通过WebChromeClient 的onJsAlert、onJsConfirm、onJsPrompt 提示接口进行相关操作
我们先看第一种 addJavaScriptInterface 方法
* 先编写Android端
1.布局编写
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/white"> <WebView android:id="@+id/webview" android:layout_width="match_parent" android:layout_height="280dp"/> <TextView android:id="@+id/result" android:layout_width="match_parent" android:layout_height="40dp" android:layout_marginTop="20dp" android:textSize="18sp" android:textColor="@color/colorAccent"/> </LinearLayout>
布局很简单,就是一个webview和一个接受js返回值的textview
2.既然是注入java对象,那就编写这个对象
public class JSInterface { private String TAG = "JSInterface"; // 定义JS需要调用的方法 // 被JS调用的方法必须加入@JavascriptInterface注解 @JavascriptInterface public void setValue(String value) { Log.e("JSInterface","setValue value="+value); } }
3.注入java对象,也就是添加js接口
private void initWebview() { WebSettings webSettings = mWebView.getSettings(); // 设置与Js交互的权限 webSettings.setJavaScriptEnabled(true); // 通过addJavascriptInterface()将Java对象映射到JS对象 //参数1:java对象 //参数2:Java对象在js里的对象名,也就是这个对象在js里叫啥 mWebView.addJavascriptInterface(new JSInterface(), "mango"); mWebView.loadUrl("file:///android_asset/javascript_android_2.html"); }
到这里客户端就写完了。
* 接下来我们编写HTML部分
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>WebView</title> <style type="text/css"> body{ background-color: rgb(210, 154, 28) } .btn{ line-height: 40px; width: 80px; margin: 10px; background: rgb(238, 244, 233) } </style> </head> <body> <h1 >JS调用Android</h1> <div > <span >请输入要传递给Android的值</span> <input type="text" id="input"/> </div> <div id="btn" class="btn" > <span >click</span> </div> <!-- javascript逻辑就是点击button将输入框的值返回给android客户端 --> <script type="text/javascript"> var btnEle = document.getElementById('btn'); var inputEle = document.getElementById('input'); // 给按钮添加一个click事件监听 btnEle.addEventListener("click",function () { var value = inputEle.value; / / 在页面show一下输入的值 // alert(value); // 先判断android客户端传入的java对象存不存在 if(window.mango){ mango.setValue(value); }else{ alert("mango not found"); } }); </script> </body>
具体逻辑都注释了,我们先把这个html在浏览器运行看看,完全符合最后那个判断。
* 然后我们运行客户端
我这用的是模拟器,在输入框输入值后,看看在JsInterface类的setValue方法打印的log
06-11 21:58:53.863 3770-3878/? D/JSInterface: setValue value=2552
这里可以得出:
1.android中原生方法成功被JS调用了
2.这个被调用的方法所在线程并不是在主线程,看上面这个日志 3770-3878 ,3770表示主线程id,3878表示子线程id;这就表示它是一个非UI线程,不能在这里更新UI.要更新UI需要通过接口或者Handler,如下
@JavascriptInterface public void setValue(String value) { Log.d("JSInterface","setValue value="+value); Message msg = mHandler.obtainMessage(); msg.what = UPDATE_TEXT; msg.obj = value; mHandler.sendMessage(msg); }
Handler mHandler = new Handler(){ @Override public void handleMessage(Message msg) { super.handleMessage(msg); int tag = msg.what ; if (UPDATE_TEXT == tag) { String result = (String) msg.obj; mResult.setText(result); } } };
这样就实现了android 和 JS 交互,但是实现功能的同时也带了安全问题,通过注入的 Java 类作为桥梁,JS 就可以利用这个漏洞,具体什么我们放在最后总结。
看第二种方法 shouldOverrideUrlLoading
工作逻辑是
1.通过shouldOverrideUrlLoading方法拦截URL,
2.解析url
3.根据约定好的规则去匹配解析后的url,如果规则相同,就进行操作
先写好html文件
<!DOCTYPE html> <html lang="en" dir="ltr"> <head> <meta charset="utf-8"> <title>webview</title> <style media="screen"> body{ background-color: rgb(224, 112, 57); } </style> </head> <body> <h1>Js调用Android二</h1> <button type="button" id="btn" onclick="callAndroid()">点击调用Android方法</button> <script type="text/javascript"> function callAndroid(){ document.location = "js://webview?id=1&value=2"; } </script> </body> </html>
再写客户端
WebSettings webSettings = mWebView.getSettings(); // 设置与Js交互的权限 webSettings.setJavaScriptEnabled(true); mWebView.loadUrl("file:///android_asset/jscallandroid.html"); // 复写WebViewClient类的shouldOverrideUrlLoading方法 // 设置允许JS弹窗 webSettings.setJavaScriptCanOpenWindowsAutomatically(true); mWebView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { Log.e(TAG,"shouldOverrideUrlLoading url="+url); // 根据协议的参数,判断是否是所需要的url // 一般根据scheme(协议格式) & authority(协议名)判断(前两个参数) //假定传入进来的 url = "js://webview?id=1&value=2"(同时也是约定好的需要拦截的) Uri uri = Uri.parse(url); // 如果url的协议 = 预先约定的 js 协议 // 就解析往下解析参数 if ( uri.getScheme().equals("js")) { // 如果 authority = 预先约定协议里的 webview,即代表都符合约定的协议 // 所以拦截url,下面JS开始调用Android需要的方法 if (uri.getAuthority().equals("webview")) { // 执行JS所需要调用的逻辑 Toast.makeText(mContext,"js调用了Android的方法",Toast.LENGTH_LONG).show(); // 可以在协议上带有参数并传递到Android上 HashMap<String, String> params = new HashMap<>(); Set<String> collection = uri.getQueryParameterNames(); //如果js需要返回值,就从这里再次执行js方法把结果返回回去 // mWebView.loadUrl("javascript:returnResult(" + "result" + ")"); } return true; } return super.shouldOverrideUrlLoading(view, url); } } );
看看打印的日志
06-11 22:57:29.033 6256-6256/? E/AndroidCallJSFrag: shouldOverrideUrlLoading url=js://webview?id=1&value=2
这个url与js里的一样,这就是双方约定好,然后一个一个解析url结构,然后就可以进行相关操作;并且看到6256-6256,说明这是在UI线程回调。
这种方法不会出现第一种方法出现的漏洞。
再看第三种方法 通过WebChromeClient 的onJsAlert、onJsConfirm、onJsPrompt
当js里调用alert(),confirm(),prompt()方法的时候,我们可以通过WebChromClient类的onJsAlert、onJsConfirm、onJsPrompt三个方法去拦截消息,然后解析。
这三个js方法有点不同,
alert():弹出一个警告框,没有返回值
confirm():弹出确认框,有两个状态返回值(ok/no)
prompt():弹出输入框,可设置任意返回值,这就是最灵活的,也是我们最常用的
我们先写html
<!DOCTYPE html> <html lang="en" dir="ltr"> <head> <meta charset="utf-8"> <title>WebView</title> <style media="screen"> body{ background-color: rgb(224, 112, 57); } </style> </head> <body> <h1>Js调用Android三</h1> <button type="button" id="btn" onclick="clickPrompt()">clickPrompt调用android</button> <script type="text/javascript"> function clickPrompt(){ // 通过prompt调用android,获取返回值result var result = prompt("js://webview?id=1&value=2"); // 将获取的android返回值通过alert弹出框显示出来 alert(result); } </script> </body> </html>
再写android客户端
WebSettings webSettings = mWebView.getSettings(); // 设置与Js交互的权限 webSettings.setJavaScriptEnabled(true); mWebView.loadUrl("file:///android_asset/jscallandroid-2.html"); // 复写WebViewClient类的shouldOverrideUrlLoading方法 // 设置允许JS弹窗 webSettings.setJavaScriptCanOpenWindowsAutomatically(true); mWebView.setWebChromeClient(new WebChromeClient(){ @Override public boolean onJsAlert(WebView view, String url, String message, JsResult result) { Log.d(TAG,"onJsAlert"); return super.onJsAlert(view, url, message, result); } @Override public boolean onJsConfirm(WebView view, String url, String message, JsResult result) { return super.onJsConfirm(view, url, message, result); } /** * 拦截js的prompt弹出框 * @param view * @param url * @param message * js里promt()方法的参数内容,不是url * @param defaultValue * @param result * 代表输入框的值 * @return */ @Override public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { Log.d(TAG,"onJsPrompt message="+message); return super.onJsPrompt(view, url, message, defaultValue, result); } });
我们如果在onJsPrompt不做处理,让webview自己处理看看什么情况
跟js代码里的逻辑是一样的,先通过prompt弹出个输入框,而且onJsPrompt方法里的日志打印了出来
06-12 22:46:51.855 5010-5010/? D/JsCallAndroid: onJsPrompt message=js://webview?id=1&value=2
看到线程id说明这个回调是在UI线程
当输入之后点击确定
也是跟js里逻辑一样,把输入内容弹出框显示
然后我们在onJsPrompt方法里对Message内容进行解析过滤,如果是我们自己想要的,进行相应处理
/** * 拦截js的prompt弹出框 * @param view * @param url * @param message * js里promt()方法的参数内容,不是url * @param defaultValue * @param result * 代表输入框的返回值 * @return */ @Override public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { Log.d(TAG,"onJsPrompt url = " + url +",message="+message ); // 根据协议的参数,判断是否是所需要的url // 一般根据scheme(协议格式) & authority(协议名)判断(前两个参数) //假定传入进来的 url = "js://webview?id=1&value=2"(同时也是约定好的需要拦截的) Uri uri = Uri.parse(message); // 如果url的协议 = 预先约定的 js 协议 // 就解析往下解析参数 if ( uri.getScheme().equals("js")) { // 如果 authority = 预先约定协议里的 webview,即代表都符合约定的协议 // 所以拦截url,下面JS开始调用Android需要的方法 if (uri.getAuthority().equals("webview")) { // // 执行JS所需要调用的逻辑 Log.d(TAG,"js调用了Android的方法"); // 可以在协议上带有参数并传递到Android上 HashMap<String, String> params = new HashMap<>(); Set<String> collection = uri.getQueryParameterNames(); Iterator<String> iterator = collection.iterator(); while (iterator.hasNext()) { String key = iterator.next(); String value = uri.getQueryParameter(key); Log.d(TAG,"key="+key+",value="+value); params.put(key,value); } //参数result:代表消息框的返回值(输入值) result.confirm("js调用了Android的方法成功啦"); } return true; } return super.onJsPrompt(view, url, message, defaultValue, result); }
我们点击js里button后回回调android里的onJsPrompt方法,这里我们可以获取js传给我们的数据
看日志
06-12 22:56:15.525 5379-5379/? D/AndroidCallJSFrag: key=id,value=1
06-12 22:56:15.525 5379-5379/? D/AndroidCallJSFrag: key=value,value=2
再与js代码中的prompt方法比较
var result = prompt("js://webview?id=1&value=2");
可以看到获取的值是一样的。
至此JS调用Android的三种方法总结完毕。
接下来讲述第五步
5.Android调用JS
android调用js主要有两种方法
1.使用webview的loadurl
2.使用webview的evaluateJavascript
我们先看第一种方法
先把html代码编写完成
<!DOCTYPE html> <html lang="en" dir="ltr"> <head> <meta charset="utf-8"> <title>WebView</title> <style > body{ background-color: rgb(230, 105, 34); } </style> </head> <body> <h1 > android调用js</h1> <script type="text/javascript"> //提供一个方法供android客户端调用 function callJS(){ //如果android调用js成功就用弹出框提示 alert("android成功调用了js"); //如果android调用js成功就返回值 return "success"; } </script> </body> </html>
然后在客户端编写
private void initWebview() { WebSettings set = mWebView.getSettings(); // 设置与Js交互的权限 set.setJavaScriptEnabled(true); // 设置允许JS弹窗 set.setJavaScriptCanOpenWindowsAutomatically(true); // 由于设置了弹窗检验调用结果,所以需要支持js对话框 // 通过设置WebChromeClient对象处理JavaScript的对话框 //设置响应js 的Alert()函数 mWebView.setWebChromeClient(new WebChromeClient()); mWebView.setWebViewClient(new WebViewClient(){ @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); Log.e(TAG,"onPageFinished url="+url); //JS代码调用一定要在 onPageFinished() 回调之后才能调用,否则不会调用。 mWebView.loadUrl("javascript:callJs()"); } }); mWebView.loadUrl("file:///android_asset/androidcalljs.html"); }
我们看第二种方法:
这个方法比第一种方法使用更高效,可以获取js返回值,要4.4以后才可以用
html不用变,只需在调用的客户端修改下
private void initWebview() { WebSettings set = mWebView.getSettings(); // 设置与Js交互的权限 set.setJavaScriptEnabled(true); mWebView.loadUrl("file:///android_asset/androidcalljs.html"); // 设置允许JS弹窗 set.setJavaScriptCanOpenWindowsAutomatically(true); // 由于设置了弹窗检验调用结果,所以需要支持js对话框 // 通过设置WebChromeClient对象处理JavaScript的对话框 //设置响应js 的Alert()函数 mWebView.setWebChromeClient(new WebChromeClient()); } @OnClick(R.id.tv_callJs) public void callJS(){ final int version = Build.VERSION.SDK_INT; if (version < 18) { mWebView.loadUrl("javascript:callJS()");//低版本使用这个方法 } else { mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() { @Override public void onReceiveValue(String value) { //此处为 js 返回的结果 Log.e(TAG,"value="+value); } }); } }
看看打印的日志
06-13 20:56:34.478 9464-9464/? E/AndroidCallJSFrag: value="success"
获取js的返回值成功,并且这个回调也是在UI线程
到这里本节就讲完了
相关代码已托管到GitHub
获取去这里下载我的下载