WebView優(yōu)化

聲明:大部分內(nèi)容為從其他文章中摘錄感興趣的部分,只為記錄給自己看。
  • 從Android4.4系統(tǒng)開始,Chromium內(nèi)核取代了Webkit內(nèi)核,正式地接管了WebView的渲染工作。Chromium是一個開源的瀏覽器內(nèi)核項目,基于Chromium開源項目修改實現(xiàn)的瀏覽器非常多,包括最著名的Chrome瀏覽器,以及一眾國內(nèi)瀏覽器(360瀏覽器、QQ瀏覽器等)。
  • 從Android5.0系統(tǒng)開始,WebView移植成了一個獨立的apk,可以不依賴系統(tǒng)而獨立存在和更新,我們可以在系統(tǒng)->設(shè)置->Android System WebView看到WebView的當前版本。
  • 從Android7.0系統(tǒng)開始,如果系統(tǒng)安裝了Chrome (version>51),那么Chrome將會直接為應(yīng)用的WebView提供渲染,WebView版本會隨著Chrome的更新而更新,WebView可以脫離應(yīng)用,在一個獨立的沙盒進程中渲染頁面(需要在開發(fā)者選項里打開)。
  • 從Android8.0系統(tǒng)開始,默認開啟WebView多進程模式,即WebView運行在獨立的沙盒進程中。

WebView的狀態(tài)

//激活WebView為活躍狀態(tài),能正常執(zhí)行網(wǎng)頁的響應(yīng)
webView.onResume() ;

//當頁面被失去焦點被切換到后臺不可見狀態(tài),需要執(zhí)行onPause
//通過onPause動作通知內(nèi)核暫停所有的動作,比如DOM的解析、plugin的執(zhí)行、JavaScript執(zhí)行。
webView.onPause();

//當應(yīng)用程序(存在webview)被切換到后臺時,這個方法不僅僅針對當前的webview而是全局的全應(yīng)用程序的webview
//它會暫停所有webview的layout,parsing,javascripttimer。降低CPU功耗。
webView.pauseTimers()
//恢復pauseTimers狀態(tài)
webView.resumeTimers();

//銷毀Webview
//在關(guān)閉了Activity時,如果Webview的音樂或視頻,還在播放。就必須銷毀Webview
//但是注意:webview調(diào)用destory時,webview仍綁定在Activity上
//這是由于自定義webview構(gòu)建時傳入了該Activity的context對象
//因此需要先從父容器中移除webview,然后再銷毀webview:
rootLayout.removeView(webView); 
webView.destroy();

Back鍵控制網(wǎng)頁后退

public boolean onKeyDown(int keyCode, KeyEvent event) {
    if ((keyCode == KEYCODE_BACK) && mWebView.canGoBack()) { 
        mWebView.goBack();
        return true;
    }
    return super.onKeyDown(keyCode, event);
}

清除緩存數(shù)據(jù)

//清除網(wǎng)頁訪問留下的緩存
//由于內(nèi)核緩存是全局的因此這個方法不僅僅針對webview而是針對整個應(yīng)用程序.
Webview.clearCache(true);

//清除當前webview訪問的歷史記錄
//只會webview訪問歷史記錄里的所有記錄除了當前訪問記錄
Webview.clearHistory();

//這個api僅僅清除自動完成填充的表單數(shù)據(jù),并不會清除WebView存儲到本地的數(shù)據(jù)
Webview.clearFormData();

WebSettings常見配置

//聲明WebSettings子類
WebSettings webSettings = webView.getSettings();

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

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

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

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

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

設(shè)置WebView緩存

  • 當加載 html 頁面時,WebView會在/data/data/包名目錄下生成 database 與 cache 兩個文件夾
  • 請求的 URL記錄保存在 WebViewCache.db,而 URL的內(nèi)容是保存在 WebViewCache 文件夾下
//優(yōu)先使用緩存: 
WebView.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); 
//緩存模式如下:
//LOAD_CACHE_ONLY: 不使用網(wǎng)絡(luò),只讀取本地緩存數(shù)據(jù)
//LOAD_DEFAULT: (默認)根據(jù)cache-control決定是否從網(wǎng)絡(luò)上取數(shù)據(jù)。
//LOAD_NO_CACHE: 不使用緩存,只從網(wǎng)絡(luò)獲取數(shù)據(jù).
//LOAD_CACHE_ELSE_NETWORK,只要本地有,無論是否過期,或者no-cache,都使用緩存中的數(shù)據(jù)。

//不使用緩存: 
WebView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);
  • 結(jié)合使用(離線加載)
if (NetStatusUtil.isConnected(getApplicationContext())) {
    webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);//根據(jù)cache-control決定是否從網(wǎng)絡(luò)上取數(shù)據(jù)。
} else {
    webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);//沒網(wǎng),則從本地獲取,即離線加載
}

webSettings.setDomStorageEnabled(true); // 開啟 DOM storage API 功能
webSettings.setDatabaseEnabled(true);   //開啟 database storage API 功能
webSettings.setAppCacheEnabled(true);//開啟 Application Caches 功能

String cacheDirPath = getFilesDir().getAbsolutePath() + APP_CACAHE_DIRNAME;
webSettings.setAppCachePath(cacheDirPath); //設(shè)置  Application Caches 緩存目錄

注意: 每個 Application 只調(diào)用一次 WebSettings.setAppCachePath(),WebSettings.setAppCacheMaxSize()

  • 加載頁面的服務(wù)器出現(xiàn)錯誤時(如404)調(diào)用
//步驟1:寫一個html文件(error_handle.html),用于出錯時展示給用戶看的提示頁面
//步驟2:將該html文件放置到代碼根目錄的assets文件夾下

//步驟3:復寫WebViewClient的onRecievedError方法
//該方法傳回了錯誤碼,根據(jù)錯誤類型可以進行不同的錯誤分類處理
webView.setWebViewClient(new WebViewClient(){
  @Override
  public void onReceivedError(WebView view, int errorCode, String description, String failingUrl){
switch(errorCode)
        {
        case HttpStatus.SC_NOT_FOUND:
            view.loadUrl("file:///android_assets/error_handle.html");
            break;
        }
    }
});
  • 處理HTTS請求,webview默認是不處理https請求的,頁面顯示空白
webView.setWebViewClient(new WebViewClient() {    
    @Override    
    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {    
        handler.proceed();    //表示等待證書響應(yīng)
        // handler.cancel();      //表示掛起連接,為默認方式
        // handler.handleMessage(null);    //可做其他處理
    }    
});  

// 特別注意:5.1以上默認禁止了https和http混用,以下方式是開啟
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    mWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}

如何避免WebView內(nèi)存泄露?

  1. 不在xml中定義webview ,而是在需要的時候在Activity中創(chuàng)建,并且Context使用 getApplicationgContext()
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
mWebView = new WebView(getApplicationContext());
mWebView.setLayoutParams(params);
mLayout.addView(mWebView);
  1. 在 Activity 銷毀( WebView )的時候,先讓 WebView 加載null內(nèi)容,然后移除 WebView,再銷毀 WebView,最后置空。
@Override
protected void onDestroy() {
    if (mWebView != null) {
        mWebView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
        mWebView.clearHistory();
        // 在關(guān)閉Activity時,如果webview的音樂或視頻還在播放。就必須銷毀webview
        // 但是注意:webview調(diào)用destroy時,webview仍綁定在activity上
        // 這是由于自定義webview構(gòu)建時傳入了該activity的context對象
        // 因此需要先從父容器中移除webview,再銷毀
        ((ViewGroup) mWebView.getParent()).removeView(mWebView);
        mWebView.destroy();
        mWebView = null;
    }
    super.onDestroy();
}

WebSettings.setJavaScriptEnabled

我相信99%的應(yīng)用都會調(diào)用下面這句

WebSettings.setJavaScriptEnabled(true);

在Android 4.3版本調(diào)用WebSettings.setJavaScriptEnabled()方法時會調(diào)用一下reload方法,同時會回調(diào)多次WebChromeClient.onJsPrompt()。如果有業(yè)務(wù)邏輯依賴于這兩個方法,就需要注意判斷回調(diào)多次是否會帶來影響了。

同時,如果啟用了JavaScript,務(wù)必做好安全措施,防止遠程執(zhí)行漏洞。

@TargetApi(11)
private static final void removeJavascriptInterfaces(WebView webView) {
    try {
        if (Build.VERSION.SDK_INT >= 11 && Build.VERSION.SDK_INT < 17) {
            webView.removeJavascriptInterface("searchBoxJavaBridge_");
            webView.removeJavascriptInterface("accessibility");
            webView.removeJavascriptInterface("accessibilityTraversal");
        }
    } catch (Throwable tr) {
        tr.printStackTrace();
    }
}

301/302業(yè)務(wù)場景及白屏問題

先來分析一下業(yè)務(wù)場景。對于需要對url進行攔截以及在url中需要拼接特定參數(shù)的WebView來說,301和302發(fā)生的情景主要有以下幾種:

  • 首次進入,有重定向,然后直接加載H5頁面,如http跳轉(zhuǎn)https
  • 首次進入,有重定向,然后跳轉(zhuǎn)到native頁面,如掃一掃短鏈,然后跳轉(zhuǎn)到native
  • 二次加載,有重定向,跳轉(zhuǎn)到native頁面

第二種情況,會遇到WebView空白頁問題,屬于原始url不能攔截到native頁面,但301/302后的url攔截到native頁面的情況,當遇到這種情況時,需要把WebView對應(yīng)的Activity結(jié)束,否則當用戶從攔截后的頁面返回上一個頁面時,是一個WebView空白頁。

第三種情況,也會遇到WebView空白頁問題,原因在于加載的第一個頁面發(fā)生了重定向到了第二個頁面,第二個頁面被客戶端攔截跳轉(zhuǎn)到native頁面,那么WebView就停留在第一個頁面的狀態(tài)了,第一個頁面顯然是空白頁。

與JS交互

1、使用系統(tǒng)方法addJavascriptInterface注入java對象來實現(xiàn)。
2、利用WebViewClient中shouldOverrideUrlLoading(WebView view, String url)接口,攔截操作。這個就是很多公司在用的scheme方式,通過制定url協(xié)議,雙方各自解析,使用iframe來調(diào)用native代碼,實現(xiàn)互通。
3、利用WebChromeClient中的onJsAlert、onJsConfirm、onJsPrompt提示接口,通用也是攔截操作。

4.2版本以下會存在漏洞,4.2以上需要添加@JavascriptInterface注解才能被調(diào)用到,Js調(diào)用方式不變。

對于Android調(diào)用JS代碼的方法有2種:

  1. 通過WebView的loadUrl()
   // Android需要調(diào)用的方法
   function callJS(){
      alert("Android調(diào)用了JS的callJS方法");
   }
// 注意調(diào)用的JS方法名要對應(yīng)上
// 調(diào)用javascript的callJS()方法
mWebView.loadUrl("javascript:callJS()");
// 由于設(shè)置了彈窗檢驗調(diào)用結(jié)果,所以需要支持js對話框
// webview只是載體,內(nèi)容的渲染需要使用webviewChromClient類去實現(xiàn)
// 通過設(shè)置WebChromeClient對象處理JavaScript的對話框
//設(shè)置響應(yīng)js 的Alert()函數(shù)
mWebView.setWebChromeClient(new WebChromeClient() {
    @Override
    public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
         AlertDialog.Builder b = new AlertDialog.Builder(MainActivity.this);
         b.setTitle("Alert");
         b.setMessage(message);
         b.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
              @Override
              public void onClick(DialogInterface dialog, int which) {
                  result.confirm();
              }
         });
         b.setCancelable(false);
         b.create().show();
         return true;
   }
});

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

  1. 通過WebView的evaluateJavascript()
  1. 因為該方法的執(zhí)行不會使頁面刷新,而第一種方法(loadUrl )的執(zhí)行則會。
  2. Android 4.4 后才可使用
// 只需要將第一種方法的loadUrl()換成下面該方法即可
mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String value) {
            //此處為 js 返回的結(jié)果
        }
    });
}
944365-30f095d4c9e638fd.png

結(jié)合使用

// Android版本變量
final int version = Build.VERSION.SDK_INT;
// 因為該方法在 Android 4.4 版本才可使用,所以使用時需進行版本判斷
if (version < 18) {
    mWebView.loadUrl("javascript:callJS()");
} else {
    mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String value) {
            //此處為 js 返回的結(jié)果
        }
    });
}

對于JS調(diào)用Android代碼的方法有3種:

  1. 通過WebView的addJavascriptInterface()進行對象映射
// 定義JS需要調(diào)用的方法
// 被JS調(diào)用的方法必須加入@JavascriptInterface注解
@JavascriptInterface
public void hello(String msg) {
    System.out.println("JS調(diào)用了Android的hello方法");
}

function callAndroid(){
    // 由于對象映射,所以調(diào)用test對象等于調(diào)用Android映射的對象
    test.hello("js調(diào)用了android中的hello方法");
}

// 通過addJavascriptInterface()將Java對象映射到JS對象
//參數(shù)1:Javascript對象名
//參數(shù)2:Java對象名
mWebView.addJavascriptInterface(new AndroidtoJs(), "test");//AndroidtoJS類對象映射到j(luò)s的test對象

// 加載JS代碼
// 格式規(guī)定為:file:///android_asset/文件名.html
mWebView.loadUrl("file:///android_asset/javascript.html");

注意:此方法存在嚴重的漏洞問題,不可以這么寫?。?!

  1. 通過 WebViewClient 的shouldOverrideUrlLoading ()方法回調(diào)攔截 url
function callAndroid(){
    /*約定的url協(xié)議為:js://webview?arg1=111&arg2=222*/
    document.location = "js://webview?arg1=111&arg2=222";
}
  1. 通過 WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回調(diào)攔截JS對話框alert()、confirm()、prompt() 消息


    優(yōu)缺點

    優(yōu)缺點

    綜合比較

WebView與JavaScript相互調(diào)用混淆問題

若webview中的js調(diào)用了本地的方法,正常情況下發(fā)布的debug包js調(diào)用的時候是沒有問題的,但是通常發(fā)布商業(yè)版本的apk都是要經(jīng)過混淆的步驟,這個時候會發(fā)現(xiàn)之前調(diào)用正常的js卻無法正常調(diào)用本地方法了。

這是因為混淆的時候已經(jīng)把本地的代碼的引用給打亂了,導致js中的代碼找不到本地的方法的地址。

解決這個問題很簡單,即在proguard.cfg文件中加上一些代碼,聲明本地中被js調(diào)用的代碼不被混淆。下面舉例說明:

被js調(diào)用的類DemoJavaScriptInterface的包名為com.test.webview,那么就要在proguard.cfg文件中加入:

-keep public class com.test.webview.DemoJavaScriptInterface{
    public <methods>;
}

若是內(nèi)部類,則大致寫成如下形式:

-keep public class com.test.webview.DemoJavaScriptInterface$InnerClass{
    public <methods>;
}

若android版本比較新,可能還需要添加上下列代碼:

-keepattributes *Annotation*  
-keepattributes *JavascriptInterface*

參考
最全面總結(jié)Android WebView

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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