#### 基础使用目录介绍 - 11.什么是302/303重定向 - 12.301/302业务场景白屏描述 - 13.301/302业务白屏解决方案 - 14.301/302回退栈问题描述 - 15.301/302回退栈问题解决方案1 - 16.301/302回退栈问题解决方案2 - 17.301/302回退栈问题解决方案3 - 18.如何用代码判断是否重定向 - 19.shouldOverrideUrlLoading - 20.重定向终极优雅解决方案 ### 12.301/302业务场景白屏描述 - 业务场景问题 - 对于需要对url进行拦截以及在url中需要拼接特定参数的WebView来说,301和302发生的情景主要有以下几种: - 首次进入,有重定向,然后直接加载H5页面,如http跳转https - 首次进入,有重定向,然后跳转到native页面,如扫一扫短链,然后跳转到native - 二次加载,有重定向,跳转到native页面 - 类似登录后跳转到某个页面的需求。如我的拼团,未登录状态下点击我的拼团跳转到登录页面,登录完成后再加载我的拼团页 - 遇到问题分析 - 第一种情况属于正常情况,暂时没遇到什么坑。 - 第二种情况,会遇到WebView空白页问题,属于原始url不能拦截到native页面,但301/302后的url拦截到native页面的情况,当遇到这种情况时,需要把WebView对应的Activity结束,否则当用户从拦截后的页面返回上一个页面时,是一个WebView空白页。 - 第三种情况,也会遇到WebView空白页问题,原因在于加载的第一个页面发生了重定向到了第二个页面,第二个页面被客户端拦截跳转到native页面,那么WebView就停留在第一个页面的状态了,第一个页面显然是空白页。 - 第四种情况,会遇到无限加载登录页面的问题。 - 为何会出现白屏 - webView自带的背景就是白色的 ### 13.301/302回退栈问题描述 - 无论是哪种重定向场景,都不可避免地会遇到回退栈的处理问题,如果处理不当,用户按返回键的时候不一定能回到重定向之前的那个页面。很多开发者在覆写WebViewClient.shouldOverrideUrlLoading()方法时,会简单地使用以下方式粗暴处理: ``` WebView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); return true; } ) ``` - 这种方法最致命的弱点就是如果不经过特殊处理,那么按返回键是没有效果的,还会停留在302之前的页面。现有的解决方案无非就几种: - 手动管理回退栈,遇到重定向时回退两次。 - 通过HitTestResult判断是否是重定向,从而决定是否自己加载url。具体看:16.301/302回退栈问题解决方案2 - 通过设置标记位,在onPageStarted和onPageFinished分别标记变量避免重定向。具体看:17.301/302回退栈问题解决方案3 - 通过用户的touch事件来判断重定向。这个看:15.301/302回退栈如何处理1 - 这几种解决方案都不是完美的,都有缺陷。 ### 15.301/302回退栈如何处理1 - 在提供解决方案之前,我们需要了解一下shouldOverrideUrlLoading方法的返回值代表什么意思。 - 简单地说,就是返回true,那么url就已经由客户端处理了,WebView就不管了,如果返回false,那么当前的WebView实现就会去处理这个url。 - WebView能否知道某个url是不是301/302呢?当然知道,WebView能够拿到url的请求信息和响应信息,根据header里的code很轻松就可以实现,事实正是如此,交给WebView来处理重定向(return false),这时候按返回键,是可以正常地回到重定向之前的那个页面的。(PS:从上面的章节可知,WebView在5.0以后是一个独立的apk,可以单独升级,新版本的WebView实现肯定处理了重定向问题) - 但是,业务对url拦截有需求,肯定不能把所有的情况都交给系统WebView处理。为了解决url拦截问题,本文引入了另一种思想——通过用户的touch事件来判断重定向。下面通过代码来说明。 - 核心代码如下所示,具体代码见本案例中的ScrollWebView类代码 ``` @Override public void setWebViewClient(final WebViewClient client) { super.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { boolean handleByChild = null != client && client.shouldOverrideUrlLoading(view, url); if (handleByChild) { // 开放client接口给上层业务调用,如果返回true,表示业务已处理。 return true; } else if (!isTouchByUser()) { // 如果业务没有处理,并且在加载过程中用户没有再次触摸屏幕,认为是301/302事件,直接交由系统处理。 return super.shouldOverrideUrlLoading(view, url); } else { //否则,属于二次加载某个链接的情况,为了解决拼接参数丢失问题,重新调用loadUrl方法添加固有参数。 loadUrl(url); return true; } } @RequiresApi(api = Build.VERSION_CODES.N) @Override public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { boolean handleByChild = null != client && client.shouldOverrideUrlLoading(view, request); if (handleByChild) { return true; } else if (!isTouchByUser()) { return super.shouldOverrideUrlLoading(view, request); } else { loadUrl(request.getUrl().toString()); return true; } } }); } ``` - 如何在android中的webView上获得onclick事件 - WebView似乎没有发送点击事件OnClickListener - 如何设置触摸事件 ### 16.301/302回退栈问题解决方案2 - WebView有一个getHitTestResult():返回的是一个HitTestResult,一般会根据打开的链接的类型,返回一个extra的信息 - 如果打开链接不是一个url,或者打开的链接是JavaScript的url,他的类型是UNKNOWN_TYPE,这个url就会通过requestFocusNodeHref(Message)异步重定向。 - 返回的extra为null,或者没有返回extra。根据此方法的返回值,判断是否为null,可以用于解决网页重定向问题。 - requestFocusNodeHref的意思是:请求焦点节点Href - 代码如下所示 ``` @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { WebView.HitTestResult hitTestResult = view.getHitTestResult(); //hitTestResult==null解决重定向问题 if (!TextUtils.isEmpty(url) && hitTestResult == null) { view.loadUrl(url); return true; } return super.shouldOverrideUrlLoading(view, url); } ``` ### 17.301/302回退栈问题解决方案3 - 页面被重定向了会走怎样的步骤呢,首先会回调shouldOverrideUrlLoading,然后回调onPageStarted,最后走onPageFinished。 - 据此我们可以修改代码如下,增加一个计数器判断当前的onPageFinished - 代码如下所示 ``` mWebView.setWebViewClient(new WebViewClient() { int running = 0; @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); running++; return true; } @Override public void onPageStarted(WebView webView, String s, Bitmap bitmap) { super.onPageStarted(webView, s, bitmap); running = Math.max(running, 1); } @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); if (--running==0) { setWebViewHeight(); } } }); ``` ### 18.如何用代码判断是否重定向 - 遇到问题说明 - 用WebView调用webView.loadUrl(url)加载某个网页。点击WebView的某个链接跳转下一个页面。但是下面这样做,会出现问题; - 如果webView第一次加载的url重定向到了另一个地址,此时也会走shouldOverrideUrlLoading的回调。这样一来,出现的现象就是WebView是空的,直接打开了浏览器。 ``` webView.setWebViewClient(new WebViewClient(){ @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { Uri uri = Uri.parse(url); Intent intent = new Intent(Intent.ACTION_VIEW, uri); try { view.getContext().startActivity(intent); } catch (ActivityNotFoundException e) { e.printStackTrace(); } return true; } }); ``` - 解决问题分析思考 - 要解决这个问题,很容易想到的解决方案是找找WebView有没有对重定向的判断方法,如果有的话,我们就可以对重定向的回调另外处理。 - 很不幸,WebView并没有提供相应的方法。是不是就没办法处理了呢?当然不是。 - 第一种解决方案案例 - WebView有一个getHitTestResult():返回的是一个HitTestResult,一般会根据打开的链接的类型,返回一个extra的信息,如果打开链接不是一个url,或者打开的链接是JavaScript的url,他的类型是UNKNOWN_TYPE,这个url就会通过requestFocusNodeHref(Message)异步重定向。返回的extra为null,或者没有返回extra。根据此方法的返回值,判断是否为null,可以用于解决网页重定向。 - 该方案有个缺陷是,如果遇到的需求是前言描述的那样,二正好点击的链接发生了重定向,就不会在另一个页面打开,而是直接在当前的WebView里了。 ``` webView.setWebViewClient(new WebViewClient(){ @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { //判断重定向的方式一 WebView.HitTestResult hitTestResult = view.getHitTestResult(); if(hitTestResult == null) { return false; } if(hitTestResult.getType() == WebView.HitTestResult.UNKNOWN_TYPE) { return false; } Uri uri = Uri.parse(url); Intent intent = new Intent(Intent.ACTION_VIEW, uri); try { view.getContext().startActivity(intent); } catch (ActivityNotFoundException e) { e.printStackTrace(); } return true; } }); ``` - 第二种解决方案案例 - WebView在加载一个页面开始的时候会回调onPageStarted方法,在该页面加载完成之后会回调onPageFinished方法。而如果该链接发生了重定向,回调shouldOverrideUrlLoading会在回调onPageFinished之前。 - 有了这个前提,我们就可以加一个mIsPageLoading的标记,在onPageStarted回调的时候置为true,在onPageFinished回调的时候置为false。在shouldOverrideUrlLoading里面就可以判断该标记,如果为true,则表示该回调是重定向,否则直接打开浏览器。代码如下: ``` private boolean mIsPageLoading; //代码省略 webView.setWebViewClient(new WebViewClient(){ @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { //判断重定向的方式二 if(mIsPageLoading) { return false; } if(url != null && url.startsWith("http")) { webView.loadUrl(url); return true; } else { Uri uri = Uri.parse(url); Intent intent = new Intent(Intent.ACTION_VIEW, uri); try { view.getContext().startActivity(intent); } catch (ActivityNotFoundException e) { e.printStackTrace(); } return true; } } @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { super.onPageStarted(view, url, favicon); mIsPageLoading= true; Log.d(TAG, "onPageStarted"); } @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); mIsPageLoading= false; Log.d(TAG, "onPageFinished"); } }); ``` - 该方案也会产生另一个问题:当页面没有全部加载完之前,加载出来的部分页面的链接也是可以点击的。这样一来在shouldOverrideUrlLoading里面本来是对链接的点击也会被当成重定向链接在当前的WebView里面打开。 - 要避免这种情况,只能在页面完全加载出来之前禁止WebView的点击。 ``` webView.setOnTouchListener(new WebViewTouchListener()); //代码省略 private class WebViewTouchListener implements View.OnTouchListener { @Override public boolean onTouch(View v, MotionEvent event) { return !mIsLoading; } } ``` ### 19.shouldOverrideUrlLoading - 首先看一下shouldOverrideUrlLoading方法 - 这个方法中可以做拦截,主要的作用是处理各种通知和请求事件。 - 返回值是true的时候控制去WebView打开,为false调用系统浏览器或第三方浏览器。 - 有哪些几个方法,之间有何区别 - boolean shouldOverrideUrlLoading(WebView view, String url) - boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) - 具体看 - https://mp.weixin.qq.com/s?__biz=MzA5MzI3NjE2MA==&mid=2650237226&idx=1&sn=d7d434b8644bb9543485ce81226125e5&chksm=88639845bf141153b265ee26b39aa8a2ef74248e010b568b288cffc0726012f8bce6f5a675bf&scene=38#wechat_redirect ### 20.重定向终极优雅解决方案 - 需要准备的条件 - 创建一个栈,主要是用来存取和移除url的操作。这个url包括所有的请求链接 - 定义一个变量,用于判断页面是否处于正在加载中。 - 定义一个变量,用于记录重定向前的链接url - 定一个重定向时间间隔,主要为了避免刷新造成循环重定向 - 具体怎么操作呢 - 在执行onPageStarted时,先移除栈中上一个url,然后将url加载到栈中。 - 当出现错误重定向的时候,如果和上一次重定向的时间间隔大于3秒,则reload页面。 - 在回退操作的时候,判断如果可以回退,则从栈中获取最后停留的url,然后loadUrl。即可解决回退问题。 - 具体的代码如下所示 ``` /** * 记录上次出现重定向的时间. * 避免由于刷新造成循环重定向. */ private long mLastRedirectTime = 0; /** * 默认重定向间隔. * 避免由于刷新造成循环重定向. */ private static final long DEFAULT_REDIRECT_INTERVAL = 3000; /** * URL栈 */ private final Stack mUrlStack = new Stack<>(); /** * 判断页面是否加载完成 */ private boolean mIsLoading = false; /** * 记录重定向前的链接 */ private String mUrlBeforeRedirect; /** * 太多的重定向错误 */ private static int ERR_TOO_MANY_REDIRECTS = -9; @Override public void onPageStarted(WebView webView, String url, Bitmap bitmap) { super.onPageStarted(webView, url, bitmap); if (mIsLoading && mUrlStack.size() > 0) { mUrlBeforeRedirect = mUrlStack.pop(); } recordUrl(url); mIsLoading = true; } @Override public void onPageFinished(WebView view, String url) { if (mIsLoading) { mIsLoading = false; } } private void recordUrl(String url) { if (!TextUtils.isEmpty(url) && !url.equals(getUrl())) { if (!TextUtils.isEmpty(mUrlBeforeRedirect)) { mUrlStack.push(mUrlBeforeRedirect); mUrlBeforeRedirect = null; } } } @Nullable public String getUrl() { //peek方法,查看此堆栈顶部的对象,而不将其从堆栈中删除。 return mUrlStack.size() > 0 ? mUrlStack.peek() : null; } /** * 回退操作 * @param webView webView * @return */ public final boolean pageGoBack(@NonNull WebView webView) { //判断是否可以回退操作 if (pageCanGoBack()) { //获取最后停留的页面url final String url = popBackUrl(); //如果不为空 if (!TextUtils.isEmpty(url)) { webView.loadUrl(url); return true; } } return false; } ```