设计思想理解

在WebView的设计中,不是什么事都要WebView类干的,有相当多的杂事是分给其他类做的,这样WebView专心干好自己的解析、渲染工作就行了。比如我们最熟知的,所有针对WebView的设置都封装到了WebSettings里。我们知道,在使用WebView加载资源过程中,可能会有大量各种类型事件的回调,为了方便开发组处理这些回调,针对不同的事件回调,google将这些回调进行了分类集合,于是就产生了WebViewClient、WebChromeClient这两个大类。
很多同学一看到WebChromeClient类里有Chrome,立马就会想到google的Chrome浏览器,其实这里并不是”特指”Chrome浏览器的意思,而是”泛指”浏览器的意思。
为什么叫WebChromeClient呢?这是因为WebChromeClient中集合了影响浏览器的事件到来时的回调方法,所以这里需要突出浏览器的概念,而Chrome则是google自家的浏览器名称,也是目前市面上最受欢迎的浏览器,所以就采用了WebChromeClient来做为名称吧,纯属臆想……
简单来说就是

  • WebViewClient:在影响【View】的事件到来时,会通过WebViewClient中的方法回调通知用户
  • WebChromeClient:当影响【浏览器】的事件到来时,就会通过WebChromeClient中的方法回调通知用法。

回调事件总结

WebViewClient就是帮助WebView处理各种通知、请求事件的,常用到的如:

  1. onLoadResource、onPageStart、onPageFinish
  2. onReceiveError、onReceivedHttpError、onReceivedSslError
  3. shouldInterceptRequest、shouldOverrideKeyEvent、shouldOverrideUrlLoading
  4. onReceivedClientCertRequest、onReceivedHttpAuthRequest、onReceivedLoginRequest
  5. 其他:doUpdateVisitedHistory、onFormResubmission、onPageCommitVisible、onRenderProcessGone、onScaleChanged、onUnhandledKeyEvent

实际使用的话,如果你的WebView只是用来处理一些html的页面内容,只用WebViewClient就行了,如果需要更丰富的处理效果,比如JS、进度条等,就要用到WebChromeClient。

API

shouldInterceptRequest 方法

  • WebResourceResponse  shouldInterceptRequest(WebView view, WebResourceRequest request)  Notify the host application of a resource request and allow the application to return the data. 通知资源请求的主机应用程序,并允许应用程序返回数据。
    • If the return value is null, the WebView will continue to load the resource as usual. Otherwise, the return返回的 response and data will be used. NOTE: This method is called on a thread other than而不是 the UI thread so clients should exercise caution谨慎 when accessing private data or the view system. 
    • 该函数会在请求资源前调用,且无论任何资源,比如超链接、JS文件、图片等,在每一次请求资源时都会回调。我们可以通过返回一个自定义的WebResourceResponse来让WebView加载指定的资源。比如,如果我们需要改变网页的背景,替换网页中图片等,都可以在这个回调时处理。但是必须注意的是,此回调是在非UI线程中执行的。
    • 参数 request:Object containing the details of the request. 包含请求的详细信息的对象
    • 返回值:A WebResourceResponse containing the response information or null if the WebView should load the resource itself.

例如,替换所有的图片为自定义的图片

if (request.getUrl().toString().endsWith(".jpg")) {
    try {
        return new WebResourceResponse("text/html", "UTF-8", view.getContext().getAssets().open("icon.jpg"));
    } catch (IOException e) {
        e.printStackTrace();
    }
}
return null;
  • WebResourceResponse  shouldInterceptRequest(WebView view, String url)  This method was deprecated in API level 21. Use shouldInterceptRequest(WebView, WebResourceRequest) instead.
  • boolean  shouldOverrideKeyEvent(WebView view, KeyEvent event)  Give the host application a chance to handle the key event synchronously. 给主机应用程序一次同步处理键事件的机会。默认行为返回false。
    • e.g. 例如 menu shortcut菜单快捷键 key events need to be filtered过滤 this way. If return true, WebView will not handle the key event. If return false, WebView will always handle the key event, so none of the super in the view chain will see the key event.
    • 重写此方法才能够处理在浏览器中的按键事件。如果应用程序想要处理该事件则返回true,否则返回false。
    • 返回值:True if the host application wants to handle the key event itself, otherwise return false. 

shouldOverrideUrlLoading 方法

boolean  shouldOverrideUrlLoading(WebView view, WebResourceRequest request)  Give the host application a chance to take over the control whena new url is about to be loaded in the current WebView. 当一个新的url即将加载到当前的WebView中时,让主机应用程序有机会接管控制权。

  • 当加载的网页需要重定向的时候就会回调这个函数,告知我们应用程序是否需要接管控制网页加载,如果应用程序接管并且return true,意味着主程序接管网页加载,如果返回false,则会让webview自己处理。
  • 由于每次超链接在加载前都会先走shouldOverrideUrlLoading回调,所以如果我们想拦截某个URL(比如将其转换成其它URL进行自定义跳转,或弹吐司等其他操作)可以在这里做。
  • If WebViewClient is not provided, by default WebView will ask Activity Manager to choose the proper handler正确的处理程序 for the url. If WebViewClient is provided, return true means the host application handles the url, while return false means the current WebView handles the url. 则返回true表示主机应用程序处理该url,而返回false表示当前的WebView处理该URL。
  • 根据以上描述可以知道:我们只需仅仅给WebView设置一个WebViewClient对象,而不需要重写shouldOverrideUrlLoading方法(即使用它的默认回调),就可以实现在此WebView中加载网页中的其他URL了。现在大部分APP采用的重写shouldOverrideUrlLoading的做法大都是画蛇添足(当然如果需要自定义跳转的话,是一定要重写此方法的)。
  • 参数 request:Object containing the details of the request.
  • 返回值: 返回true则当前应用程序要自己处理这个url, 返回false则不处理。
Notes:
This method is not called for requests using the POST "method".  当请求的方式是"POST"方式时这个回调是不会通知的。
This method is also called for subframes with non-http schemes,  这种方法也被称为具有非http方案的子帧
    thus it is strongly disadvised劝止,劝阻(某人做某事) to unconditionally(无条件地) call loadUrl(String) 
    from inside the method 
  • boolean  shouldOverrideUrlLoading(WebView view, String url)  This method was deprecated in API level 24. Use shouldOverrideUrlLoading(WebView, WebResourceRequest) instead.

Error回调

  • void  onReceivedError(WebView view, int errorCode, String description, String failingUrl)  This method was deprecated in API level 23. Use onReceivedError(WebView, WebResourceRequest, WebResourceError) instead.
  • void  onReceivedError(WebView view, WebResourceRequest request, WebResourceError error)  Report web resource loading error to the host application. 向主机应用程序报告Web资源加载错误
    • 当浏览器访问指定的网址发生错误时会通知我们应用程序,比如网络错误。我们可以在这里做错误处理,比如再请求加载一次,或者提示404的错误页面。
    • These errors usually indicate表示 inability to connect to the server无法连接到服务器. Note that unlike the deprecated废弃 version of the callback, the new version will be called for any resource (iframe, image, etc), not just for the main page. Thus因此, it is recommended to perform minimum required work 执行最低要求的工作 in this callback.
    • 参数 request:The originating request.
    • 参数 error:Information about the error occured.
onPageFinished tells you that the WebView has stopped loading, onReceivedError tells you there was an error.
They're not "success" and "failure" callbacks which is why you'll get both in case of an error.
也即:onPageFinished仅仅表示网页加载完成了,不能说明这个网页是否成功的加载了。
  • void  onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse)  Notify the host application that an HTTP error has been received from the server while loading a resource. 通知主机应用程序在加载资源时从服务器收到HTTP错误。
    • HTTP errors have status codes >= 400. This callback will be called for any resource (iframe, image, etc), not just for the main page. Thus, it is recommended to perform minimum required work 执行最低要求的工作 in this callback. 
    • Note that the content of the server response服务器响应的内容 may not be provided within the errorResponse parameter 参数中可能不提供.
    • 参数 request:The originating request.
    • 参数 errorResponse:Information about the error occured.
  • void  onReceivedSslError(WebView view, SslErrorHandler handler, SslError error)  Notify the host application that an SSL error occurred while loading a resource. 当网页加载资源过程中发现SSL错误时回调。
    • The host application must call either handler.cancel() or handler.proceed(). Note that the decision决定 may be retained保留 for use in response用来响应 to future SSL errors. The default behavior is to cancel the load.
    • HTTPS协议是通过SSL来通信的,所以当使用HTTPS通信的网址出现错误时,就会通过onReceivedSslError回调通知过来
    • SslErrorHandler只有两个函数proceed()和cancel()。proceed()表示忽略错误继续加载,cancel()表示取消加载。在onReceivedSslError的默认实现中是使用的cancel()来取消加载,所以一旦出来SSL错误,HTTPS网站就会被取消加载了。如果想忽略错误继续加载就只有重写onReceivedSslError,并在其中调用proceed()。
    • 当HTTPS传输出现SSL错误时,错误会只通过onReceivedSslError回调传过来,而不会触发onReceivedError回调。
    • 参数 handler:An SslErrorHandler object that will handle the user’s response. 处理用户请求的对象。
    • error :The SSL error object. 包含了当前SSL错误的基本所有信息

开始加载和加载完成

  • void  onPageFinished(WebView view, String url)  Notify the host application that a page has finished loading. 当内核加载完当前页面时会通知我们的应用程序
    • When onPageFinished() is called, the rendering picture渲染的图片 may not be updated yet. To get the notification for the new Picture, use onNewPicture(WebView, Picture).
    • This method is called only for main frame. 
  • void  onPageStarted(WebView view, String url, Bitmap favicon)  Notify the host application that a page has started loading.当内核开始加载访问的url时会通知应用程序
    • This method is called once for each main frame load so a page with iframes or framesets will call onPageStarted one time for the main frame. 对每个main frame,这个函数只会被调用一次,所以如果一个页面包含 iframe 或者 framesets 不会另外调用一次
    • This also means that onPageStarted will not be called when the contents of an embedded frame changes, i.e. clicking a link whose target is an iframe, it will also not be called for fragment navigations (navigations to #fragment_id). 当网页内内嵌的 frame 发生改变时也不会调用onPageStarted。即点击目标是iframe的链接,也不会调用fragment导航(导航到#fragment_id)
    • 参数 Bitmap favicon(网站图标):如果这个favicon已经存储在本地数据库中,则会返回这个网页的favicon,否则返回为null。
(1) iframe 可能不少人不知道什么含义,这里我解释下,iframe 我们加载的一张,下面有很多链接,我们随便点击一个链接是即当前host的一个iframe.
(2) 有个问题可能是开发者困惑的,onPageStarted和shouldOverrideUrlLoading 在网页加载过程中这两个函数到底哪个先被调用。
     当我们通过loadUrl的方式重新加载一个网址时候,这时候会先调用onPageStarted再调用shouldOverrideUrlLoading
     当我们在打开的这个网址点击一个link,这时候会先调用shouldOverrideUrlLoading再调用onPageStarted。
     不过shouldOverrideUrlLoading不一定每次都被调用,只有需要的时候才会被调用。

其他回调方法

  • void  doUpdateVisitedHistory(WebView view, String url, boolean isReload)  Notify the host application to update its visited links database. 通知主机应用程序更新其访问链接数据库(更新访问历史)
    • 通知应用程序可以将当前的url存储在数据库中,意味着当前的访问url已经生效并被记录在内核当中。这个函数在网页加载过程中只会被调用一次。注意网页前进后退并不会回调这个函数。
    • 参数 url:The url being visited. 当前正在访问的url 
    • 参数 isReload:True if this url is being reloaded. 如果是true,那么这个是正在被reload的url
  • void  onFormResubmission(WebView view, Message dontResend, Message resend)  As the host application if the browser should resend data asthe requested page was a result of a POST. 作为主机应用程序,如果浏览器应该重新发送数据,因为请求的页面是POST的结果
    • 如果浏览器需要重新发送POST请求,可以通过这个时机来处理。默认是不重新发送数据。
    • 参数 dontResend:The message to send if the browser should not resend 当浏览器不需要重新发送数据时使用的参数。
    • 参数 resend:The message to send if the browser should resend data 当浏览器需要重新发送数据时使用的参数。
  • void  onLoadResource(WebView view, String url)  Notify the host application that the WebView will load the resource specified by the given url. 通知应用程序WebView即将加载 url 指定的资源。
    • 注意,每一个资源(比如图片)的加载都会调用一次此方法。
  • void  onPageCommitVisible(WebView view, String url)  Notify the host application that WebView content left over from previous page navigations will no longer be drawn. 通知主机应用程序将不再绘制从上一页导航遗留的WebView内容。
    • This callback can be used to determine确定 the point at which it is safe to make a recycled WebView visible, ensuring保证 that no stale陈旧的 content is shown. It is called at the earliest point at which it can be guaranteed确保 that onDraw(Canvas) will no longer draw any content from previous navigations. The next draw will display either the background color of the WebView, or some of the contents of the newly loaded page.
    • This method is called when the body of the HTTP response has started loading, is reflected in反映在 the DOM, and will be visible in subsequent随后 draws. This callback occurs early in the document loading process在文档加载过程的早期发生, and as such you should expect期望、明白 that linked resources (for example, css and images) may not be available.
    • This callback is only called for main frame navigations.
  • void  onReceivedClientCertRequest(WebView view, ClientCertRequest request)  Notify the host application to handle a SSL client certificate request. 通知主机应用程序来处理SSL客户端证书请求。
    • The host application is responsible负责 for showing the UI if desired需要 and providing the keys. There are three ways to respond: proceed(), cancel() or ignore(). Webview stores the response in memory (for the life of the application) if proceed() or cancel() is called and does not call onReceivedClientCertRequest() again for the same host and port pair 针对相同的主机和端口. Webview does not store the response if ignore() is called. 
    • Note that, multiple layers多层 in chromium network stack might be caching the responses缓存响应, so the behavior for ignore is only a best case effort只是最好努力的情况. This method is called on the UI thread. During the callback, the connection is suspended暂停. 
    • For most use cases, the application program should implement the KeyChainAliasCallback interface and pass it to choosePrivateKeyAlias(Activity, KeyChainAliasCallback, String[], Principal[], Uri, String) to start an activity for the user to choose the proper alias别名. The keychain activity will provide the alias through the callback method in the implemented interface. Next the application should create an async task to call getPrivateKey(Context, String) to receive the key. 
    • An example implementation of client certificates can be seen at AOSP Browser. 
    • The default behavior is to cancel, returning no client certificate.
  • void  onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm)  Notifies the host application that the WebView received an HTTP authentication request. 通知应用程序WebView接收到了一个Http auth的请求
    • The host application can use the supplied提供的 HttpAuthHandler to set the WebView’s response to the request. The default behavior is to cancel the request.
    • 参数 handler:用来响应WebView请求的HttpAuthHandler对象
    • 参数 host:请求认证的host
    • 参数 realm:认证请求所在的域
  • void  onReceivedLoginRequest(WebView view, String realm, String account, String args)  Notify the host application that a request to automatically log in the user has been processed. 通知应用程序有个自动登录帐号过程(通知主程序执行了自动登录请求)
    • 参数 realm :The account realm used to look up accounts. 账户的域名,用来查找账户。
    • 参数 account:An optional account 可选的账户. If not null, the account should be checked against accounts on the device 需要和本地的账户进行. If it is a valid可用 account, it should be used to log in the user.
    • 参数 args:Authenticator specific arguments used to log in the user. 验证指定参数的登录用户
  • boolean  onRenderProcessGone(WebView view, RenderProcessGoneDetail detail)  Notify host application that the given webview’s render process has exited. 通知主机应用程序,给定的Webview渲染进程已退出。
    • Multiple多个 WebView instances may be associated with关联 a single render渲染 process; onRenderProcessGone will be called for each WebView that was affected受影响的. 
    • The application’s implementation of this callback should only attempt to clean up the specific特定的 WebView given as a parameter作为参数提供的, and should not assume假定、假设 that other WebView instances are affected. The given WebView can’t be used, and should be removed from the view hierarchy视图层次结构, all references to it should be cleaned up, e.g any references in the Activity or other classes saved using findViewById and similar calls, etc等等. To cause an render process crash for test purpose 为了测试目的,导致渲染过程崩溃, the application can call loadUrl(“chrome://crash”) on the WebView. 
    • Note that multiple WebView instances may be affected if they share共享 a render process, not just而不仅仅是 the specific WebView which loaded chrome://crash.
    • 参数 detail:the reason why it exited.
    • 返回值:true if the host application handled the situation情况 that process has exited, otherwise, application will crash if render process crashed, or be killed if render process was killed by the system.
  • void  onScaleChanged(WebView view, float oldScale, float newScale)  Notify the host application that the scale applied to the WebView has changed.
    • WebView显示缩放比例发生改变时调用
  • void  onTooManyRedirects(WebView view, Message cancelMsg, Message continueMsg)  This method was deprecated in API level 8. This method is no longer called. When the WebView encounters a redirect loop, it will cancel the load.
  • void  onUnhandledKeyEvent(WebView view, KeyEvent event)  Notify the host application that a key was not handled by the WebView. 通知主机应用程序,一个键未被WebView处理。
    • Except system keys, WebView always consumes消耗 the keys in the normal flow正常流中的键 or if shouldOverrideKeyEvent returns true. 
    • This is called asynchronously异步 from where the key is dispatched调用. It gives the host application a chance to handle the unhandled key events.
    • 注意:如果事件为MotionEvent,则事件的生命周期只存在方法调用过程中,如果WebViewClient想要使用这个Event,则需要复制Event对象。

案例

public class MyWebViewClient extends WebViewClient {
private ProgressBar mProgressBar;
private WebViewActivity activity;
public MyWebViewClient(WebViewActivity activity) {
super();
this.activity = activity;
mProgressBar = activity.getProgress_bar();
}
@Override
public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) {
//通知主机应用程序更新其访问链接数据库(更新访问历史)。isReload:是否是正在被reload的url
Log.i("bqt", "【doUpdateVisitedHistory】" + url + "   " + isReload);
super.doUpdateVisitedHistory(view, url, isReload);
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
//favicon(网站图标):如果这个favicon已经存储在本地数据库中,则会返回这个网页的favicon,否则返回为null
Log.i("bqt", "【onPageStarted】" + url);
if (mProgressBar != null) mProgressBar.setVisibility(View.VISIBLE);//在开始加载时显示进度条
activity.getIv_icon().setVisibility(View.GONE);
super.onPageStarted(view, url, favicon);
}
@Override
public void onPageFinished(WebView view, String url) {
Log.i("bqt", "【onPageFinished】" + url);
if (mProgressBar != null) mProgressBar.setVisibility(View.GONE);//在结束加载时隐藏进度条
super.onPageFinished(view, url);
}
@Override
public void onLoadResource(WebView view, String url) {
Log.i("bqt", "【onLoadResource】" + url);//每一个资源(比如图片)的加载都会调用一次
super.onLoadResource(view, url);
}
@TargetApi(Build.VERSION_CODES.M)
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
//访问指定的网址发生错误时回调,我们可以在这里做错误处理,比如再请求加载一次,或者提示404的错误页面
//如点击一个迅雷下载的资源时【ftp://***  -10  net::ERR_UNKNOWN_URL_SCHEME】
Log.i("bqt", "【onReceivedError】" + request.getUrl().toString() + "  " + error.getErrorCode() + "  " + error.getDescription());
if (error.getErrorCode() == -10) view.loadUrl("file:///android_asset/h5/test.html");
else super.onReceivedError(view, request, error);
}
@TargetApi(Build.VERSION_CODES.M)
@Override
public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
//HTTP错误具有> = 400的状态码。请注意,errorResponse参数中可能不提供服务器响应的内容。
//如【502  utf-8  text/html】【http://www.dy2018.com/favicon.ico  404    text/html】
Log.i("bqt", "【onReceivedHttpError】" + request.getUrl().toString() + "  " + errorResponse.getStatusCode()
+ "  " + errorResponse.getEncoding() + "  " + errorResponse.getMimeType());
super.onReceivedHttpError(view, request, errorResponse);
}
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
//HTTP错误具有> = 400的状态码。请注意,errorResponse参数中可能不提供服务器响应的内容。
//如,点击12306中的购票时【https://kyfw.12306.cn/otn/  3  Issued to: CN=kyfw.12306.cn,***】
Log.i("bqt", "【onReceivedSslError】" + error.getUrl() + "  " + error.getPrimaryError() + "  " + error.getCertificate().toString());
if (new Random().nextBoolean()) super.onReceivedSslError(view, handler, error);//默认行为,取消加载
else handler.proceed();//忽略错误继续加载
}
@Override
public void onScaleChanged(WebView view, float oldScale, float newScale) {
//应用程序可以处理改事件,比如调整适配屏幕
Log.i("bqt", "【onScaleChanged】" + "oldScale=" + oldScale + "  newScale=" + newScale);
super.onScaleChanged(view, oldScale, newScale);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
//每一次请求资源时都会回调。如果我们需要改变网页的背景,可以在这里处理。
//如果返回值为null,则WebView会照常继续加载资源。 否则,将使用返回的响应和数据。
Log.i("bqt", "【shouldInterceptRequest】" + request.getUrl().toString() + "  " + request.getMethod());
if (new Random().nextBoolean()) return super.shouldInterceptRequest(view, request);
else if (request.getUrl().toString().endsWith("你妹的.jpg")) {
try {
return new WebResourceResponse("text/html", "UTF-8", view.getContext().getAssets().open("icon.jpg"));
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
@Override
public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {
//给主机应用程序一次同步处理键事件的机会。如果应用程序想要处理该事件则返回true,否则返回false。
Log.i("bqt", "【shouldOverrideKeyEvent】" + event.getAction() + "  " + event.getKeyCode());
return super.shouldOverrideKeyEvent(view, event);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
//貌似都还是调用的废弃的那个方法
Log.i("bqt", "【shouldOverrideUrlLoading】" + request.getUrl().toString() + "  " + request.getMethod());
return super.shouldOverrideUrlLoading(view, request);
}
@SuppressWarnings("deprecation")
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
boolean b = new Random().nextBoolean();
Log.i("bqt", "【shouldOverrideUrlLoading废弃方法】" + b + "  " + url);
//识别电话、短信、邮件等
if (url.startsWith(WebView.SCHEME_TEL) || url.startsWith("sms:") || url.startsWith(WebView.SCHEME_MAILTO)) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(url));
view.getContext().startActivity(intent);
return true;
}
if (b) return super.shouldOverrideUrlLoading(view, url);//没必要折腾,只要设置了WebViewClient,使用默认的实现就行!
else {
view.loadUrl(url);//不去调用系统浏览器, 而是在本WebView中跳转
return true;
}
}
}