1、问题背景:
- 我们用一个webview加载url时,在服务器响应远端数据之前,webview是空白的,我们期望这段时间显示一个端的loading
- webview加载出错后,页面展示不够友好,我们期望展示一个端的错误页面,包含“刷新”和“关闭”按钮
2、技术背景
webview的生命周期对应以下几种方法:

2.1、原生方法介绍
2.1.1、onReceivedError (WebView view, int errorCode, String description, String failingUrl)
官方描述该方法在 API level 23已经不推荐使用
This method was deprecated in API level 23.
UseonReceivedError(WebView, WebResourceRequest, WebResourceError)
instead.
2.1.2、onReceivedError (WebView view, WebResourceRequest request, WebResourceError error)
1 2 |
//该方法在API 23 版本里面添加,用于替代<br> onReceivedError(WebView view, int errorCode, String description, String failingUrl) |
该方法会向app回调web 资源加载失败的错误,这些错误通常表示无法连接到服务器。注意,与前面一个不推荐使用的回调方法不同,这个回调方法将会为任何的资源(例如: iframe, image,等),而不仅仅是主页面的失败回调。因此,建议在此回调中执行最少的必要工作。
实际使用过程中我们发现在API 23的手机上面,onReceivedError的新旧两个方法都会跑,所以实际使用时我们只要复写旧的onReceivedError方法就OK了
2.1.3、onReceivedHttpError
onReceivedError并不会在服务器返回错误码时被回调,那么当我们需要捕捉HTTP ERROR并进行相应操作时应该怎么办呢?API23便引入了该方法。当服务器返回一个HTTP ERROR并且它的status code>=400时,该方法便会回调。这个方法的作用域并不局限于Main Frame,任何资源的加载引发HTTP ERROR都会引起该方法的回调,所以我们也应该在该方法里执行尽量少的操作,只进行非常必要的错误处理等。
现在混合开发的app越来越多,Google对webview也越来越重视,在早期的版本webview有很多的bug,例如404、500等请求错误码我们无发直接获取(这个bug早在2008年就有人提交过issue给Google),好在Google在Android6.0修复了这个问题。根据google官网提供的最新的官网文档,我们可以重写onReceivedHttpError()方法可以捕获http Error。
2.2、状态接管分析
可以看出webview默认没有我们想要的加载中、加载成功、加载失败的状态,如何接管这些状态呢?
webview提供了”开始加载”的起始点,我们想要拿到加载中,首先必须拿到“加载成功”和“加载失败”这两个状态,其中“加载失败”需要考虑url加载超时的场景。
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 |
/** * webview是否加载成功,默认为false */ private boolean mWebViewLoadSuccess = false; /** * webview是否记载错误,默认为false */ private boolean mWebViewLoadError = false; @Override public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { super.onReceivedError(view, errorCode, description, failingUrl); //做对应的处理,弹出错误提示 onPageLoadError(false, failingUrl, errorCode, description); } @TargetApi(android.os.Build.VERSION_CODES.M) @Override public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) { super.onReceivedHttpError(view, request, errorResponse); String url = request.getUrl().toString(); int errCode = errorResponse.getStatusCode(); String errMsg = errorResponse.getReasonPhrase(); onPageLoadError(false, url, errCode, errMsg); } /** * webview onPageStart 方法回调 */ private void onPageLoadStart(WebView view, final String url, Bitmap favicon) { mWebViewLoadSuccess = false; mWebViewLoadError = false; } /** * webview onPageFinished 回调中调用此方法 */ private void onPageLoadFinish(WebView view, String url) { if(mWebViewStateCallback == null || !isValidUrl(url)) { return; } mWebViewStateCallback.onPageFinish(view, url); //设置url加载状态 if(!mWebViewLoadError) { mWebViewLoadSuccess = true; mWebViewStateCallback.onPageLoadSuccess(view, url); } mWebViewLoadError = false; } /** * 在webview 的 onReceivedError 和 onReceivedHttpError 回调方法中调用此方法,用于标记此次Url加载过程中出错 */ private void onPageLoadError(boolean isTimeout, String failingUrl, int errCode, String errMsg) { if(mWebViewStateCallback != null) { if(isTimeout) { mWebViewStateCallback.onTimeout(failingUrl); } else { mWebViewStateCallback.onLoadError(failingUrl, errCode, errMsg); } } //设置url加载状态 mWebViewLoadError = true; mWebViewLoadSuccess = false; } /** * webview onPageFinished 方法可能会回调多次,且url可能不是标准的url格式,需要过滤此种情况 */ private boolean isValidUrl(String url) { return !TextUtils.isEmpty(url) && Patterns.WEB_URL.matcher(url).matches(); } |
2.3、异常处理
2.3.1、页面加载成功,但仍然收到 onReceivedHttpError
1、收到错误信息如下:

2、为什么收到这个错误?
WevView是Android系统内置的一个浏览器,同别的浏览器一样,WebView在请求加载一个页面的同时,还会发送一个请求图标文件的请求。
比如我们采用WebView去加载一个页面:
webView.loadUrl("http://xxx.com/xxx.html");
同时还会发送一个请求图标文件的请求
http://xxx.com/favicon.ico
onReceivedHttpError这个方法主要用于响应服务器返回的Http错误(状态码大于等于400),这个回调将被调用任何资源(IFRAME,图像等),而不仅仅是主页面。所以就会出现主页面虽然加载成功,但由于网站没有favicon.ico文件导致返回404错误。
3、解决思路:
上面的分析我们知道不是每个网站都有favicon.ico,所以我们可以拦截webview的ico请求,返回一个空的结果,这样可以规避此问题。具体实现如下:
步骤一:复现shouldInterceptRequest,拦截ico请求。需要考虑高低版本兼容:
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 |
@TargetApi(android.os.Build.VERSION_CODES.LOLLIPOP) @SuppressLint("NewApi") @Override public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { if(!request.isForMainFrame() && !TextUtils.isEmpty(request.getUrl().getPath()) && request.getUrl().getPath().endsWith("/favicon.ico")) { try { return new WebResourceResponse("image/png", null, null); } catch (Exception e) { e.printStackTrace(); } } return null; } @Override public WebResourceResponse shouldInterceptRequest(WebView view, String url) { if(url.toLowerCase().contains("/favicon.ico")) { try { return new WebResourceResponse("image/png", null, null); } catch (Exception e) { e.printStackTrace(); } } return null; } |
步骤二:修改onReceivedHttpError
1 2 3 4 5 6 7 8 9 10 11 |
@TargetApi(android.os.Build.VERSION_CODES.M) @Override public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) { super.onReceivedHttpError(view, request, errorResponse); if (!request.isForMainFrame() && request.getUrl().getPath().endsWith("/favicon.ico") ) { Log.e(TAG,"favicon.ico 请求错误"+errorResponse.getStatusCode()+errorResponse.getReasonPhrase()); return; } // 省略其它代码 } |