Android WebView 常见问题总结

本文总结了一些开发中常见的问题,如 WebView 中图片不显示,inputfile 标签无效等问题。

H5 和 Native 交互

  1. 使用 JavaScriptInterface 注解约定的方法名称,方法名要禁止混淆。
  2. 自定义协议。Native 和 H5 互相发送消息(json 格式),解析,反射调用客户端方法。

WebView 的坑

图片显示不出来

Android 5.0 以后 url 协议和图片链接的 url 协议不一致导致的图片显示不出来,默认不允许混合模式。

1
2
3
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
webSetting.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
  • MIXED_CONTENT_ALWAYS_ALLOW 允许从任何来源加载内容,即使来源是不安全的;
  • MIXED_CONTENT_NEVER_ALLOW 不允许混合模式,即不允许从安全的起源去加载一个不安全的资源;
  • MIXED_CONTENT_COMPLTIBILITY_MODE 当涉及到混合式内容时,WebView 会尝试去兼容最新 Web 浏览器的风格;

在认证证书不被 Android 所接受的情况下,我们可以通过重写WebViewClient#onReceivedSslError() 方法,设置接受所有网站的证书来解决,具体代码如下:

1
2
3
4
5
6
7
webView.setWebViewClient(new WebViewClient() {
@Override public void onReceivedSslError(WebView view,
SslErrorHandler handler, SslError error){
// handler.cancel();// Android默认的处理方式
handler.proceed();// 接受所有网站的证书
}
});

< input type=”file” /> 标签无效

不同版本 SDK 中的 WebChromeClient 中的回调方法做了多次修改。5.0 以下的 openFileChooser() 有几种重载方法,在 5.0 及以上该方法废弃,回调方法为onShowFileChooser()。一定要注意 ValueCallback< Uri > mFileCallback 和 ValueCallback<Uri[]> mFileCallbacks 一定要执行 onReceiveValue() 方法 ,调用完成后 mFileCallback 和 mFileCallbacks 置为 null。

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
webview.setWebChromeClient(new WebChromeClient() {
// android 3.0以下:用的这个方法
public void openFileChooser(ValueCallback<Uri> valueCallback){
uploadMessage = valueCallback;
openImageChooserActivity();
}
// android 3.0以上,android4.0以下:用的这个方法
public void openFileChooser(ValueCallback valueCallback, String acceptType){
uploadMessage = valueCallback;
openImageChooserActivity();
}
//android 4.0 - android 4.3 安卓4.4.4也用的这个方法
public void openFileChooser(ValueCallback<Uri> valueCallback, String acceptType,
String capture){
uploadMessage = valueCallback;
openImageChooserActivity();
}

// Android 5.0及以上用的这个方法
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]>
filePathCallback, WebChromeClient.FileChooserParams fileChooserParams){
return true;
}
});

硬件加速导致大图无法显示

WebView 开启硬件加速时,加载大图无法显示。因为硬件加速中 OpenGL 对于内存是有限制的。如果遇到了这个限制,LogCat 只会报一个 Warning: Bitmap too large to be uploaded into a texture (587x7696, max=2048x2048)

硬件加速的四个级别:

  1. Application 级别
    android:hardwareAccelerated=”true”

  2. Activity 级别
    android:hardwareAccelerated=”true”

  3. window 级别

    1
    2
    getWindow().setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
    WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
  4. View 级别

    1
    view.setLayerType(View.LAYER_TYPE_HARDWARE, null);

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
/**
* 销毁WebView,避免内存泄露.<br/>
*
* @param clearCache 是否清空缓存.注意:所有WebView公用缓存.
* @see <a href="https://stackoverflow.com/questions/17418503/destroy-webview-in-android">stackoverflow</a>
*/
public static void destroyWebView(WebView webView, boolean clearCache) {
if (webView != null) {
final ViewParent parent = webView.getParent();
if (parent != null) {
//Error: WebView.destroy() called while still attached!
((ViewGroup) parent).removeView(webView);
}

webView.clearHistory();
// NOTE: clears RAM cache, if you pass true, it will also clear the disk cache.
// Probably not a great idea to pass true if you have other WebViews still alive.
webView.clearCache(clearCache);
// Loading a blank page is optional, but will ensure that the WebView isn't doing anything when you destroy it.
//webView.loadUrl("about:blank");

webView.onPause();
webView.removeAllViews();
webView.destroyDrawingCache();

webView.destroy();
webView = null;
}
}

启动优化

前端优化

  1. 降低请求量: 合并资源,减少 HTTP 请求数,minify / gzip 压缩,webP,lazyLoad。
  2. 加快请求速度: 预解析 DNS,减少域名数,并行加载,CDN 分发。
  3. 缓存: HTTP 协议缓存请求,离线缓存 manifest,离线数据缓存 localStorage。
  4. 渲染: JS/CSS 优化,加载顺序,服务端渲染,pipeline。

等页面 finish 再加载图片

webViewSettings().setLoadsImagesAutomatically(false);//默认“true”
webViewSettings().setBlockNetworkLoads(false);//有网络权限时默认“false”

1
2
3
4
5
6
7
webView.setWebViewClient(new WebViewClient(){
@Override public void onPageFinished(WebView view, String url) {
if(!settings.getLoadsImagesAutomatically()) {
settings.setLoadsImagesAutomatically(true);
}
}
});

Webview 预加载

第一步:Webview 预加载。App 启动就初始化一次 WebView。副作用是 WebView 的初始化必须位于主线程,但主线程会阻塞其他业务代码导致 ANR。

1
2
3
4
5
6
7
8
9
public class App extends Application {
@Override
public void onCreate() {
...
WebView webView = new WebView(this); // 无脑初始化一次Webview
webView.destroy();
webView = null;
}
}

第二步:WebView 池。在首次后台创建 WebView 后并不销毁,而是存入备用池,当用户需要时直接取出来使用,这样可以将 WebView 初始化时间降到几乎为 0。
副作用是内存占用上,首个 WebView 会占用十几兆内存,非首个 WebView 内存占用 0.2M 左右内存。另外 Android 里 WebView 是和 Activity 进行绑定的,为了避免内存泄露,我们在预先创建的时候,借助 Context 的中间层 MutableContextWrapper,使用 MutableContextWrapper 包裹 applicationContext 的方式去提前创建 WebView,当使用时将 context 置为 activity 的即可。

1
2
3
4
5
6
// 预创建WebView:
MutableContextWrapper contextWrapper = new MutableContextWrapper(applicationContext);
mPool[0] = new WebView(contextWrapper);

// 使用WebView
((MutableContextWrapper)webview.getContext()).setBaseContext(activityContext);

WebView 定制

WebSettings 设置

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
WebSettings settings = getSettings();
// 供 H5 判断是否是在 App 中打开
settings.setUserAgentString("Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0");
// 默认false,设置true后我们才能在WebView里与我们的JS代码进行交互
settings.setJavaScriptEnabled(true);
// 设置JS是否可以打开WebView新窗口
settings.setJavaScriptCanOpenWindowsAutomatically(true);
settings.setSupportZoom(true); // 支持缩放
settings.setBuiltInZoomControls(true); // 支持手势缩放
settings.setDisplayZoomControls(false); // 不显示缩放按钮
settings.setGeolocationEnabled(true);

//解决url是http但是连接内的图片时https,图片不显示的问题
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}

settings.setSaveFormData(true);
settings.setAppCacheEnabled(true); //启用应用缓存
settings.setDomStorageEnabled(true); //启用或禁用DOM缓存。
settings.setDatabaseEnabled(true); //启用或禁用数据库缓存。
settings.setDatabasePath("");
// LOAD_DEFAULT: 默认设置,如果有本地缓存,且缓存有效未过期,则直接使用本地缓存,否则加载网络数据
// LOAD_NORMAL: 废弃
// LOAD_CACHE_ELSE_NETWORK: 如果有本地缓存则直接使用本地缓存,而不管缓存数据是否过期失效,否则加载网络数据
// LOAD_NO_CACHE: 加载网络数据,不使用缓存
// LOAD_CACHE_ONLY: 有本地缓存,加载数据,否则加载失败
if (isNetworkAvailable()) { //对Page导航时才有效。比如按返回键回到上一个页面的情况.
// LOAD_DEFAULT: 默认的使用模式,即支持浏览器缓存机制
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
} else {
// 不从网络加载数据,只从缓存加载数据。
settings.setCacheMode(WebSettings.LOAD_CACHE_ONLY);
}

settings.setUseWideViewPort(true); // 将图片调整到适合WebView的大小
settings.setLoadWithOverviewMode(true); // 自适应屏幕
settings.setLoadsImagesAutomatically(false);//默认 true
settings.setBlockNetworkLoads(false);//有网络权限时默认 false

setHorizontalScrollBarEnabled(false);
setScrollbarFadingEnabled(true);
setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
// 取消 WebView 中滚动或拖动到顶部、底部时的阴影
setOverScrollMode(View.OVER_SCROLL_NEVER);

添加进度条

1
2
3
4
5
webView.setWebChromeClient(new WebChromeClient(){
@Override public void onProgressChanged(WebView view, int newProgress) {
super.onProgressChanged(view, newProgress);
}
});

自定义错误界面

需要处理下拉刷新或返回键返回上一页面的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
webView.setWebViewClient(new WebViewClient(){
@Override public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
//super.onReceivedError(view, request, error);
loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
view.setVisibility(View.VISIBLE);
}

@Override public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
super.onReceivedHttpError(view, request, errorResponse);
}

@Override public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
super.onReceivedSslError(view, handler, error);
}
});

判断是否滚动到页面底端

  1. 使用系统的 api 判断。

    1
    2
    3
    4
    if (webView.getContentHeight() * webView.getScale() ==
    (webView.getHeight() + webView.getScrollY())) {
    //滑动到底部
    }
  2. 重写 WebView#onScrollChanged() 方法处理。

    1
    2
    3
    @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) {
    super.onScrollChanged(l, t, oldl, oldt);
    }

判断是否存在垂直滚动条

1
2
3
public boolean existVerticalScrollbar(){
return computeVerticalScrollRange() > computeVerticalScrollExtent();
}

computeVerticalScrollRange() 得到的是可滑动的最大高度,computeVerticalScrollExtent() 得到的是滚动把手自身的高,当不存在滚动条时,两者的值是相等的。当有滚动条时前者一定是大于后者的。

使用网页的标题设置标题栏

1
2
3
4
5
6
7
WebChromeClient mWebChromeClient = new WebChromeClient() {
@Override public void onReceivedTitle(WebView view, String title){
super.onReceivedTitle(view, title);
txtTitle.setText(title);
}
};
mWedView.setWebChromeClient(mWebChromeClient());
  • 有的页面没有标题,可以设置一个默认标题;
  • WebView#goBack() 方法不一定调用,标题不更新。通过 HashMap 保存 url 和 title 的对应关系。

参考

[1] 腾讯浏览器服务(TBS、 x5)