老早之前就想总结下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
获取去这里下载我的下载