WebView拦截history.back的正确姿势

3,974 阅读3分钟

一、问题场景

总的来说,Webview的页面加载可分为四种流程。

  • 正常流程: normal.png
  • 重定向(302)流程: redirect.png
  • 内部跳转(location.href)流程: location href.png
  • 历史栈回退(history.back)流程:

history.back.png

其中,历史栈回退流程比较特殊。这是因为,历史栈回退首先触发的是shouldInterceptRequest,而非loadUrlshouldOverrideUrlLoading

private String loadUrl;
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    loadUrl = url;
    return false;
}


@Override
public void loadUrl(String url) {
  loadUrl = url;
  super.loadUrl(url);
}

public WebResourceResponse shouldInterceptRequest(WebView view, final WebResourceRequest request) {
    String url = request.getUrl().toString();
    if (loadUrl.equals(url)) {
        //进行拦截处理
    }
    return null;
}

这使得对WebView页面加载进行拦截处理时,由于缺少了loadUrlshouldOverrideUrlLoading提前告知页面loadUrl的过程,而页面(loadUrl)、各种资源(js、css、jpg等)以及业务请求(ajax)都会先在shouldInterceptRequest中回调后再执行加载,从而无法确定当前要加载的url是不是需要拦截的loadUrl,变得非常棘手。

如果一定要对history.back进行拦截,那该怎么办呢?经过一番折腾终于解决了问题,鉴于网上(google keywords: webview dectect history.back)也有一些同行遇到了相同问题,却未看到有人给出行之有效的解决方案,于是将在下文撰写我的解决办法。

二、WebView栈列表

WebView提供了一个获取栈列表WebBackForwardList的方法,

copyBackForwardList()

WebBackForwardList源码的javadoc描述里非常清晰地说明了这点。

/**
 * This class contains the back/forward list for a WebView.
 * WebView.copyBackForwardList() will return a copy of this class used to
 * inspect the entries in the list.
 */
public abstract class WebBackForwardList implements Cloneable, Serializable {
    /**
     * Return the current history item. This method returns {@code null} if the list is
     * empty.
     * @return The current history item.
     */
    @Nullable
    public abstract WebHistoryItem getCurrentItem();

    /**
     * Get the index of the current history item. This index can be used to
     * directly index into the array list.
     * @return The current index from 0...n or -1 if the list is empty.
     */
    public abstract int getCurrentIndex();

    /**
     * Get the history item at the given index. The index range is from 0...n
     * where 0 is the first item and n is the last item.
     * @param index The index to retrieve.
     */
    public abstract WebHistoryItem getItemAtIndex(int index);

    /**
     * Get the total size of the back/forward list.
     * @return The size of the list.
     */
    public abstract int getSize();

    /**
     * Clone the entire object to be used in the UI thread by clients of
     * WebView. This creates a copy that should never be modified by any of the
     * webkit package classes. On Android 4.4 and later there is no need to use
     * this, as the object is already a read-only copy of the internal state.
     */
    protected abstract WebBackForwardList clone();
}

我们可以利用WebBackForwardList获取WebView的栈历史快照WebHistoryItem对象,这个对象里提供了获取页面url的方法。

/**
 * A convenience class for accessing fields in an entry in the back/forward list
 * of a WebView. Each WebHistoryItem is a snapshot of the requested history
 * item.
 * @see WebBackForwardList
 */
public abstract class WebHistoryItem implements Cloneable {
  ...
    /**
     * Return the url of this history item. The url is the base url of this
     * history item. See getTargetUrl() for the url that is the actual target of
     * this history item.
     * @return The base url of this history item.
     */
    public abstract String getUrl();

 ...
}

三、解决方案

遍历栈列表获取各个栈快照对象,并解析得到一个栈url列表,值得注意的是,copyBackForwardList()getCurrentIndex()需要在WebView创建线程(主线程)调用,而shouldInterceptRequest是在子线程中回调的,从而updateBackForwardList()不能直接shouldInterceptRequest中调用,也不建议通过线程阻塞的方式在shouldInterceptRequest中等待url栈解析完再进行拦截判断,毕竟除了要拦截的loadUrl,还有大量其他的请求也会在这里回调执行判断。

private List<String> backForwardUrlList = new ArrayList<>();

private void updateBackForwardList() {
    backForwardUrlList.clear();
    WebBackForwardList backForwardList = copyBackForwardList();
    int currentIndex = backForwardList.getCurrentIndex();
    for (int index = 0; index <= currentIndex; index++) {
        WebHistoryItem webHistoryItem = backForwardList.getItemAtIndex(index);
        backForwardUrlList.add(webHistoryItem.getUrl());
    }
}

推荐在onPageFinished()执行完后,调用updateBackForwardList()解析url栈,一方面onPageFinished()本身是在主线程中回调的,另一方面这时页面加载完毕,不会影响页面加载性能。

@Override
public void onPageFinished(WebView view, final String url) {
    super.onPageFinished(view, url);
  ...//业务逻辑
    updateBackForwardList();
}

接下来就可以很方便地进行拦截判断了,如果拦截的url在栈url列表中存在,就说明js执行了history.backhistory.forward执行流程相同),需要做拦截处理。

public WebResourceResponse shouldInterceptRequest(WebView view, final WebResourceRequest request) {
    String url = request.getUrl().toString();
    if (loadUrl.equals(url) || backForwardUrlList.contains(url)) {
        //进行拦截处理
    }
    return null;
}