WebView#shouldOverrideUrlLoading究竟要返回true还是false

11,964 阅读5分钟

各大教程的常见做法

初接触Android WebView的萌新,在博览前人代码的时候都会看到,WebView控件并不是一初始化后就直接loadUrl的,这其中还有一系列的初始化设置。

比如:如果没有给WebView设置WebViewClient,那么,你点击网页内的a链接,该链接会跳转到系统本身的浏览器去打开,而如果想要自己处理这个a链接,就要设置自定义的WebViewClient实例,并且实现shouldOverrideUrlLoading方法。如何实现呢,网上常见的实现方式如下:

    @Override
    public boolean shouldOverrideUrlLoading(WebView webView, String url) {
        webView.loadUrl(url);
        return true;
    }

这样,就可以完美的让a链接在自己的WebVie里面加载啦。

返回值为什么是boolean类型

看到这里,也许你会想考究一下这个boolean返回值的作用。

boolean返回值的方法在Android SDK里很常见,比如View#dispatchKeyEvent方法的返回值就是boolean,在实现该方法时,如果你返回true则表示开发者自己来处理这个keyevent,让系统不要再往下一个View传播了;返回false则表示你不处理,让系统接着把事件告诉下一个View吧。

那么,WebView#shouldOverrideUrlLoading的返回值也是这个作用吗?

返回true的作用我们已经证实了,确实是表示开发者自己处理了这个url的加载。那么返回false试试呢?

@Override
    public boolean shouldOverrideUrlLoading(WebView webView, String url) {
        return false;
    }

编译运行,发现和前一种处理方式的结果一样,WebView自己来加载a链接。找找源码,发现shouldOverrideUrlLoading的注释里有这么一句:

    /**
     * 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.
     */

就是说:如果没有设置WebViewClient实例,那么就会由系统默认浏览器来处理url,如果设置了WebViewClient且本方法返回true,就表示已经处理了这个url;如果返回false,就表示让当前webview来处理url。如此看来,这个模式和事件机制还是很类似的,是一个责任链模式,如果你WebViewClient不想处理,那就把事件传给WebView,WebView的处理方式就是加载。

另外可以看到shouldOverrideUrlLoading本身的返回值就是false,也就是说,其实根本用不着自定义这个方法,传一个原生的WebViewClient实例给WebView,就能愉快的让WebView自己处理所有a链接跳转啦!

    WebView webView = new WebView(context);
    webView.setWebViewClient(new WebViewClient());
    webview.loadUrl("http://www.uc.cn");

至于为什么网上教程里基本上都是写return true的那种处理方法,这个,笔者自然也不得而知了。但是既然跟到了这里,不妨更加深入一点。到底两个处理方式有没有区别呢?

shouldOverrideUrlLoading详解

什么时候会有shouldOverrideUrlLoading回调?
在网页内点击一个a链接,会回调;
302跳转前,会回调;
用JavaScript里的location.href来跳转,会回调;
主动调用WebView.loadUrl方法来加载url,不会回调。
...
如果枚举下去,情况太多。所以还是看代(zhu)码(shi)吧。

/**
     * Give the host application a chance to take over the control when a new
     * url is about to be loaded in the current WebView.
     ...
     * This method is not called for requests using the POST "method".
*/

“给你的宿主app一个机会来控制‘当前WebView将要加载新的url’的情况”——简单来说,就是除了你主动用webview加载的url,其他的加载都会回调shouldOverrideLoading。
当然,后面还有一句注释:post请求不会回调shouldOverrideLoading。这个就很明显是为的数据安全考虑了。

区别

终于要说到区别了,这个区别的情况也是笔者偶然间才发现的。
前端有时候会在html的<head>节点里用JavaScript做一些判断,然后跳转到另一个url。比如,我们有一个a网页,它的源代码如下:

<html>
    <head>
        <script>
            location.href = "http://www.uc.cn"
        </script>
    </head>
</html>

这个跳转当然可以在shouldOverrideUrlLoading里面捕获到,然而,这个时候,用true还是false的方式来处理,就会出现区别了。
如果用主动loadUrl来实现,并且返回true,那么,a网页会被加入到WebView的历史记录中,也就是说,跳转后你按返回键,是可以回到a网页的,当然,这个时候又会触发js里面的跳转,跳去了“http://www.uc.cn”。
如果用直接return false的方式来实现呢。a网页是不会被加入WebView的历史记录的,返回键也无法回到a网页,彷佛它从来没有出现过。

产生这种区别的原因是什么?这就关系到WebView的底层的实现了。location.href被重置时,会调用JNI层的NavigationScheduler类

bool NavigationScheduler::MustReplaceCurrentItem(LocalFrame* target_frame) {
  // Non-user navigation before the page has finished firing onload should not
  // create a new back/forward item. See https://webkit.org/b/42861 for the
  // original motivation for this.
  if (!target_frame->GetDocument()->LoadEventFinished() &&
      !Frame::HasTransientUserActivation(target_frame))
    return true;

  // Navigation of a subframe during loading of an ancestor frame does not
  // create a new back/forward item. The definition of "during load" is any time
  // before all handlers for the load event have been run. See
  // https://bugs.webkit.org/show_bug.cgi?id=14957 for the original motivation
  // for this.
  Frame* parent_frame = target_frame->Tree().Parent();
  return parent_frame && parent_frame->IsLocalFrame() &&
         !ToLocalFrame(parent_frame)->Loader().AllAncestorsAreComplete();
}

当这个方法返回true时,location.href传的参数会替换掉当前url,也就是说,不留下历史记录。什么时候返回true呢?在主文档的onload event发生之前改动href,就会返回true。我们的例子中的js是在<head>里面执行的,属于这一类情况。
所以,当我们在shouldOverrideUrlLoading里面直接返回false,让WebView使用自己默认的方式来加载,就不会有历史记录存在了。
然而,当我们通过主动调用loadUrl并返回true的方式来干预加载时,WebView会先执行完a网页的加载(发生了onload event)之后,再执行loadUrl加载新的url,如此,a网页就被加入到历史记录里面了。

基于此,笔者的建议是,shouldOverrideUrlLoading回调时,如果你不需要做其他的特殊操作,仅仅只是要加载新url的话,可以直接return false,让系统WebView以自己的方式来加载url,会更健壮。