Android开发日记(十四)——带你解决 WebView 里的常见问题

通常我们在自己开发的 APP 中打开网页无非两种方法: 一是跳转到系统自带的浏览器,二是使用 WebView 控件加载页面。使用 WebView 控件的好处就是可以通过各种 api 接口来定制各种行为,常用的几个设置地方为 WebSettingsJavaScriptInterfaceWebViewClientWebChromeClient。平时出现的问题都可以通过修改这些设置来解决。

使用了 WebView 还是跳转到了系统自带的浏览器?

很简单的解决方法,为你的 webview 设置一个新的 WebViewClient。

1
2
3
4
5
6
7
webView.setWebViewClient(new WebViewClient(){
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
});

1
2
// 或者直接添加,效果是一样的
webView.setWebViewClient(new WebViewClient());

获取网页的标题和图标

通过 WebChromeClient 可以获取到这些信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
webView.setWebChromeClient(new WebChromeClient() {
@Override
public void onReceivedTitle(WebView view, String title) {
super.onReceivedTitle(view, title);
setTitle(title);
}

@Override
public void onReceivedIcon(WebView view, Bitmap icon) {
super.onReceivedIcon(view, icon);
setIcon(icon);
}

});

但是,这里有个问题,当通过 webView.goBack() 方式返回上一级Web页面的时候不会触发这个方法,因此会导致标题无法跟随历史记录返回上一级页面。所以需要在 onPageFinished() 中对界面标题重新设置。

1
2
3
4
5
6
7
webView.setWebViewClient(new WebViewClient(){
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
setTitle(String.valueOf(view.getTitle()));
}
});

返回键实现网页的后退键

在 WebView 中可以通过 goBack() 方法后退到历史记录的上一项。

1
2
3
4
5
6
7
8
// 在 Actvity 中监听返回键按钮
@Override
public void onBackPressed() {
if (webView.canGoBack())
webView.goBack();
else
super.onBackPressed();
}

设置 WebView 的 header

在 WebView 的 loadUrl() 方法中传入 Header 参数即可。

1
2
3
4
5
6
7
public void loadURLWithHTTPHeaders() {
final String url = "http://cpacm.net";
WebView webView = new WebView(getActivity());
Map<String,String> extraHeaders = new HashMap<String, String>();
extraHeaders.put("Referer", "http://www.google.com");
webView.loadUrl(url, extraHeaders);
}

设置 WebView 的 User-Agent

不要试图在 Header 里面去修改,而是在 WebSettings 修改

1
webView.getSettings().setUserAgentString("Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0");

如何设置 WebView 的缓存

当需要本地缓存网页的时候就需要打开 WebViewSettings 的缓存开关,这样子当下次进到该页面无网络的情况下也能打开页面。

1
2
3
4
5
6
7
8
9
WebSettings settings = webView.getSettings();
settings.setAppCacheEnabled(true); //启用应用缓存
settings.setDomStorageEnabled(true); //启用或禁用DOM缓存。
settings.setDatabaseEnabled(true); //启用或禁用DOM缓存。
if (SystemUtil.isNetworkConnected()) { //判断是否联网
settings.setCacheMode(WebSettings.LOAD_DEFAULT); //默认的缓存使用模式
} else {
settings.setCacheMode(WebSettings.LOAD_CACHE_ONLY); //不从网络加载数据,只从缓存加载数据。
}

无法下载文件?

在自己写的 WebView 下是无法直接下载文件,需要自己监听下载事件并对下载的动作进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 当下载文件时打开系统自带的浏览器进行下载,当然也可以对捕获到的 url 进行处理在应用内下载。
**/

webView.setDownloadListener(new FileDownLoadListener());

private class FileDownLoadListener implements DownloadListener {
@Override
public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
Uri uri = Uri.parse(url);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);
}
}

无法打开文件选择器?

通过重写 WebChromeClient 来实现点击 <input type='file'> 来打开系统文件选择器。

一个完整的Activity示例

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
144
public class MainActivity extends AppCompatActivity {

/** Android 5.0以下版本的文件选择回调 */
protected ValueCallback<Uri> mFileUploadCallbackFirst;
/** Android 5.0及以上版本的文件选择回调 */
protected ValueCallback<Uri[]> mFileUploadCallbackSecond;

protected static final int REQUEST_CODE_FILE_PICKER = 51426;


protected String mUploadableFileTypes = "image/*";

private WebView mWebView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

initWebView();
}

private void initWebView() {
mWebView = (WebView) findViewById(R.id.my_webview);

mWebView.loadUrl("file:///android_asset/index.html");
mWebView.setWebChromeClient(new OpenFileChromeClient());
}

private class OpenFileChromeClient extends WebChromeClient {

// Android 2.2 (API level 8)到Android 2.3 (API level 10)版本选择文件时会触发该隐藏方法
@SuppressWarnings("unused")
public void openFileChooser(ValueCallback<Uri> uploadMsg) {
openFileChooser(uploadMsg, null);
}

// Android 3.0 (API level 11)到 Android 4.0 (API level 15))版本选择文件时会触发,该方法为隐藏方法
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) {
openFileChooser(uploadMsg, acceptType, null);
}

// Android 4.1 (API level 16) -- Android 4.3 (API level 18)版本选择文件时会触发,该方法为隐藏方法
@SuppressWarnings("unused")
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
openFileInput(uploadMsg, null, false);
}

// Android 5.0 (API level 21)以上版本会触发该方法,该方法为公开方法
@SuppressWarnings("all")
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) {
if (Build.VERSION.SDK_INT >= 21) {
final boolean allowMultiple = fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE;//是否支持多选
openFileInput(null, filePathCallback, allowMultiple);
return true;
}
else {
return false;
}
}
}

@SuppressLint("NewApi")
protected void openFileInput(final ValueCallback<Uri> fileUploadCallbackFirst, final ValueCallback<Uri[]> fileUploadCallbackSecond, final boolean allowMultiple) {
//Android 5.0以下版本
if (mFileUploadCallbackFirst != null) {
mFileUploadCallbackFirst.onReceiveValue(null);
}
mFileUploadCallbackFirst = fileUploadCallbackFirst;

//Android 5.0及以上版本
if (mFileUploadCallbackSecond != null) {
mFileUploadCallbackSecond.onReceiveValue(null);
}
mFileUploadCallbackSecond = fileUploadCallbackSecond;

Intent i = new Intent(Intent.ACTION_GET_CONTENT);
i.addCategory(Intent.CATEGORY_OPENABLE);

if (allowMultiple) {
if (Build.VERSION.SDK_INT >= 18) {
i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
}
}

i.setType(mUploadableFileTypes);

startActivityForResult(Intent.createChooser(i, "选择文件"), REQUEST_CODE_FILE_PICKER);

}

public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) {
if (requestCode == REQUEST_CODE_FILE_PICKER) {
if (resultCode == Activity.RESULT_OK) {
if (intent != null) {
//Android 5.0以下版本
if (mFileUploadCallbackFirst != null) {
mFileUploadCallbackFirst.onReceiveValue(intent.getData());
mFileUploadCallbackFirst = null;
}
else if (mFileUploadCallbackSecond != null) {//Android 5.0及以上版本
Uri[] dataUris = null;

try {
if (intent.getDataString() != null) {
dataUris = new Uri[] { Uri.parse(intent.getDataString()) };
}
else {
if (Build.VERSION.SDK_INT >= 16) {
if (intent.getClipData() != null) {
final int numSelectedFiles = intent.getClipData().getItemCount();

dataUris = new Uri[numSelectedFiles];

for (int i = 0; i < numSelectedFiles; i++) {
dataUris[i] = intent.getClipData().getItemAt(i).getUri();
}
}
}
}
}
catch (Exception ignored) { }
mFileUploadCallbackSecond.onReceiveValue(dataUris);
mFileUploadCallbackSecond = null;
}
}
}
else {
//这里mFileUploadCallbackFirst跟mFileUploadCallbackSecond在不同系统版本下分别持有了
//WebView对象,在用户取消文件选择器的情况下,需给onReceiveValue传null返回值
//否则WebView在未收到返回值的情况下,无法进行任何操作,文件选择器会失效
if (mFileUploadCallbackFirst != null) {
mFileUploadCallbackFirst.onReceiveValue(null);
mFileUploadCallbackFirst = null;
}
else if (mFileUploadCallbackSecond != null) {
mFileUploadCallbackSecond.onReceiveValue(null);
mFileUploadCallbackSecond = null;
}
}
}
}

}

怎么为 WebView 的加载添加进度条

这里的 onPageFinished() 有个问题,不能在这里监听页面是否加载完毕(我自己测试的时候,好像在重定向和加载完 iframes 时都会调用这个方法)。
把页面加载完毕的判断放在 onProgressChanged() 里可能会更为准确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
webView.setWebChromeClient(new WebChromeClient() {

@Override
public void onProgressChanged(WebView view, int position) {
progressBar.setProgress(position);
if (position == 100) {
progressBar.setVisibility(View.GONE);
}
super.onProgressChanged(view, position);
}
});

webView.setWebViewClient(new WebViewClient(){
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
progressBar.setVisibility(View.VISIBLE);
super.onPageStarted(view, url, favicon);
}
});

怎样对页面进行 Js 注入?

首先你要在 WebView 开启 JavaScript,然后搭建桥梁

1
2
3
4
5
6
7
8
9
10
11
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webView.addJavascriptInterface(new WebAppBridge(new WebAppBridge.OauthLoginImpl() {
@Override
public void getResult(String s) {
//TODO
}
}),
"oauth");
webView.loadUrl("javascript:" + getAssetsJs("autologin.js"));
webView.loadUrl("javascript:adduplistener()");

WebAppBridge的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class WebAppBridge {

private OauthLoginImpl oauthLogin;

public WebAppBridge(OauthLoginImpl oauthLogin) {
this.oauthLogin = oauthLogin;
}

@JavascriptInterface
public void getResult(String str) {
if (oauthLogin != null)
oauthLogin.getResult(str);
}

public interface OauthLoginImpl {
void getResult(String s);
}
}

简单的说就是向网页注入一段 js, 在这段 js 里面设置回调到java中的方法 getResult(),由 WebAppBridge.getResult 来回收。
其中js的核心代码为

1
oauth.getResult(str);

其中 oauth 这个名称要与 webView.addJavascriptInterface()方法的第二个参数一样。

具体的代码可以参考这个项目中写的 js 注入逻辑 OauthDialog

需要获得 CookieManager 的对象并将 cookie 设置进去。
从服务器的返回头中取出 cookie 根据Http请求的客户端不同,获取 cookie 的方式也不同,请自行获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 将cookie设置到 WebView
* @param url 要加载的 url
* @param cookie 要同步的 cookie
*/

public static void syncCookie(String url,String cookie) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
CookieSyncManager.createInstance(context);
}
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.setAcceptCookie(true);

/**
* cookie 设置形式
* cookieManager.setCookie(url, "key=value;" + "domain=[your domain];path=/;")
**/

cookieManager.setCookie(url, cookie);
}

删除 Cookie 的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 这个两个在 API level 21 被抛弃
* CookieManager.getInstance().removeSessionCookie();
* CookieManager.getInstance().removeAllCookie();
*
* 推荐使用这两个, level 21 新加的
* CookieManager.getInstance().removeSessionCookies();
* CookieManager.getInstance().removeAllCookies();
**/

public static void removeCookies() {
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.removeAllCookie();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cookieManager.flush();
} else {
CookieSyncManager.createInstance(Application.getInstance());
CookieSyncManager.getInstance().sync();
}
}

如何使 HTML5 video 在 WebView 全屏显示

当网页全屏播放视频时会调用 WebChromeClient.onShowCustomView() 方法,所以可以通过将 video 播放的视图全屏达到目的。

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
@Override
public void onShowCustomView(View view, CustomViewCallback callback) {
if (view instanceof FrameLayout && fullScreenView != null) {
// A video wants to be shown
this.videoViewContainer = (FrameLayout) view;
this.videoViewCallback = callback;
fullScreenView.addView(videoViewContainer, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
fullScreenView.setVisibility(View.VISIBLE);
isVideoFullscreen = true;
}
}

@Override
public void onHideCustomView() {
if (isVideoFullscreen && fullScreenView != null) {
// Hide the video view, remove it, and show the non-video view
fullScreenView.setVisibility(View.INVISIBLE);
fullScreenView.removeView(videoViewContainer);

// Call back (only in API level <19, because in API level 19+ with chromium webview it crashes)
if (videoViewCallback != null && !videoViewCallback.getClass().getName().contains(".chromium.")) {
videoViewCallback.onCustomViewHidden();
}

isVideoFullscreen = false;
videoViewContainer = null;
videoViewCallback = null;
}
}

但是很多的手机版本在网页视频播放时是不会调用这个方法的,所以这个方法局限性很大。

Android5.0上 WebView中Http和Https混合问题

1
2
3
4
5
6
7
8
/**
* MIXED_CONTENT_ALWAYS_ALLOW:允许从任何来源加载内容,即使起源是不安全的;
* MIXED_CONTENT_NEVER_ALLOW:不允许Https加载Http的内容,即不允许从安全的起源去加载一个不安全的资源;
* MIXED_CONTENT_COMPATIBILITY_MODE:当涉及到混合式内容时,WebView 会尝试去兼容最新Web浏览器的风格。
**/

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
webView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}

如何避免 WebView 的内存泄露问题

  1. 可以将 Webview 的 Activity 新起一个进程,结束的时候直接System.exit(0);退出当前进程;
  2. 不在xml中定义 WebView,而是在代码中创建,使用 getApplicationgContext() 作为传递的 Conetext;
  3. 在 Activity 销毁的时候,将 WebView 置空
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Override
    protected void onDestroy() {
    if (webView != null) {
    webView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
    webView.clearHistory();
    ((ViewGroup) webView.getParent()).removeView(webView);
    webView.destroy();
    webView = null;
    }
    super.onDestroy();
    }

总结

如果你踩到了 WebView 上的坑,请先默哀一分钟,然后努力找找解决方法吧,总会有人体验过你的悲剧,也会有人重蹈你的覆辙。
当然 WebView 里肯定不止我上面列出来的这些问题,如果你有更多的 WebView 问题解决方案欢迎评论交流。