Android WebView 防坑总结

02 后台无法释放js导致发热耗电

  • 在有些手机你如果webview加载的html里,有一些js一直在执行比如动画之类的东西,如果此刻webview 挂在了后台这些资源是不会被释放用户也无法感知。
  • 导致一直占有cpu 耗电特别快,所以如果遇到这种情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在onStop里面设置setJavaScriptEnabled(false);
在onResume里面设置setJavaScriptEnabled(true)。
@Override
protected void onResume() {
super.onResume();
if (mWebView != null) {
mWebView.getSettings().setJavaScriptEnabled(true);
}

}
@Override
protected void onStop() {
super.onStop();
if (mWebView != null) {
mWebView.getSettings().setJavaScriptEnabled(false);
}
}

03.301/302业务场景及白屏问题

04 301/302重定向问题

  • 什么是重定向
  • 专业叙述
    • 302重定向又称之为302代表暂时性转移
  • 网络解释
    • 重定向是网页制作中的一个知识,几个例子跟你说明,假设你现在所处的位置是一个论坛的登录页面,你填写了帐号,密码,点击登陆,如果你的帐号密码正确,就自动跳转到论坛的首页,不正确就返回登录页;这里的自动跳转,就是重定向的意思。或者可以说,重定向就是,在网页上设置一个约束条件,条件满足,就自动转入到其它网页、网址 。比如,你输入一个网站链接,一般可以直接进入网站,如果出现错误,则又跳转到另外一个网页。
  • 举个例子
    • 叙述下这种问题的情况,就是WebView首先加载A链接,然后在WebView上点击一个B链接进行加载,B链接会自动跳转到C链接,这个时候调用WebView的goback方法,会返回到加载B链接,但是B链接又会跳转到C链接,从而导致没法返回到A链接界面(当然也有朋友说快速的按两次返回键-也就是连续触发了两次goback可以返回到A链接,但并不是所有用户都懂这个,而且操作上也很恶心。),这就是重定向问题。

05 301/302回退栈问题

  • WebView能否知道某个url是不是301/302呢?当然知道,WebView能够拿到url的请求信息和响应信息,根据header里的code很轻松就可以实现,事实正是如此,交给WebView来处理重定向(return false),这时候按返回键,是可以正常地回到重定向之前的那个页面的。(PS:从上面的章节可知,WebView在5.0以后是一个独立的apk,可以单独升级,新版本的WebView实现肯定处理了重定向问题)
  • 但是,业务对url拦截有需求,肯定不能把所有的情况都交给系统WebView处理。为了解决url拦截问题,本文引入了另一种思想——通过用户的touch事件来判断重定向。
  • 代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
/**
* ================================================
* 作 者:杨充
* 版 本:1.0
* 创建日期:2017/3/28
* 描 述:实现WebView的滑动监听和优雅处理回退栈问题
* 修订历史:
* ================================================
*/
public class ScrollWebView extends WebView {

private boolean mTouchByUser;
public ScrollWebView(Context context) {
super(context);
}

public ScrollWebView(Context context, AttributeSet attrs) {
super(context, attrs);
}

public ScrollWebView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

public OnScrollWebListener getOnScrollWebListener() {
return onScrollWebListener;
}

private OnScrollWebListener onScrollWebListener;
public void setScrollWebListener(OnScrollWebListener onScrollWebListener) {
this.onScrollWebListener = onScrollWebListener;
}

public interface OnScrollWebListener {
void onScroll(int dx, int dy);
}

@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (onScrollWebListener != null) {
onScrollWebListener.onScroll(l - oldl, t - oldt);
}
}

@Override
public final void loadUrl(String url, Map<String, String> additionalHttpHeaders) {
super.loadUrl(url, additionalHttpHeaders);
resetAllStateInternal(url);
}

@Override
public void loadUrl(String url) {
super.loadUrl(url);
resetAllStateInternal(url);
}

@Override
public final void postUrl(String url, byte[] postData) {
super.postUrl(url, postData);
resetAllStateInternal(url);
}

@Override
public final void loadData(String data, String mimeType, String encoding) {
super.loadData(data, mimeType, encoding);
resetAllStateInternal(getUrl());
}

@Override
public final void loadDataWithBaseURL(String baseUrl, String data,
String mimeType, String encoding, String historyUrl) {
super.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl);
resetAllStateInternal(getUrl());
}

@Override
public void reload() {
super.reload();
resetAllStateInternal(getUrl());
}

public boolean isTouchByUser() {
return mTouchByUser;
}

private void resetAllStateInternal(String url) {
if (!TextUtils.isEmpty(url) && url.startsWith("javascript:")) {
return;
}
resetAllState();
}

// 加载url时重置touch状态
protected void resetAllState() {
mTouchByUser = false;
}

@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//用户按下到下一个链接加载之前,置为true
mTouchByUser = true;
break;
}
return super.onTouchEvent(event);
}

@Override
public void setWebViewClient(final WebViewClient client) {
super.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
boolean handleByChild = null != client && client.shouldOverrideUrlLoading(view, url);
if (handleByChild) {
// 开放client接口给上层业务调用,如果返回true,表示业务已处理。
return true;
} else if (!isTouchByUser()) {
// 如果业务没有处理,并且在加载过程中用户没有再次触摸屏幕,认为是301/302事件,直接交由系统处理。
return super.shouldOverrideUrlLoading(view, url);
} else {
//否则,属于二次加载某个链接的情况,为了解决拼接参数丢失问题,重新调用loadUrl方法添加固有参数。
loadUrl(url);
return true;
}
}
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
boolean handleByChild = null != client && client.shouldOverrideUrlLoading(view, request);
if (handleByChild) {
return true;
} else if (!isTouchByUser()) {
return super.shouldOverrideUrlLoading(view, request);
} else {
loadUrl(request.getUrl().toString());
return true;
}
}
});
}
}

06.WebSettings.setJavaScriptEnabled安全问题

6.1 问题描述

  • 跟js交互的话,会调用:WebSettings.setJavaScriptEnabled(true)
  • 在Android 4.3版本调用WebSettings.setJavaScriptEnabled()方法时会调用一下reload方法,同时会回调多次WebChromeClient.onJsPrompt()。如果有业务逻辑依赖于这两个方法,就需要注意判断回调多次是否会带来影响了。
  • 如果启用了JavaScript,务必做好安全措施,防止远程执行漏洞。

6.2 解决该问题办法

1
2
3
4
5
6
7
8
9
10
11
12
@TargetApi(11)
private static final void removeJavascriptInterfaces(WebView webView) {
try {
if (Build.VERSION.SDK_INT >= 11 && Build.VERSION.SDK_INT < 17) {
webView.removeJavascriptInterface("searchBoxJavaBridge_");
webView.removeJavascriptInterface("accessibility");
webView.removeJavascriptInterface("accessibilityTraversal");
}
} catch (Throwable tr) {
tr.printStackTrace();
}
}

10 如何处理加载错误(Http、SSL、Resource)?

  • 对于WebView加载一个网页过程中所产生的错误回调,大致有三种
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
* 只有在主页面加载出现错误时,才会回调这个方法。这正是展示加载错误页面最合适的方法。
* 然而,如果不管三七二十一直接展示错误页面的话,那很有可能会误判,给用户造成经常加载页面失败的错觉。
* 由于不同的WebView实现可能不一样,所以我们首先需要排除几种误判的例子:
* 1.加载失败的url跟WebView里的url不是同一个url,排除;
* 2.errorCode=-1,表明是ERROR_UNKNOWN的错误,为了保证不误判,排除
* 3failingUrl=null&errorCode=-12,由于错误的url是空而不是ERROR_BAD_URL,排除
* @param webView webView
* @param errorCode errorCode
* @param description description
* @param failingUrl failingUrl
*/
@Override
public void onReceivedError(WebView webView, int errorCode,
String description, String failingUrl) {
super.onReceivedError(webView, errorCode, description, failingUrl);
// -12 == EventHandle.ERROR_BAD_URL, a hide return code inside android.net.http package
if ((failingUrl != null && !failingUrl.equals(webView.getUrl())
&& !failingUrl.equals(webView.getOriginalUrl())) /* not subresource error*/
|| (failingUrl == null && errorCode != -12) /*not bad url*/
|| errorCode == -1) { //当 errorCode = -1 且错误信息为 net::ERR_CACHE_MISS
return;
}
if (!TextUtils.isEmpty(failingUrl)) {
if (failingUrl.equals(webView.getUrl())) {
//做自己的错误操作,比如自定义错误页面
}
}
}

/**
* 只有在主页面加载出现错误时,才会回调这个方法。这正是展示加载错误页面最合适的方法。
* 然而,如果不管三七二十一直接展示错误页面的话,那很有可能会误判,给用户造成经常加载页面失败的错觉。
* 由于不同的WebView实现可能不一样,所以我们首先需要排除几种误判的例子:
* 1.加载失败的url跟WebView里的url不是同一个url,排除;
* 2.errorCode=-1,表明是ERROR_UNKNOWN的错误,为了保证不误判,排除
* 3failingUrl=null&errorCode=-12,由于错误的url是空而不是ERROR_BAD_URL,排除
* @param webView webView
* @param webResourceRequest webResourceRequest
* @param webResourceError webResourceError
*/
@Override
public void onReceivedError(WebView webView, WebResourceRequest webResourceRequest,
WebResourceError webResourceError) {
super.onReceivedError(webView, webResourceRequest, webResourceError);
}

/**
* 任何HTTP请求产生的错误都会回调这个方法,包括主页面的html文档请求,iframe、图片等资源请求。
* 在这个回调中,由于混杂了很多请求,不适合用来展示加载错误的页面,而适合做监控报警。
* 当某个URL,或者某个资源收到大量报警时,说明页面或资源可能存在问题,这时候可以让相关运营及时响应修改。
* @param webView webView
* @param webResourceRequest webResourceRequest
* @param webResourceResponse webResourceResponse
*/
@Override
public void onReceivedHttpError(WebView webView, WebResourceRequest webResourceRequest,
WebResourceResponse webResourceResponse) {
super.onReceivedHttpError(webView, webResourceRequest, webResourceResponse);
}

/**
* 任何HTTPS请求,遇到SSL错误时都会回调这个方法。
* 比较正确的做法是让用户选择是否信任这个网站,这时候可以弹出信任选择框供用户选择(大部分正规浏览器是这么做的)。
* 有时候,针对自己的网站,可以让一些特定的网站,不管其证书是否存在问题,都让用户信任它。
* 坑:有时候部分手机打开页面报错,绝招:让自己网站的所有二级域都是可信任的。
* @param webView webView
* @param sslErrorHandler sslErrorHandler
* @param sslError sslError
*/
@Override
public void onReceivedSslError(WebView webView, SslErrorHandler sslErrorHandler, SslError sslError) {
super.onReceivedSslError(webView, sslErrorHandler, sslError);
//判断网站是否是可信任的,与自己网站host作比较
if (WebViewUtils.isYCHost(webView.getUrl())) {
//如果是自己的网站,则继续使用SSL证书
sslErrorHandler.proceed();
} else {
super.onReceivedSslError(webView, sslErrorHandler, sslError);
}
}

11 如何操作cookie?H5页面如何处理登录失效问题?

  • Cookie默认情况下是不需要做处理的,如果有特殊需求,如针对某个页面设置额外的Cookie字段,可以通过代码来控制。下面列出几个有用的接口:
    • 获取某个url下的所有Cookie:CookieManager.getInstance().getCookie(url)
    • 判断WebView是否接受Cookie:CookieManager.getInstance().acceptCookie()
    • 清除Session Cookie:CookieManager.getInstance().removeSessionCookies(ValueCallback callback)
    • 清除所有Cookie:CookieManager.getInstance().removeAllCookies(ValueCallback callback)
    • Cookie持久化:CookieManager.getInstance().flush()
    • 针对某个主机设置Cookie:CookieManager.getInstance().setCookie(String url, String value)

12.WebView先加载样式之后再加载图片配置

  • 默认情况html代码下载到WebView后,webkit开始解析网页各个节点,发现有外部样式文件或者外部脚本文件时,会异步发起网络请求下载文件。但如果在这之前也有解析到image节点,那势必也会发起网络请求下载相应的图片 。在网络情况较差的情况下,过多的网络请求就会造成带宽紧张,影响到css或js文件加载完成的时间 。造成页面空白loading过久。解决的方法就是告诉WebView先不要自动加载图片,等页面finish后再发起图片加载。在系统API在19以上的版本作了兼容,因为4.4以上系统在onPageFinished时再恢复图片加载时,如果存在多张图片引用的是相同的src时,会只有一个image标签得到加载,因而对于这样的系统我们就先直接加载。
1
2
3
4
5
6
if (Build.VERSION.SDK_INT >= 19) {
webView.getSettings().setLoadsImagesAutomatically(true); // 图片下载阻塞
webView.getSettings().setBlockNetworkImage(true);
} else {
webView.getSettings().setLoadsImagesAutomatically(false);
}

13.H5页面有混合http和https的链接,5.0以上系统不支持混合模式,需要通过配置来开启

1
2
3
4
//5.0及以上webview不支持http和https混合模式 需要通过配置来开启混合模式
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}

14 WebView播放视频问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1、此次的方案用到WebView,而且其中会有视频嵌套,在默认的WebView中直接播放视频会有问题, 而且不同的SDK版本情况还不一样,网上搜索了下解决方案,在此记录下. webView.getSettings.setPluginState(PluginState.ON);
webView.setWebChromeClient(new WebChromeClient());

2、然后在webView的Activity配置里面加上: android:hardwareAccelerated="true"

3、以上可以正常播放视频了,但是webview的页面都finish了居然还能听 到视频播放的声音, 于是又查了下发现webview的
onResume方法可以继续播放,onPause可以暂停播放, 但是这两个方法都是在Added in API level 11添加的,所以需要用反射来完成。

4、停止播放:在页面的onPause方法中使用:
webView.getClass().getMethod("onPause").invoke(webView, (Object[])null);

5、继续播放:在页面的onResume方法中使用:
webView.getClass().getMethod("onResume").invoke(webView,(Object[])null);
这样就可以控制视频的暂停和继续播放了。

15 如何解压zip包