WebView使用詳細(xì)介紹

WebSettings

對(duì)WebView進(jìn)行配置和管理

// 如果訪問(wèn)的頁(yè)面中要與Javascript交互,則webview必須設(shè)置支持Javascript
// 若加載的 html 里有JS 在執(zhí)行動(dòng)畫(huà)等操作,會(huì)造成資源浪費(fèi)(CPU、電量)
// 在 onStop 和 onResume 里分別把 setJavaScriptEnabled() 給設(shè)置成 false 和 true 即可
webSettings.setJavaScriptEnabled(true);  

// 支持插件
webSettings.setPluginsEnabled(true); 

// 設(shè)置自適應(yīng)屏幕,兩者合用
webSettings.setUseWideViewPort(true); //將圖片調(diào)整到適合webview的大小 
webSettings.setLoadWithOverviewMode(true); // 縮放至屏幕的大小

// 縮放操作
webSettings.setSupportZoom(true); // 支持縮放,默認(rèn)為true。是下面那個(gè)的前提。
webSettings.setBuiltInZoomControls(true); // 設(shè)置內(nèi)置的縮放控件。若為false,則該WebView不可縮放
webSettings.setDisplayZoomControls(false); // 隱藏原生的縮放控件

// 其他細(xì)節(jié)操作
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); // 關(guān)閉webview中緩存 
webSettings.setAllowFileAccess(true); // 設(shè)置可以訪問(wèn)文件 
webSettings.setJavaScriptCanOpenWindowsAutomatically(true); // 支持通過(guò)JS打開(kāi)新窗口 
webSettings.setLoadsImagesAutomatically(true); // 支持自動(dòng)加載圖片
webSettings.setDefaultTextEncodingName("utf-8");// 設(shè)置編碼格式

WebClient

處理各種通知 & 請(qǐng)求事件

mWebView.setWebViewClient(new MyWebViewClient());
private class MyWebViewClient extends WebViewClient
{
  // 復(fù)寫(xiě)shouldOverrideUrlLoading()方法,使得打開(kāi)網(wǎng)頁(yè)時(shí)不調(diào)用系統(tǒng)瀏覽器, 而是在本W(wǎng)ebView中顯示
  @Override
  public boolean shouldOverrideUrlLoading(WebView view, String url)
  {

    // 特定的url調(diào)到native 頁(yè)面進(jìn)行處理 返回true
    if (LinkHandleUtils.handle(FNWebPageActivity.this, url, true))
    {
      return true;
    }

    mCurUrl = url;
    return false;
  }

  // 開(kāi)始載入頁(yè)面調(diào)用的,我們可以設(shè)定一個(gè)loading的頁(yè)面,告訴用戶程序在等待網(wǎng)絡(luò)響應(yīng)。
  @Override
  public void onPageStarted(WebView webView, String s, Bitmap bitmap)
  {
    super.onPageStarted(webView, s, bitmap);
  }

  // 在頁(yè)面加載結(jié)束時(shí)調(diào)用。我們可以關(guān)閉loading 條,切換程序動(dòng)作
  @Override
  public void onPageFinished(WebView webView, String s)
  {
    super.onPageFinished(webView, s);
  }

  // 在加載頁(yè)面資源時(shí)會(huì)調(diào)用,每一個(gè)資源(比如圖片)的加載都會(huì)調(diào)用一次。
  @Override
  public void onLoadResource(WebView webView, String s)
  {
    super.onLoadResource(webView, s);
  }

  // 加載頁(yè)面的服務(wù)器出現(xiàn)錯(cuò)誤時(shí)(如404)調(diào)用
  @Override
  public void onReceivedError(WebView view, int errorCode, String description, String failingUrl)
  {
    super.onReceivedError(view, errorCode, description, failingUrl);
  }

  // 處理https請(qǐng)求
  @Override
  public void onReceivedSslError(WebView webView, SslErrorHandler sslErrorHandler, SslError sslError)
  {
    sslErrorHandler.proceed();    // 表示等待證書(shū)響應(yīng)
    // sslErrorHandler.cancel();      // 表示掛起連接,為默認(rèn)方式
    // sslErrorHandler.handleMessage(null);    // 可做其他處理
  }
}

WebChromeClient

輔助 WebView 處理 Javascript 的對(duì)話框,網(wǎng)站圖標(biāo),網(wǎng)站標(biāo)題等等。

setWebChromeClient(new MyWebChromeClient());
private class MyWebChromeClient extends WebChromeClient
{
  // 獲得網(wǎng)頁(yè)的加載進(jìn)度并顯示
  @Override
  public void onProgressChanged(com.tencent.smtt.sdk.WebView webView, int newProgress)
  {
    if (newProgress <= 100 && mProgressBar != null)
    {
      if (GONE == mProgressBar.getVisibility())
      {
        mProgressBar.setVisibility(VISIBLE);
      }
      startProgressAnimation(newProgress);
    }
    super.onProgressChanged(webView, newProgress);
  }

  // 獲取Web頁(yè)中的標(biāo)題
  @Override
  public void onReceivedTitle(WebView webView, String title)
  {
    super.onReceivedTitle(webView, title);
    if (mCallback != null && StringUtils.isNotBlank(title))
    {
      mCallback.setTitle(title);
    }
  }

  // 支持javascript的警告框
  @Override
  public boolean onJsAlert(WebView webView, String url, String message, final JsResult result)
  {
    new AlertDialog.Builder(getContext())
            .setTitle("JsAlert")
            .setMessage(message)
            .setPositiveButton("OK", new DialogInterface.OnClickListener()
            {
              @Override
              public void onClick(DialogInterface dialog, int which)
              {
                result.confirm();
              }
            })
            .setCancelable(false)
            .show();
    return true;
  }

  // 支持javascript的確認(rèn)框
  @Override
  public boolean onJsConfirm(WebView webView, String url, String message, final JsResult jsResult)
  {
    new AlertDialog.Builder(getContext())
            .setTitle("JsConfirm")
            .setMessage(message)
            .setPositiveButton("OK", new DialogInterface.OnClickListener()
            {
              @Override
              public void onClick(DialogInterface dialog, int which)
              {
                jsResult.confirm();
              }
            })
            .setNegativeButton("Cancel", new DialogInterface.OnClickListener()
            {
              @Override
              public void onClick(DialogInterface dialog, int which)
              {
                jsResult.cancel();
              }
            })
            .setCancelable(false)
            .show();
    // 返回布爾值:判斷點(diǎn)擊時(shí)確認(rèn)還是取消
    // true表示點(diǎn)擊了確認(rèn);false表示點(diǎn)擊了取消;
    return true;
  }

  // 支持javascript輸入框
  @Override
  public boolean onJsPrompt(WebView webView, String url, String message, String defaultValue, final JsPromptResult result)
  {
    return super.onJsPrompt(webView, s, s1, s2, jsPromptResult);
  }
}

WebView與原生代碼的交互

Java->JS

loadUrl

// mJSMethodName對(duì)應(yīng)js方法名
// result對(duì)應(yīng)js方法參數(shù)
mWebView.loadUrl("javascript:" + mJSMethodName + "(\" " + param + "\")");

對(duì)應(yīng)的html文件如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
      // JS代碼
      <script>
        // Android需要調(diào)用的方法
        function mJSMethodName()
        {
          alert("Android調(diào)用了JS的mJSMethodName方法");
        }
      </script>
    </head>
</html>

特別注意:JS代碼調(diào)用一定要在 onPageFinished() 回調(diào)之后才能調(diào)用,否則不會(huì)調(diào)用。

evaluateJavascript

  1. 該方法的執(zhí)行不會(huì)使頁(yè)面刷新,而第一種方法(loadUrl )的執(zhí)行則會(huì)。所以該方法比第一種方法效率更高。
  2. Android 4.4 后才可使用
mWebView.evaluateJavascript("javascript:" + mJSMethodName + "(\" " + param + "\")", new ValueCallback<String>() 
  {
    @Override
    public void onReceiveValue(String result) 
    {
      // result為js方法返回結(jié)果
    }
  });

注:上面兩種方法各有優(yōu)劣,建議根據(jù)Android版本混合使用

// 因?yàn)樵摲椒ㄔ?Android 4.4 版本才可使用,所以使用時(shí)需進(jìn)行版本判斷
if (Build.VERSION.SDK_INT < 18) 
{
  mWebView.loadUrl("javascript:" + mJSMethodName + "(\" " + param + "\")");
} else 
{
  mWebView.evaluateJavascript("javascript:" + mJSMethodName + "(\" " + param + "\")", new ValueCallback<String>() 
  {
    @Override
    public void onReceiveValue(String result) 
    {
      // result為js方法返回結(jié)果
    }
  });
}

JS->Java

通過(guò) WebView 的 addJavascriptInterface() 方法

這種方法是我們最常用的方法,使用方法如下:

// 添加映射對(duì)象以及命名空間
mWebView.addJavascriptInterface(new MyJsInteration(), "android");
private class MyJsInteration 
{
  @JavascriptInterface
  public void hello(String msg) 
  {
  }
}

上面的java代碼對(duì)應(yīng)的js代碼是:

// 注意android是上面定義的命名空間
window.android.hello(message)

通過(guò)WebViewClient 的shouldOverrideUrlLoading()方法回調(diào)

這個(gè)我們已經(jīng)在上面的代碼里寫(xiě)過(guò)了,比如你可以自己維護(hù)一些特殊的URL以及處理這些URL的 Activity,然后復(fù)寫(xiě) shouldOverrideUrlLoading(),在該方法中攔截特定URL轉(zhuǎn)到特定的Activity進(jìn)行處理。也能達(dá)到JS->Java的目的。并且這種形式也是比較常見(jiàn)的處理方式。

// 復(fù)寫(xiě)shouldOverrideUrlLoading()方法,使得打開(kāi)網(wǎng)頁(yè)時(shí)不調(diào)用系統(tǒng)瀏覽器, 而是在本W(wǎng)ebView中顯示
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) 
{
  // 特定的url調(diào)到native 頁(yè)面進(jìn)行處理 返回true
  if (LinkHandleUtils.handle(MyWebPageActivity.this, url, true)) 
  {
    return true;
  }
  mCurUrl = url;
  return false;
}

通過(guò)WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回調(diào)

這種方法跟上面的沒(méi)有本質(zhì)差異,也是在回調(diào)函數(shù)中進(jìn)行Java代碼操作,目前我在項(xiàng)目中用到的地方較少,主要用來(lái)做一些比較特殊的功能,例如檢測(cè)到Alert彈框中的內(nèi)容符合條件進(jìn)行Java代碼。

JS->Java方法總結(jié)

三種方法優(yōu)劣比較:

  1. 通過(guò)WebView的addJavascriptInterface()方法比較簡(jiǎn)單,并且也更為常見(jiàn),不過(guò)其存在不小的安全隱患。
  2. 通過(guò)WebViewClient 的shouldOverrideUrlLoading()方法回調(diào)這個(gè)使用起來(lái)也比較簡(jiǎn)單,也不存在方式1的安全隱患,不過(guò)JS獲取Android方法的返回值復(fù)雜。

如果JS想要得到Android方法的返回值,只能通過(guò) WebView 的 loadUrl ()去執(zhí)行 JS 方法把返回值傳遞回去

WebView的文件上傳

當(dāng)在網(wǎng)頁(yè)里有文件上傳組件時(shí),我們驚奇的發(fā)現(xiàn)Android端這個(gè)文件上傳組件并沒(méi)有起作用。原因何在呢?因?yàn)锳ndroid 中的 WebView是不能直接打開(kāi)文件選擇彈框的。
接下來(lái)我講簡(jiǎn)單提供一下解決方案,先說(shuō)一下思路:

  1. 接收WebView打開(kāi)文件選擇器的通知,收到通知后,打開(kāi)文件選擇器等待用戶選擇需要上傳的文件
  2. 在onActivityResult中得到用戶選擇的文件的Uri
  3. 然后把Uri傳遞給Html5
    這樣就完成了一次H5選擇文件的過(guò)程,下面我把代碼貼出來(lái)看一下.
  4. 當(dāng)H5在調(diào)用上傳文件的Api的時(shí)候,WebView 會(huì)回調(diào) openFileChooser和onShowFileChooser 方法來(lái)通知我們,那我們就得重寫(xiě)了

需要注意的是openFileChooser在不同的Android版本上是形參不同的

private class MyWebChromeClient extends WebChromeClient
{
  // 支持文件選擇上傳
  @Override
  public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> valueCallback, FileChooserParams fileChooserParams)
  {
    return super.onShowFileChooser(webView, valueCallback, fileChooserParams);
  }

  // Android > 4.1.1 調(diào)用這個(gè)方法
  public void openFileChooser(ValueCallback<Uri> uploadMsg,
                              String acceptType, String capture)
  {
    if (mFileUploadSupportListener == null)
      return;
    // 調(diào)用傳入的接口進(jìn)行回調(diào)
    mFileUploadSupportListener.call(uploadMsg);
  }


  // 3.0 + 調(diào)用這個(gè)方法
  public void openFileChooser(ValueCallback<Uri> uploadMsg,
                              String acceptType)
  {
    if (mFileUploadSupportListener == null)
      return;
    mFileUploadSupportListener.call(uploadMsg);
  }

  // Android < 3.0 調(diào)用這個(gè)方法
  public void openFileChooser(ValueCallback<Uri> uploadMsg)
  {
    if (mFileUploadSupportListener == null)
      return;
    mFileUploadSupportListener.call(uploadMsg);
  }
}
  1. 注入接口
// 注入接口
mWebView.setFileUploadSupportListener(new IFileUploadSupportListener()
{
  @Override
  public void call(ValueCallback<Uri> valueCallback)
  {
    mUploadMessage = valueCallback;
    chooseFile();
  }
});
// 選擇文件
private void chooseFile()
{
  PhotoPicker.builder()         
          .setPhotoCount(1)
          .setShowCamera(true)
          .setShowGif(true)
          .setPreviewEnabled(false)
          .start(MyWebPageActivity.this, PhotoPicker.REQUEST_CODE);
}
  1. 進(jìn)行回傳
if (null == mUploadMessage)
{
  return;
}
if (resultCode == RESULT_OK && requestCode == PhotoPicker.REQUEST_CODE)
{
  ArrayList<String> photos = data.getStringArrayListExtra(PhotoPicker.KEY_SELECTED_PHOTOS);

  Uri result = Uri.parse(photos.get(0));
  mUploadMessage.onReceiveValue(result);
  mUploadMessage = null;
} else
{
  mUploadMessage.onReceiveValue(null);
}

WebView的優(yōu)化

WebView的addJavascriptInterface()方法的安全隱患

上面已經(jīng)稍微說(shuō)了一下,該方法只能在Android4.4以上安全使用,那么我們來(lái)看一下Android 系統(tǒng)占比,Google公布的數(shù)據(jù):截止 2018 .6 .28 ,Android4.4 之下占有約5.7%,具體占比如下圖

Android版本分布

解決方案請(qǐng)參考Android:你不知道的 WebView 使用漏洞

WebView的內(nèi)存泄露

WebView的內(nèi)存泄露問(wèn)題已經(jīng)是個(gè)老生常談的問(wèn)題了,現(xiàn)在只要用到WebView的開(kāi)發(fā)者都得注意到這個(gè)問(wèn)題。
現(xiàn)在流行的有以下兩種解決方案:

獨(dú)立進(jìn)程法

獨(dú)立進(jìn)程法顧名思義是讓包含 WebView 的 Acitivy 以android:process=":web" 的形式指定單獨(dú)進(jìn)程,然后在需要退出的時(shí)候使用System.exit(0) 結(jié)束整個(gè)進(jìn)程,內(nèi)存自然回收了。該方法簡(jiǎn)單暴力,并有以下優(yōu)點(diǎn):

  1. 每個(gè)獨(dú)立的進(jìn)程都能分配獨(dú)立的內(nèi)存,這樣的話,你的app可以獲得雙倍的內(nèi)存,其中一半給Webview吃。增大Webview獲得的內(nèi)存,變相的減小內(nèi)存泄露產(chǎn)生OOM的概率。
  2. 在適當(dāng)時(shí)機(jī)直接殺掉Webview獨(dú)立進(jìn)程,什么內(nèi)存泄露,內(nèi)存占用巨大的問(wèn)題都見(jiàn)鬼去吧。要問(wèn)什么時(shí)機(jī)?比如退出app時(shí),檢測(cè)到?jīng)]有Webview頁(yè)面時(shí)。
  3. Webview發(fā)生崩潰時(shí)不會(huì)導(dǎo)致app閃退,就像第二點(diǎn)說(shuō)的,因?yàn)閃ebview是在獨(dú)立進(jìn)程中,如果發(fā)生崩潰,主進(jìn)程還安然無(wú)事,app還在運(yùn)行中,沒(méi)有閃退,不閃的才是健康的。

源碼解決法

這個(gè)方法就是RTFSC(Read The Fucking Source Code),從LeakCannary分析得出內(nèi)存泄露在 org.chromium.android_webview.AwContents 類

//org.chromium.android_webview.AwContents 類的onAttachedToWindow() 和  onDetachedFromWindow()方法
@Override
public void onAttachedToWindow() {
    if (isDestroyed()) return;
    if (mIsAttachedToWindow) {
        Log.w(TAG, "onAttachedToWindow called when already attached. Ignoring");
        return;
    }
    mIsAttachedToWindow = true;

    mContentViewCore.onAttachedToWindow();
    nativeOnAttachedToWindow(mNativeAwContents, mContainerView.getWidth(),
            mContainerView.getHeight());
    updateHardwareAcceleratedFeaturesToggle();

    if (mComponentCallbacks != null) return;
    mComponentCallbacks = new AwComponentCallbacks();
    mContext.registerComponentCallbacks(mComponentCallbacks);
}

@Override
public void onDetachedFromWindow() {
    if (isDestroyed()) return;//注意這里
    if (!mIsAttachedToWindow) {
        Log.w(TAG, "onDetachedFromWindow called when already detached. Ignoring");
        return;
    }
    mIsAttachedToWindow = false;
    hideAutofillPopup();
    nativeOnDetachedFromWindow(mNativeAwContents);

    mContentViewCore.onDetachedFromWindow();
    updateHardwareAcceleratedFeaturesToggle();

    if (mComponentCallbacks != null) {
        mContext.unregisterComponentCallbacks(mComponentCallbacks);
        mComponentCallbacks = null;
    }

    mScrollAccessibilityHelper.removePostedCallbacks();
}

一般情況下,我們的activity退出的時(shí)候,都會(huì)主動(dòng)調(diào)用 WebView.destroy() 方法,經(jīng)過(guò)分析,destroy()的執(zhí)行時(shí)間在onDetachedFromWindow之前,所以就會(huì)導(dǎo)致不能正常進(jìn)行unregister(),從而造成內(nèi)存泄露。

知道原因了,那么解決辦法也就來(lái)了。
在Activity的onDestroy里方法里如下代碼

@Override
protected void onDestroy() 
{
  if (mWebView != null) 
  {
    try 
    {
      ViewGroup parent = (ViewGroup) mWebView.getParent();
      if (parent != null) 
      {
        parent.removeView(mWebView);
      }
      mWebView.removeAllViews();
      mWebView.destroy();
      } catch (Exception e)     
      {
        e.printStackTrace();
      }
    }
  }
  super.onDestroy();
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 昨天我在家里呆了一天,這一天呆的實(shí)在是太難受了。家中陰冷的氛圍讓我想逃,我想逃離那個(gè)沒(méi)有溫度,只有抑郁的家,那...
    山轉(zhuǎn)轉(zhuǎn)閱讀 465評(píng)論 0 3
  • 沒(méi)落的富豪,窮苦的卑微,志向算什么。 傷心的淚眼,冰冷的殘殼,無(wú)奈的賤命,茍活于凡世。 人為什么要吃飯?為什么要活...
    照亮Br閱讀 213評(píng)論 1 0
  • 像每一個(gè)晚飯后的時(shí)光一樣,劉東又跟蘇妍有一搭沒(méi)一搭的聊著,說(shuō)實(shí)話,劉東并不是一個(gè)很好的聊天對(duì)象,開(kāi)頭第一個(gè)...
    瑾沐閱讀 1,461評(píng)論 0 0
  • 一直都是夏天的時(shí)候出游,難得這次冬天出來(lái),假期短,于是就選擇了不遠(yuǎn)的重慶,體驗(yàn)一番霧都的冬。 買(mǎi)的下午6點(diǎn)15的機(jī)...
    禪木老師閱讀 348評(píng)論 0 0

友情鏈接更多精彩內(nèi)容