Webview 整理笔记

  • 初始化操作及常见用法
  • WebSettings、WebViewClient、WebChromeClient 的常用方法
  • 优化等相关扩展
  • 302 白屏问题
  • JS 的通讯

相关参考:

简单的初始化操作

  • 用 webview 读取 url 是默认在系统的浏览器打开的,若要在webview中打开
webview.setWebViewClient(new WebViewClient(){
      @Override
      public boolean shouldOverrideUrlLoading(WebView view, String url) {
          view.loadUrl(url);
          return true;
      }
      @Override
      public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
          //上面的方法被标记过时
          view.loadUrl(String.valueOf(request.getUrl()));
          return true;
      }
  });
  • 按返回键的时候,默认是退出activity,这里设置为返回上一个网页
public boolean onKeyDown(int keyCode, KeyEvent event) {       
        if ((keyCode == KeyEvent.KEYCODE_BACK) &&   mWebView .canGoBack()) {       
            webview.goBack();       
            return true;       
        }       
        return super.onKeyDown(keyCode, event);       
    }
  • 如果webView中需要用户手动输入用户名、密码或其他,则 webview 必须设置支持获取手势焦点。
webview.requestFocusFromTouch();
  • 还有一些常见的方法
/**
  * 判断 WebView 当前是否可以返回上一页
  * @return true iff this WebView has a back history item
  */
public boolean canGoBack();

/**
  * Goes back in the history of this WebView.
  */
public goBack()//回退到上一页

//判断 WebView 当前是否可以向前一页
public boolean canGoForward()

//回退到前一页
public void goForward()

/**
  * 类似 Activity 生命周期,页面进入后台不可见状态
  * Does a best-effort attempt to pause any processing that can be paused
  * safely, such as animations and geolocation. Note that this call
  * does not pause JavaScript. To pause JavaScript globally, use
  * {@link #pauseTimers}.
  *
  * To resume WebView, call {@link #onResume}.
  */
public void onPause()

//在调用 onPause()后,可以调用该方法来恢复 WebView 的运行。
public void onResume();

/**
  * 该方法面向全局整个应用程序的 webview
  * 它会暂停所有 webview 的 layout,parsing,JavaScript Timer
  * Pauses all layout, parsing, and JavaScript timers for all WebViews. This
  * is a global requests, not restricted to just this WebView. This can be
  * useful if the application has been paused.
  */
public void pauseTimers();

//恢复 pauseTimers() 的所有操作
//pauseTimers和resumeTimers 方法必须一起使用,否则再使用其它场景下的 WebView 会有问题
public void resumeTimers();

//销毁 WebView
public void destroy();
//清除当前 WebView 访问的历史记录。
public void clearHistory();

/**
  * 清空缓存数据,因为 WebView 的缓存是全局的,所有的 WebView 都会被影响。
  * 
  * Clears the resource cache. Note that the cache is per-application, so
  * this will clear the cache for all WebViews used.
  * @param includeDiskFiles if false,只清空内存里的资源缓存,而不清空磁盘里的
  * @param includeDiskFiles if false, only the RAM cache is cleared
  */
public void clearCache(boolean includeDiskFiles);

//重新加载当前请求
public void reload();
//设置硬件加速、软件加速
public void setLayerType(int layerType, Paint paint);
//清除子view
public void removeAllViews();

销毁 WebView

一般而言销毁 WebView 仅调用 destroy() 方法是不足够的:

@Override
public void destroy()
    //flushMessageQueue();
    clearCache(true);
    clearFormData();
    clearMatches();
    clearSslPreferences();
    clearDisappearingChildren();
    clearHistory();
    //@Deprecated
    //clearView();
    clearAnimation();
    loadUrl("about:blank");
    removeAllViews();
    freeMemory();
    super.destroy();
}

为 WebView 自定义错误显示界面:

/**
 * 显示自定义错误提示页面,用一个 View 覆盖在 WebView
 */
  protected void showErrorPage() {
       //获取父控件的实例,用于添加新的 View
       LinearLayout webParentView = (LinearLayout)mWebView.getParent();

       initErrorPage();
       //去掉 WebView 中的内容
       while (webParentView.getChildCount() > 1) {
           webParentView.removeViewAt(0);
       }
       //要显示的 ErrorView
       LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LayoutParams.FILL_PARENT,LayoutParams.FILL_PARENT);
       //显示 ErrorView
       webParentView.addView(mErrorView, 0, lp);
       mIsErrorPage = true;
   }

   protected void hideErrorPage() {
       LinearLayout webParentView = (LinearLayout)mWebView.getParent();

       mIsErrorPage = false;
       while (webParentView.getChildCount() > 1) {
           webParentView.removeViewAt(0);
       }
   }
   //初始化要显示的内容
   protected void initErrorPage() {
       if (mErrorView == null) {
           mErrorView = View.inflate(this, R.layout.online_error, null);
           Button button = (Button)mErrorView.findViewById(R.id.online_error_btn_retry);
           button.setOnClickListener(new OnClickListener() {
               public void onClick(View v) {
                   mWebView.reload();
               }
           });
           mErrorView.setOnClickListener(null);
       }
   }

Cookie && Cache

  • WebView cookies清理
CookieSyncManager.createInstance(this);
CookieSyncManager.getInstance().startSync();
CookieManager.getInstance().removeSessionCookie();
  • 清理cache 和历史记录
webView.clearCache(true);
webView.clearHistory();

判断 WebView 是否已经滚动到页面底端:

getScrollY() 返回的是当前可见区域的顶端距整个页面顶端的距离,也就是当前内容滚动的距离
getHeight()/getBottom() 返回当前 WebView 这个容器的高度
getContentHeight() 返回的是整个html 的高度但并不等同于当前整个页面的高度,因为WebView 有缩放功能
所以当前整个页面的高度实际上应该是原始 html 的高度再乘上缩放比例

if(WebView.getContentHeight*WebView.getScale() == (webview.getHeight()+WebView.getScrollY())){
      //已经处于底端
 }

加载图片时的自适应设置

用 WebSettings 设置:

/**
  * NARROW_COLUMNS:可能的话使所有列的宽度不超过屏幕宽度
  * NORMAL:正常显示不做任何渲染 
  * SINGLE_COLUMN:把所有内容放大webview等宽的一列中
  */
WebSettings settings = webView.getSettings();
settings.setLayoutAlgorithm(LayoutAlgorithm.SINGLE_COLUMN);

但是以上方法在不同的版本、机型中总是不如人意,所以还可以用 js 来设置:

//首先要重写一个 WebViewClient,以及调用 js 来设置图片的宽度
private class MyWebViewClient extends WebViewClient {

        @Override
        public void onPageFinished(WebView view, String url) {   
            super.onPageFinished(view, url);
            //html加载完成之后,调用js的方法         
            imgReset();
        }

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

private void imgReset() {
      //获取所有的图片实例,设置宽度为 100%
      webView.loadUrl("javascript:(function(){"
              + "var objs = document.getElementsByTagName('img'); "
              + "for(var i=0;i<objs.length;i++)  " + "{"
              + "var img = objs[i];   "
              + "    img.style.width = '100%';   " 
              + "    img.style.height = 'auto';   "
              + "}" + "})()");
}

//然后在 WebView 初始化的时候设置上面的 WebViewClient
webView.setWebViewClient(new MyWebViewClient());


URL 拦截:

Android WebView 是拦截不到页面内的 fragment 跳转的。但是 url 跳转的话,又会引起页面刷新,H5 页面的体验又下降了。只能给 WebView 注入 JS 方法了。

处理 WebView 中的非超链接请求(如 Ajax 请求)

有时候需要加上请求头,但是非超链接的请求,没有办法在 shouldOverrinding 中拦截并用webView.loadUrl(String
url,HashMap headers) 方法添加请求头
目前用了一个临时的办法解决:

首先需要在 url 中加特殊标记/协议,
如在 onWebViewResource 方法中拦截对应的请求,然后将要添加的请求头,以 get 形式拼接到 url 末尾
在 shouldInterceptRequest() 方法中,可以拦截到所有的网页中资源请求,比如加载 JS,图片以及 Ajax 请求等等

@SuppressLint("NewApi")
@Override
public WebResourceResponse shouldInterceptRequest(WebView view,String url) {
    // 非超链接(如Ajax)请求无法直接添加请求头,现拼接到url末尾,这里拼接一个imei作为示例

    String ajaxUrl = url;
    // 如标识:req=ajax
    if (url.contains("req=ajax")) {
       ajaxUrl += "&imei=" + imei;
    }

    return super.shouldInterceptRequest(view, ajaxUrl);

}

在页面中先显示图片:

@Override
public void onLoadResource(WebView view, String url) {
  mEventListener.onWebViewEvent(CustomWebView.this, OnWebViewEventListener.EVENT_ON_LOAD_RESOURCE, url);
    if (url.indexOf(".jpg") > 0) {
     hideProgress(); //请求图片时即显示页面
     mEventListener.onWebViewEvent(CustomWebView.this, OnWebViewEventListener.EVENT_ON_HIDE_PROGRESS, view.getUrl());
     }
    super.onLoadResource(view, url);
}

屏蔽掉长按事件,因为 webview 长按时将会调用系统的复制控件:

mWebView.setOnLongClickListener(new OnLongClickListener() {
          @Override
          public boolean onLongClick(View v) {
              return true;
          }
      });

在WebView加入 flash支持:

String temp = "<html><body bgcolor=\"" + "black"
                + "\"> <br/><embed src=\"" + url + "\" width=\"" + "100%"
                + "\" height=\"" + "90%" + "\" scale=\"" + "noscale"
                + "\" type=\"" + "application/x-shockwave-flash"
                + "\"> </embed></body></html>";
String mimeType = "text/html";
String encoding = "utf-8";
web.loadDataWithBaseURL("null", temp, mimeType, encoding, "");

WebSettings 的常用方法介绍

WebSettings webSettings =  webview.getSettings();       

setJavaScriptEnabled(true);  //支持js

setPluginsEnabled(true);  //支持插件
 
/*两者基本一起设置来适应屏幕*/
setUseWideViewPort(false);  //将图片调整到适合 webview 的大小 
setLoadWithOverviewMode(true); // 网页自适应屏幕

setSupportZoom(true);  //支持缩放变焦 
setBuiltInZoomControls(false);  //设置是否支持缩放

setLayoutAlgorithm(LayoutAlgorithm.SINGLE_COLUMN); //支持内容重新布局  

supportMultipleWindows();  //多窗口 

setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);  //优先使用webview中缓存
setCacheMode(WebSettings.LOAD_NO_CACHE)//关闭webview中缓存 

setAllowFileAccess(true);  //设置可以访问文件 

setNeedInitialFocus(true); //当webview调用requestFocus时为webview设置节点

webview webSettings.setBuiltInZoomControls(true); //设置支持缩放 

setJavaScriptCanOpenWindowsAutomatically(true); //支持通过JS打开新窗口 

setLoadsImagesAutomatically(true);  //支持自动加载图片

setBlockNetworkImage(true);   //是否显示网络图像

setDefaultFontSize(int size);  //设置默认的字体大小

setDefaultTextEncodingName(String encoding); //设置默认文字编码

setFixedFontFamily(String font); //设置固定使用的字体

setLightTouchEnabled(boolean enable); //设置用鼠标激活被选项

WebViewClient 的方法

以下是 WebViewClient 的一些主要方法及注释。

/**
  * 点击请求链接时调用,重写此方法返回 true 表明点击网页里面的链接还是在当前的 webview 里跳转,不跳到浏览器那边。
  * 这个函数我们可以做很多操作,比如我们读取到某些特殊的 URL,可以自定义自己的操作。
  * 当加载的网页需要重定向的时候就会回调这个函数告知我们应用程序是否需要接管控制网页加载,如果应用程序接管,并且 return true 意味着主程序接管网页加载,如果返回 false 让 webview 自己处理。
  * 当请求的方式是 POST 方式时这个回调是不会通知的。
  * @param  view 接收WebViewClient的那个实例
  * @param  url    即将要被加载的url
  * @return true 当前应用程序要自己处理这个 url, 返回 false 则不处理。
  */
shouldOverrideUrlLoading(WebView view, String url) 

/**
  * 当内核开始加载访问的url时,会通知应用程序,对每个 main frame 这个函数只会被调用一次
  * 页面包含iframe 或者 framesets 不会另外调用一次 onPageStarted,当网页内内嵌的 frame 发生改变时也不会调用onPageStarted。
  * 在网页加载过程中会先调用 onPageStarted 再调用 shouldOverrideUrlLoading
  * 当我们在打开的这个网址点击一个 link,这时候会先调用 shouldOverrideUrlLoading 再调用 onPageStarted。
  * 不过 shouldOverrideUrlLoading 不一定每次都被调用,只有需要的时候才会被调用。
  * @param view 接收 WebViewClient 的实例
  * @param url    即将要被加载的 url
  * @param favicon 如果这个 favicon 已经存储在本地数据库中,则会返回这个网页的 favicon,否则返回为null。
*/
onPageStarted(WebView view, String url, Bitmap favicon) 
 
/**
  * 当内核加载完当前页面时会通知我们的应用程序,这个函数只有在 main frame 情况下才会被调用,
  * 当调用这个函数之后,渲染的图片不会被更新。
  * 如果需要获得新图片的通知可以使用{@link WebView.PictureListener#onNewPicture}
  * 
  * Notify the host application that a page has finished loading. This method
  * is called only for main frame. When onPageFinished() is called, the
  * rendering picture may not be updated yet. To get the notification for the
  * new Picture, use {@link WebView.PictureListener#onNewPicture}.
  *
  * @param view The WebView that is initiating the callback.
  * @param url The url of the page.
  */
public void onPageFinished(WebView view, String url)

/*
  * 通知应用程序可以将当前的 url 存储在数据库中,意味着当前的访问 url 已经生效并被记录在内核当中。
  * 这个函数在网页加载过程中只会被调用一次。注意网页前进后退并不会回调这个函数。
  * @param view 接收WebViewClient的那个实例
  * @param url 当前正在访问的url 
  * @param isReload 如果是true 这个是正在被reload的url
  */
public void doUpdateVisitedHistory(WebView view, String url,boolean isReload) 

/**
  * 如果浏览器需要重新发送 POST 请求,可以通过这个时机来处理。默认是不重新发送数据。(应用程序重新请求网页数据)
  * @param view 接收 WebViewClient 的实例
  * @param dontResent 当浏览器不需要重新发送数据时,可以使用这个参数。
  * @param resent 当浏览器需要重新发送数据时, 可以使用这个参数。
  */
public void onFormResubmission(WebView view, Message dontResend,   Message resend) 

/**
  * 在加载页面资源时会调用,每一个资源(比如图片)的加载都会调用一次。
  * Notify the host application that the WebView will load the resource
  * specified by the given url.
  *
  * @param view The WebView that is initiating the callback.
  * @param url The url of the resource the WebView will load.
  */
public void onLoadResource(WebView view, String url) 

/**
  * 当浏览器访问发生错误时回调,比如网络错误。可以在这里自定义网页的错误页面 
  * @param view 接收 WebViewClient 的实例
  * @param errorCode 错误号可以在 WebViewClient.ERROR_* 里面找到对应的错误名称。
  * @param description 描述错误的信息
  * @param failingUrl  当前访问失败的 url,注意并不一定是我们主 url
  */
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl)

/**
  * 获取返回信息授权请求
  * 通知应用程序 WebView 接收到了一个 Http auth 的请求
  * 应用程序可以使用 supplied 设置 webview 的响应请求。默认行为是 cancel 本次请求。 
  * @param view 接收 WebViewClient 的实例
  * @param handler 用来响应 WebView 请求的对象
  * @param host  请求认证的 host
  * @param realm 认真请求所在的域
  */
public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm)

/**
  * 重写此方法可以让 webview 处理 https 请求。
  * 当网页加载资源过程中发现 SSL 错误会调用此方法。我们应用程序必须做出响应,
  * 是取消请求 handler.cancel(),还是继续请求 handler.proceed();内核的默认行为是 handler.cancel();
  * 内核会记住本次选择,如果下次还有相同的错误,内核会直接执行之前选择的结果。
  * @param view 接收 WebViewClient 的实例
  * @param handler 处理用户请求的对象。
  * @param error  SSL错误对象
*/
public void onReceivedSslError(WebView view, SslErrorHandler handler,  SslError error)

/**
  * WebView 的 scale 值发生改变时调用,比如调整适配屏幕
  * Notify the host application that the scale applied to the WebView has
  * changed.
  * @param view The WebView that is initiating the callback.
  * @param oldScale The old scale factor
  * @param newScale The new scale factor
  */
public void onScaleChanged(WebView view, float oldScale, float newScale) 

/*
 * 通知应用程序有个自动登录的帐号过程
 * @param view 请求登陆的webview
 * @param realm 账户的域名,用来查找账户。
 * @param account 一个可选的账户,如果是null 需要和本地的账户进行 check, 如果是一个可用的账户,则提供登录。
 * @param  args  验证制定参数的登录用户
 */
public void onReceivedLoginRequest(WebView view, String realm,  
            String account, String args)

/**
  * Key 事件未被加载时调用,
  * Notify the host application that a key was not handled by the 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.
  *
  * @param view The WebView that is initiating the callback.
  * @param event The key event.
  */
public void onUnhandledKeyEvent(WebView view, KeyEvent event) 

/*
 * 重写此方法才能够处理在浏览器中的按键事件。
 * 提供应用程序同步一个处理按键事件的机会,菜单快捷键需要被过滤掉。
 * 如果返回true,webview不处理该事件,如果返回false, webview 会一直处理这个事件,
 * 因此在 view 链上没有一个父类可以响应到这个事件。默认行为是return false;
* @param view 接收WebViewClient的那个实例
* @param  event 键盘事件名
* @return  如果返回true,应用程序处理该时间,返回false 交有webview处理。
*/
public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event)

/*
 * 通知应用程序内核即将加载 url 指定的资源,应用程序可以返回本地的资源提供给内核,若本地处理返回数
 * 据,内核不从网络上获取数据。        
 * 这个回调并不一定在UI线程执行,所以我们需要注意在这里操作View或者私有数据相关的动作。
 * 如果我们需要改变网页的背景,或者需要实现网页页面颜色定制化的需求,可以在这个回调时机处理。

 * @param view 接收 WebViewClient的 那个实例,
 * @param url    raw url 制定的资源
 * @return 返回 WebResourceResponse 包含数据对象,或者返回null
 */
public WebResourceResponse shouldInterceptRequest(WebView view,   String url)

WebChromeClient 中的方法:

WebChromeClient 的常用方法及注释:

/**
 * 当前 WebView 加载网页进度
 *
 * @param view
 * @param newProgress
 */
@Override
public void onProgressChanged(WebView view, int newProgress) {
    super.onProgressChanged(view, newProgress);
}
/**
 * Js 中调用 alert() 函数,产生的对话框
 *
 * @param view
 * @param url
 * @param message
 * @param result
 * @return
 */@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
     return super.onJsAlert(view, url, message, result);
 }
 /**
 * 处理 Js 中的 Confirm 对话框
 *
 * @param view
 * @param url
 * @param message
 * @param result
 * @return
 */@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
     return super.onJsConfirm(view, url, message, result); 
 }
 /**
 * 处理 JS 中的 Prompt对话框
 *
 * @param view
 * @param url
 * @param message
 * @param defaultValue
 * @param result
 * @return
 */@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
     return super.onJsPrompt(view, url, message, defaultValue, result); 
 }
 /**
 * 接收web页面的icon
 *
 * @param view
 * @param icon
 */@Override
public void onReceivedIcon(WebView view, Bitmap icon) {
     super.onReceivedIcon(view, icon); 
 }
 /**
 * 接收web页面的 Title
 *
 * @param view
 * @param title
 */@Override
public void onReceivedTitle(WebView view, String title) {
     super.onReceivedTitle(view, title); 
 }

自适应操作

比较常用的上面提到过:

WebSettings settings = webView.getSettings(); 
settings.setUseWideViewPort(true); 
settings.setLoadWithOverviewMode(true);

可以通过设置 Algorithm 属性:

/**
  * NARROW_COLUMNS:可能的话使所有列的宽度不超过屏幕宽度
  * NORMAL:正常显示不做任何渲染
  * SINGLE_COLUMN:把所有内容放大 webview 等宽的一列中
  */
settings.setLayoutAlgorithm(LayoutAlgorithm.SINGLE_COLUMN); 

也可以通过注入 JS 的方式去控制元素的属性,可以参考上面对图片自适应大小的处理。

最后一种是通过获取分辨率去设置,已经过时不推荐:

DisplayMetrics metrics = new DisplayMetrics();  
  getWindowManager().getDefaultDisplay().getMetrics(metrics);  
  int mDensity = metrics.densityDpi;  
  if (mDensity == 240) {   
   webSettings.setDefaultZoom(ZoomDensity.FAR);  
  } else if (mDensity == 160) {  
     webSettings.setDefaultZoom(ZoomDensity.MEDIUM);  
  } else if(mDensity == 120) {  
   webSettings.setDefaultZoom(ZoomDensity.CLOSE);  
  }else if(mDensity == DisplayMetrics.DENSITY_XHIGH){  
   webSettings.setDefaultZoom(ZoomDensity.FAR);   
  }else if (mDensity == DisplayMetrics.DENSITY_TV){  
   webSettings.setDefaultZoom(ZoomDensity.FAR);   
  }else{  
      webSettings.setDefaultZoom(ZoomDensity.MEDIUM);  
  }  

302 重定向导致的白屏问题

如上诉,一般我们会简单粗暴的拦截 webView 对 url 跳转逻辑:

WebView.setWebViewClient(new WebViewClient() {
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        view.loadUrl(url);
        return true;
    }
)

这里有个隐藏的问题当网页中存在重定向、跳转 native 页面等情况下,出现空白、无限加载、回退栈异常等问题;
原因就在于 shouldOverrideUrlLoading 返回 true 意味着开发者自己处理 url 的加载,而重定向更应该返回给系统处理;

  • 有一种方案是使用标志位 onTouchEvent 来判断本次 url 跳转是否来自用户点击,并在 reLoad 回调中 reset 标志位;
public class BaseWebView extends WebView {

    private boolean mTouchByUser;

    public BaseWebView(Context context) {
        super(context);
    }

    public BaseWebView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public BaseWebView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public final void loadUrl(String url, Map<String, String> additionalHttpHeaders) {
        super.loadUrl(url, additionalHttpHeaders);
        resetAllStateInternal(url);
    }

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

    @Override
    public final void postUrl(String url, byte[] postData) {
        super.postUrl(url, postData);
        resetAllStateInternal(url);
    }

    @Override
    public final void loadData(String data, String mimeType, String encoding) {
        super.loadData(data, mimeType, encoding);
        resetAllStateInternal(getUrl());
    }

    @Override
    public final void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding,
            String historyUrl) {
        super.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl);
        resetAllStateInternal(getUrl());
    }

    @Override
    public void reload() {
        super.reload();
        resetAllStateInternal(getUrl());
    }

    public boolean isTouchByUser() {
        return mTouchByUser;
    }

    private void resetAllStateInternal(String url) {
        if (!TextUtils.isEmpty(url) && url.startsWith("javascript:")) {
            return;
        }
        resetAllState();
    }

    // 加载url时重置touch状态
    protected void resetAllState() {
        mTouchByUser = false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //用户按下到下一个链接加载之前,置为true
                mTouchByUser = true;
                break;
        }
        return super.onTouchEvent(event);
    }

    @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;
                }
            }
        });
    }
}
  • 另一种方案则通过 onPageFinished 回调来判断当前的状态是否会白屏;
public class KaolaWebview extends BaseWebView implements DownloadListener, Lifeful, OnActivityResultListener {

    private boolean mIsBlankPageRedirect;  //是否因重定向导致的空白页面。

    public KaolaWebview(Context context) {
        super(context);
        init();
    }

    public KaolaWebview(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public KaolaWebview(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    protected void back() {
        if (mBackStep < 1) {
            mJsApi.trigger2("kaolaGoback");
        } else {
            realBack();
        }
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_UP) {
            mIsBlankPageRedirect = true;
        }
        return super.dispatchTouchEvent(ev);
    }

    private WebViewClient mWebViewClient = new WebViewClient() {
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            url = WebViewUtils.removeBlank(url);
            //允许启动第三方应用客户端
            if (WebViewUtils.canHandleUrl(url)) {
                boolean handleByCaller = false;
                // 如果不是用户触发的操作,就没有必要交给上层处理了,直接走url拦截规则。
                if (null != mIWebViewClient && isTouchByUser()) {
                    handleByCaller = mIWebViewClient.shouldOverrideUrlLoading(view, url);
                }
                if (!handleByCaller) {
                    handleByCaller = handleOverrideUrl(url);
                }
                return handleByCaller || super.shouldOverrideUrlLoading(view, url);
            } else {
                try {
                    notifyBeforeLoadUrl(url);
                    Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
                    intent.addCategory(Intent.CATEGORY_BROWSABLE);
                    mContext.startActivity(intent);
                    if (!mIsBlankPageRedirect) {
                        // 如果遇到白屏问题,手动后退
                        back();
                    }
                } catch (Exception e) {
                    ExceptionUtils.printExceptionTrace(e);
                }
                return true;
            }
        }

        @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
            return shouldOverrideUrlLoading(view, request.getUrl().toString());
        }
        
        private boolean handleOverrideUrl(final String url) {
           RouterResult result =  WebActivityRouter.startFromWeb(
                    new IntentBuilder(mContext, url).setRouterActivityResult(new RouterActivityResult() {
                        @Override
                        public void onActivityFound() {
                            if (!mIsBlankPageRedirect) {
                                // 路由已经拦截到跳转到native页面,但此时可能发生了
                                // 301/302跳转,那么执行后退动作,防止白屏。
                                back();
                            }
                        }

                        @Override
                        public void onActivityNotFound() {
                            if (mIWebViewClient != null) {
                                mIWebViewClient.onActivityNotFound();
                            }
                        }
                    }));
            return result.isSuccess();
        }
    };

    @Override
    public void onPageFinished(WebView view, String url) {
        mIsBlankPageRedirect = true;
        if (null != mIWebViewClient) {
            mIWebViewClient.onPageReallyFinish(view, url);
        }
        super.onPageFinished(view, url);
    }
}

优化相关

  • 独立进程

这种方式在
将 WebView 独立运行于单独的进程可以增大 APP 运行的内存,一般在 AndroidManifest.xml 的标签加上 Android:process=":web" 启动一个私有进程:

<activity  
    android:name=".WebViewActivity"  
    android:launchMode="singleTop"
    android:process=":web"  
    android:label="@string/app_name" />  

这样一来与其他组件的通讯就属于跨进程通讯,且对应的销毁方式应该为杀死进程:

@Overrideprotected void onDestroy() {                
     super.onDestroy(); 
     System.exit(0);
}

这种方式是一种成本很高当效果也很好的方法。

  • 选择其他内核的 WebView

见得比较多的是腾讯的 TBS 腾讯浏览服务,基于 X5 内核。优点是在安全性、兼容性方面确实做得越来越好。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容