前言
本篇是webview的下篇,主要对webview和网页的js交互处理
本篇的gif效果先贴下
Android对网页Javascript的调用
想让Android通过webview调用网页上面的Javascript其实很简单
只要网页上面的Javascript方法名字就可以直接调用了
在Android Api 19之前
webview调用js的方法只有直接loadUrl("javascript:js方法名字")
在此之后由于原来低版本调用js的安全问题
谷歌对js进行了修改并且加入了一个新的方法evaluateJavascript(String script, ValueCallback<String> resultCallback)
对比如下
调用方法 | 优点 | 缺点 | 使用 |
---|---|---|---|
loadUrl | 方法简便 | 效率低,取js回调的值麻烦 | api19以下 |
evaluateJavascript | 效率高,可以很好的获取返回结果 | 无法兼容19以下的版本 | api19以上 |
上面gif中三个按钮的调用和js方法如下
function callJS(){
alert("Android调用了JS的callJS方法");
}
function callJS2(msg){
var r = confirm(msg);
if(r == true){
alert("是");
}else{
alert("否");
}
}
function callJS3(){
var r = prompt("callJS3方法,随便输入");
alert(r);
return r;
}
//按钮1
webView.loadUrl("javascript:callJS()");
//按钮2
webView.loadUrl("javascript:callJS2('Android调用了JS的callJS2方法')");
//按钮3
webView.evaluateJavascript("javascript:callJS3()", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
//此处为 js 返回的结果
Log.e("-s-", "=>"+value);
}
});
打印的log如下
正常使用的时候建议加入判断,低于19的系统就使用原来的loadUrl方法调用,之后都建议都换成evaluateJavascript
以上的方法相对应了,无参数调用、有参数调用以及获取返回结果
网页Javascript对Android的调用
从上面的gif我们能看到 点击网页上面的按钮可以直接调用app的Toast功能,这就是网页对安卓的调用,我们可以通过设置让网页点击之后调用手机的相机等一系列操作等
下面说下正常的使用
第一步
我们需要设置webview的WebSettings让webview支持js
即wv.getSettings().setJavaScriptEnabled(true);
设置之后我们能看到一个XSS警告,这个后面讲第二步
为webview添加js的触发方法和设置调用的方法名字
wv.addJavascriptInterface(new javascriptInterface(),"cwebview");
这边我定义了一个class和命名与js上面的方法调用是一样的
js方法
function callAndroid(){
cwebview.hello("js调用了android中的hello方法");
}
javascriptInterface.class
private class javascriptInterface{
@JavascriptInterface
public void hello(String msg){
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
}
}
这样我们就能在网页上面调用Android了,记住必须加入标示@JavascriptInterface
不然无法被webview识别为映射的js方法。
继续我们上面讲的警告,对于这种情况,如果想要调用js方法又不需要开启js方法也是可以的,这边有两种方法,都需要和服务器进行约定设置,然后就可以实现不开启js就直接使用。
- 第一种shouldOverrideUrlLoading
我们在重写webview的WebViewClient的时候,里面的shouldOverrideUrlLoading
方法是用来让webview可以继续加载,而不会跳转到系统的浏览器。这样我们就可以和服务器约定,设置一个js的调用格式,然后进行js的操作。
这边我们约定的url是 js://webview?msg=调用callAndroid2方法&msg2=test2
js方法
function callAndroid2(){
document.location = "js://webview?msg=调用callAndroid2方法&msg2=test2";
}
然后我们改写一下shouldOverrideUrlLoading方法
wv.setWebViewClient(new WebViewClient(){
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
//目标url为 js://webview?msg=调用callAndroid2方法&msg2=test2
//判断是否有js标示
if (url.contains("js://")) {
//将网址转换为Uri
Uri uri = Uri.parse(url);
//判断Uri的开头 即 :// 前的部分 js
if (uri.getScheme().equals("js")) {
//判断Uri的权 即 :// 到 ? 之间的部分 webview
if (uri.getAuthority().equals("webview")) {
//遍历Uri的参数 即 ? 之后的全部参数
Map<String,String> parms = new HashMap<>();
Set<String> names = uri.getQueryParameterNames();
StringBuilder sb = new StringBuilder();
for (String name : names) {
parms.put(name,uri.getQueryParameter(name));
sb.append(name+"="+uri.getQueryParameter(name)+"\n");
}
Toast.makeText(TestActivity.this, sb.toString(), Toast.LENGTH_SHORT).show();
}
}
return true;
}
view.loadUrl(url);
return true;
}
});
这样就实现了不开启js也不需要设置方法就直接可以让js和Android进行交互。
- 第二种onJsPrompt
我们上篇也讲过对话框,先来看下三种对话框的对比
方法 | 作用 | 返回值 | 备注 |
---|---|---|---|
alert | 弹出一个只有确认的对话框 | 无返回值 | 在文本后输入\n可以换行 |
confirm | 弹出一个确认和取消的对话框 | 两个返回值 | 返回值为布尔类型 |
prompt | 弹出一个可以输入的对话框可以确认和取消 | 可以任意设置返回值 | 点击确认会返回输入框内输入的值,取消则返回null |
所以我们在选择用对话框进行js调用Android的时候最好选择使用onJsPrompt来实现
js方法
function callAndroid3(){
prompt("js://webview?msg111=调用callAndroid3方法&msg222=test3");
}
然后我们改写WebChromeClient中的onJsPrompt
方法,由于和上面的第一种方法差不多就不详写备注了,只有一个取消对话框的需要写下!
wv.setWebChromeClient(new WebChromeClient(){
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
//目标url为 js://webview?msg111=调用callAndroid3方法&msg222=test3
if (message.contains("js://")){
Uri uri = Uri.parse(message);
if (uri.getScheme().equals("js")) {
if (uri.getAuthority().equals("webview")) {
Map<String,String> params = new HashMap<>();
Set<String> names = uri.getQueryParameterNames();
StringBuilder sb = new StringBuilder();
for (String s : names) {
params.put(s,uri.getQueryParameter(s));
sb.append(s+"=>"+uri.getQueryParameter(s)+"\n");
}
Toast.makeText(TestActivity.this, sb.toString(), Toast.LENGTH_SHORT).show();
//关闭对话框
result.cancel();
}
return true;
}
}
return super.onJsPrompt(view, url, message, defaultValue, result);
}
});
这样就是另一种实现js交互的方案。
三种JS调用Android方案的比对
调用方法 | 优点 | 缺点 | 使用场景 |
---|---|---|---|
使用addJavascriptInterface | 官方方法简洁方便 | Android4.2以下有漏洞问题 | Android4.2以上版本使用方便容易 |
使用shouldOverrideUrlLoading | 不存在漏洞问题 | 使用复杂,需要和服务器设置相应的条件标示 | 只是直接调用一次Android不需要Android的返回值的情况 |
使用onJsPrompt | 不存在漏洞问题 | 使用复杂,需要和服务器设置相应的条件标示 | 如果有漏洞问题,又需要返回值等经常调用Android的情况 |
关于本篇的整个测试html代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS_TEST</title>
# JS代码
<script>
function callJS(){
alert("Android调用了JS的callJS方法");
}
function callJS2(msg){
var r = confirm(msg);
if(r == true){
alert("是");
}else{
alert("否");
}
}
function callJS3(){
var r = prompt("callJS3方法,随便输入");
alert(r);
return r;
}
function callAndroid(){
cwebview.hello("js调用了android中的hello方法");
}
function callAndroid2(){
document.location = "js://webview?msg=调用callAndroid2方法&msg2=test2";
}
function callAndroid3(){
prompt("js://webview?msg111=调用callAndroid3方法&msg222=test3");
}
</script>
</head>
<body>
<br />
//点击按钮则调用callAndroid函数
<br />
<br />
<button type="button" id="button1" onclick="callAndroid()">网页调用Android1</button>
<br />
<br />
<button type="button" id="button2" onclick="callAndroid2()">网页调用Android2</button>
<br />
<br />
<button type="button" id="button3" onclick="callAndroid3()">网页调用Android3</button>
</body>
</html>
WebView优化建议
当WebView的使用频率变得频繁的时候,对于其各方面的优化就变得逐渐重要了起来。可以知道的是,我们每加载一个 H5页面,都会有很多的请求。除了HTML主URL自身的请求外,HTML外部引用的 JS、CSS、字体文件、图片都是一个个独立的HTTP 请求,虽然请求是并发的,但当网页整体数量达到一定程度的时候,再加上浏览器解析、渲染的时间,Web整体的加载时间变得很长。同时请求文件越多,消耗的流量也会越多。那么对于加载的优化就变得非常重要,这方面的经验我也没有什么别的,大概三个方面:
第一个,就是资源本地化的问题
首先可以明确的是,以目前的网络条件,通过网络去服务器获取资源的速度是远远比不上从本地读取的。谈论各种优化策略其实恰恰忽略了“需要加载”才是阻挡速度提升的最大绊脚石。所以我们的思路一,就是将一些较重的资源比如js、css、图片甚至HTML本身进行本地化处理,在每次加载到这些资源的时候,从本地读取进行加载,可以简单记忆为“存·取·更”。
具体实现思路为:
“存”——将上述重量级资源打包进apk文件,每次加载相应文件时时从本地取即可。也可不打包,在第一次加载时以及接下来的若干间隔时间里动态下载存储,将所有的资源文件都存在Android的asset目录下;
“取”——重写WebViewClient的WebResourceResponse#shouldInterceptRequest方法,通过一定的判别方法(例如正则表达式)拦截相应的请求,从本地读取相应资源并返回;
“更”——建立起Cache Control机制,定期或使用API通知的形式控制本地资源的更新,保证本地资源是最新和可用的。
这里附上一篇博客链接,非常棒可供参考:caching-web-resources-in-the-android-device
第二个,就是缓存的问题
倘若你不采用或不完全采用第一条资源本地化的思路,那么你的WebView缓存是必须要开启的(虽然这一思路和第一条有重合的地方)。
WebSettings settings = webView.getSettings();
settings.setAppCacheEnabled(true);
settings.setDatabaseEnabled(true);
settings.setDomStorageEnabled(true);//开启DOM缓存
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
在网络正常时,采用默认缓存策略,在缓存可获取并且没有过期的情况下加载缓存,否则通过网络获取资源以减少页面的网络请求次数。
这里值得提起的是,我们经常在app里用WebView展示页面时,并不想让用户觉得他是在访问一个网页。因为倘若我们的app里网页非常多,而我们给用户的感觉又都像在访问网页的话,我们的app便失去了意义。(我的意思是为什么用户不直接使用浏览器呢?)
所以这时,离线缓存的问题就值得我们注意。我们需要让用户在没有网的时候,依然能够操作我们的app,而不是面对一个和浏览器里的网络错误一样的页面,哪怕他能进行的操作十分有限。
这里我的思路是,在开启缓存的前提下,WebView在加载页面时检测网络变化,倘若在加载页面时用户的网络突然断掉,我们应当更改WebView的缓存策略。
ConnectivityManager connectivityManager = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
if(networkInfo.isAvailable()) {
settings.setCacheMode(WebSettings.LOAD_DEFAULT);//网络正常时使用默认缓存策略
} else {
settings.setCacheMode(WebSettings.LOAD_CACHE_ONLY);//网络不可用时只使用缓存
}
既然有缓存,就要有缓存控制,与一相似的是我们也要建立缓存控制机制,定期或接受服务器通知来进行缓存的清空或更新。
第三个,就是延迟加载和执行js
在WebView中,onPageFinished()的回调意味着页面加载的完成。但该方法会在JavScript脚本执行完成后才会触发,倘若我们要加载的页面使用了JQuery,会在处理完DOM对象,执行完$(document).ready(function() {})后才会渲染并显示页面。这是不可接受的,所以我们需要对Js进行延迟加载,当然这部分是Web前端的工作。
如果说还有什么
那就是JsBridge一律不得滥用,这个对页面加载的完成速度是有很大影响的,倘若一个页面很多操作都通过JSbridge来控制,再怎么优化也无济于事(因为毕竟有那么多操作要实际执行)。同时要注意的是,不管你是否对资源进行缓存,都请将资源在服务器端进行压缩。因为无论是资源的获取和更新,都是要从服务器获取的,所以对于资源文件的压缩其实是最直接也最应该做的事情之一,但是一般服务器端都会做好,所以主要就是上面这三件事。
以上全部来自开车指南