Andriod WebView 填坑小結

在學習WebView的時候就知道了WebView會出現很多稀奇古怪的問題,真碰上的時候還是焦頭爛額,很多問題的解決方案要在網上找很久很久很久。這里做了稍微全面的總結。

劃重點:

1.內存泄露的解決方法

2.Native獲得的cookie同步到WebView中

3.API5.0以上Ajax跨域訪問無法攜帶cookie的問題

4.Alert劫持問題

1. 內存泄露

關于內存泄漏,想要徹底解決,最好的方法是當你要用webview的時候,另外單獨開一個進程(如何單開進程請自行搜索) 去使用webview 并且當這個 進程結束時,請手動調用System.exit(0)。 但是這種情況又會有另外的問題,就是進程間的通信。

不單開進程時:

1.1 JS無法釋放,WebView在執行JS時被關閉,這些JS資源會無法釋放,一直耗電,一直占CPU,解決方法是在onStop和onResume里分別把setJavaScriptEnabled();給設置成false和true。
@Override
protected void onResume() {
    mWebView.getSettings().setJavaScriptEnabled(true);
    super.onResume();
}
@Override
protected void onStop() {
    mWebView.getSettings().setJavaScriptEnabled(false);
    super.onStop();
}
1.2在onDestroy中對webView的銷毀做處理,下面是我覺得比較好的方式(主要是處理的比較全面)
if (mWebView != null) {
// 如果先調用destroy()方法,則會命中if (isDestroyed()) return;這一行代碼,需要先onDetachedFromWindow(),再
// destory()
ViewParent parent = mWebView.getParent();
if (parent != null) {
    ((ViewGroup) parent).removeView(mWebView);
}

mWebView.stopLoading();
// 退出時調用此方法,移除綁定的服務,否則某些特定系統會報錯
mWebView.getSettings().setJavaScriptEnabled(false);
mWebView.clearHistory();
mWebView.clearAnimation();
mWebView.clearView();
mWebView.removeAllViews();

try {
    mWebView.destroy();
} catch (Throwable ex) {
}

2. WebView的各種設置

2.1 首先是創建

最好不要在XML中直接添加WebView,而是預留一個FrameLayout,代碼中創建WebView,添加到FrameLayout中。
這樣能解決很多內存泄露的問題。

mWebViewContainer = (FrameLayout) findViewById(R.id.web_view_container);
mWebView = new WebView(WebViewActivity.this);
mWebViewContainer.addView(mWebView, new FrameLayout.LayoutParams(
                FrameLayout.LayoutParams.MATCH_PARENT,
                FrameLayout.LayoutParams.MATCH_PARENT));

2.2 然后是settings,N多種方法

WebSettings webSettings = webView.getSettings();
//設置了這個屬性后我們才能在 WebView 里與我們的 Js 代碼進行交互,對于 WebApp 是非常重要的,默認是 false,
//因此我們需要設置為 true,這個本身會有漏洞,具體的下面我會講到
webSettings.setJavaScriptEnabled(true);

//設置 JS 是否可以打開 WebView 新窗口
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);

//WebView 是否支持多窗口,如果設置為 true,需要重寫 
//WebChromeClient#onCreateWindow(WebView, boolean, boolean, Message) 函數,默認為 false
webSettings.setSupportMultipleWindows(true);

//這個屬性用來設置 WebView 是否能夠加載圖片資源,需要注意的是,這個方法會控制所有圖片,包括那些使用 data URI 協議嵌入
//的圖片。使用 setBlockNetworkImage(boolean) 方法來控制僅僅加載使用網絡 URI 協議的圖片。需要提到的一點是如果這
//個設置從 false 變為 true 之后,所有被內容引用的正在顯示的 WebView 圖片資源都會自動加載,該標識默認值為 true。
webSettings.setLoadsImagesAutomatically(false);
//標識是否加載網絡上的圖片(使用 http 或者 https 域名的資源),需要注意的是如果 getLoadsImagesAutomatically() 
//不返回 true,這個標識將沒有作用。這個標識和上面的標識會互相影響。
webSettings.setBlockNetworkImage(true);

//顯示WebView提供的縮放控件
webSettings.setDisplayZoomControls(true);
webSettings.setBuiltInZoomControls(true);

//推薦使用。打開 WebView 的 storage 功能,這樣 JS 的 localStorage,sessionStorage 對象才可以使用
webSettings.setDomStorageEnabled(true);
//推薦打開。設置是否啟動 WebView 的DB API,默認值為 false
webSettings.setDatabaseEnabled(true);
webSettings.setDatabasePath(Utils.getContext().getDir("WebDb",MODE_PRIVATE).getPath());

//打開 WebView 自帶的的 LBS 功能,這樣 JS 的 geolocation 對象才可以使用,注意manifest中的權限
webSettings.setGeolocationEnabled(true);
webSettings.setGeolocationDatabasePath("");

//設置是否打開 WebView 表單數據的保存功能
webSettings.setSaveFormData(true);

//設置 WebView 的默認 userAgent 字符串
webSettings.setUserAgentString("");

// 不推薦使用。設置緩存,是否開啟,緩存模式,緩存大小,緩存路徑
webSettings.setAppCacheEnabled(true);
webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
webSettings.setAppCacheMaxSize(10 * 1024 * 1024); 
webSettings.setAppCachePath(Utils.getContext().getExternalCacheDir().getAbsolutePath());

// 這兩個一起設置,表明WebView會自適應手機屏幕的寬度
webSettings.setUseWideViewPort(true);
webSettings.setLoadWithOverviewMode(true);

//設置 WebView 的字體,可以通過這個函數,改變 WebView 的字體,默認字體為 "sans-serif"
webSettings.setStandardFontFamily("");
//設置 WebView 字體的大小,默認大小為 16
webSettings.setDefaultFontSize(20);
//設置 WebView 支持的最小字體大小,默認為 8
webSettings.setMinimumFontSize(12);

//設置文本的縮放倍數,默認為 100
webSettings.setTextZoom(2);

// 允許高版本http和https混合訪問
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}

// 支持PC上的Chrome調試WebView,具體方法請百度
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    // 只支持Debug模式下調試
    if (0 != (getApplicationInfo().flags &= ApplicationInfo.FLAG_DEBUGGABLE)) {
        WebView.setWebContentsDebuggingEnabled(true);
    }
}

2.3 然后是WebViewClient和ChromeClient的設置

  • WebViewClient主要幫助WebView處理各種通知、請求事件

  • WebChromeClient主要輔助WebView處理Javascript的對話框、網站圖標、網站title、加載進度等

2.3.1 WebViewClient的常用設置

    1. shouldOverrideUrlLoading方法:在點擊請求的是鏈接時會調用此方法。返回false表明交給系統處理,我們自己不干預,正常情況下這么做;返回true,表明在這里攔截了WebView加載的事件,具體如何處理自己看。

      一般情況下

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
        view.loadUrl(request.getUrl().toString());
        return true;
    }
    
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        view.loadUrl(url);
        return true;
    }
    
  • 2.onReceivedSslError:處理https請求。2.1以上版本,前提setJavaScriptEnabled(true);

    @Override
    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
        //handler.cancel(); 默認的處理方式,WebView變成空白頁
        handler.proceed();//接受證書
        //handleMessage(Message msg); 其他處理
    }
    
  • 3.onLoadResource:加載資源時調用,每個資源(比如圖片,js,CSS等)都會調用一次。

    @Override
    public void onLoadResource(WebView view, String url) 
    
  • 4.onPageStarted和onPageStarted:頁面加載開始和結束后會調用。

    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon)
      
    @Override
    public void onPageFinished(WebView view, String url) 
    
  • 5.shouldInterceptRequest:同樣是加載資源時調用,但是這里WebView可以加載本地的資源提供給內核,若本地處理返回數據,內核不從網絡上獲取數據。從API 11時引入, API21更新重載方法。加載本地資源使用方法請看 這篇博客常用資源預加載

    //API 22添加的重載方法
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) 
      
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, String url) 
    

因為上面的博客被刪除了,在這里寫一下如何加載本地資源,包括Js文件,圖片文件等等

這里舉例:需要加載本地的Js文件jsTest.js和icon.png。注意,這里需要把文件放在src/main/assests下

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
    String url = request.getUrl().toString();

    if (!TextUtils.isEmpty(url) && url.contains("jsTest.js")) {
        return editResponse("jsTest.js");
    } else if (!TextUtils.isEmpty(url) && url.contains("icon.png")) {
        return editResponse("icon.png");
    }

    return super.shouldInterceptRequest(view, request);
}

@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
      // 因為加載每個圖片,Js資源都要請求一次網絡。所以這種判斷這種資源是否在本地,如果在本地,就直接返回本地的資源,不再去網上取
    if (url.contains("jsTest.js")) {
        return editResponse("jsTest.js");
    } else if (url.contains("icon.png")) {
        return editResponse("icon.png");
    }
    return super.shouldInterceptRequest(view, url);
}

/**
* 自定義的得到src/main/assests下資源文件的方法
*/
private WebResourceResponse editResponse(String fileName) {
    try {
        return new WebResourceResponse("application/x-javascript", "utf-8", getAssets().open(fileName));
    } catch (IOException e) {
        e.printStackTrace();
    }
    //需處理特殊情況
    return null;
}

2.3.2 ChromeClient的常用設置

  • 1.onJsAlert/onJsConfirm/onJsPrompt方法,對應JS的對話框,確認框,輸入框。JS彈出對話框等時,會調用對應的方法,我們在方法中彈出AlertDialog等,顯示相應信息即可

  • 2.onProgressChanged:通知應用程序當前網頁加載的進度

    @Override
    public void onProgressChanged(WebView view, int newProgress)
    
  • 3.onReceivedTitle:獲取網頁title標題。

    獲取標題的時間主要取決于網頁前段設置標題的位置,一般設置在頁面加載前面,可以較早調用到這個函數

    @Override
    public void onReceivedTitle(WebView view, String title)
    
  • 4.H5播放器全屏和去全屏方法

    //有H5視頻,按下全屏播放時調用的方法
    @Override
    public void onShowCustomView(View view, CustomViewCallback callback)
    // 對應的取消全屏方法
    @Override
    public void onHideCustomView()
    
  • 5.設置WebView視頻未播放時默認顯示占位圖。關于WebView中的視頻播放的相關知識,請點這里

    @Override
    public Bitmap getDefaultVideoPoster()
    

3. Native與JS的相互調用

這個網上有很多資源了,選擇一個我覺得比較全面的博客:Android:你要的WebView與 JS 交互方式 都在這里了

這里只說重點:

Js調用Android現在一般是用WebView的addJavascriptInterface()方式,當然因為Android版本造成的漏洞不能忽視

Android調用Js,毫無疑問,evaluateJavascript()和loadUrl結合使用,根據版本判斷

常見錯誤:

  • 1.線程錯誤。Js調Android時發生,Android調時也經常發生,因為調了Js,很多情況還是會回調Android。報錯信息大致為Js線程必須一致等。很好解決,Android中在UI線程即可。但是推薦WebView.post()的方式,不推薦runOnUiThread()方式

    mWebView.post(new Runnable() {
        @Override
        public void run() {
            // TODO
        }
    });
    

4. Cookie問題

4.1 WebView是有自己的Cookie系統的

WebView會在本地維護每次會話的cookie(保存在data/data/package_name/app_WebView/Cookies.db)。當WebView加載URL的時候,WebView會從本地讀取該URL對應的cookie,并攜帶該cookie與服務器進行通信。
WebView通過android.webkit.CookieManager類來維護cookie。CookieManager是WebView的cookie管理類。

4.2 Native獲得Cookie,設置給WebView如何做。

很多時候我們需要將在native中的 登陸的狀態 同步到H5中避免再次登陸,這就需要用到對Cookie的管理。

問題分解:
1.在OkHttp(假定使用的是OkHttp,其他的方式獲得cookie更簡單)中獲得cookie,并儲存;
2.取得cookie并設置給WebView。

解決:

4.2.1 Okhttp中獲得cookie。

獲得cookie很簡單,只需在OkHttpClient的構建過程中加一行代碼。

List<Cookie> mCookies;
mOkHttpClient = new OkHttpClient.Builder()
        ...
        // 下面就是對OkHttp的cookie的處理
        .cookieJar(new CookieJar() {
                    // 對服務器返回的cookie的處理的方法
                    // 參數url和cookie就是cookie對應的url和cookie的值
                    @Override
                    public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
                        // 對cookie的處理,一般是存到內存中,使其他地方可以獲得
                        mCookies = cookies;
                    }
                    // 發送請求時的cookie的處理,返回的List<Cookie>即請求的cookie
                    @Override
                    public List<Cookie> loadForRequest(HttpUrl url) {
                        return mCookies;
                        //return null;
                    }
                })
        .build();

當然,也可以新建一個類,實現CookieJar,專門處理cookie問題。這里只是給出了最簡單的對cookie的處理,對于持久化等,就是另外的問題了。

4.2.2 將cookie同步到WebView中

private void syncCookies(String url, List<Cookie> cookies) {
    // 一些前提設置
    CookieSyncManager.createInstance(this);
    final CookieManager cookieManager = CookieManager.getInstance();
    cookieManager.setAcceptCookie(true);
    /**
     * 設置webView支持JS的Cookie的調用,5.0以上才要設置
     */
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        cookieManager.setAcceptThirdPartyCookies(mWebView, true);
    }
    //cookieManager.removeAllCookie();
    // 向WebView中添加Cookie,
    for (Cookie cookie : cookies) {
        cookieManager.setCookie(url, cookie.toString());
    }
    // 刷新,同步
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
        CookieManager.getInstance().flush();
    } 
    CookieSyncManager.getInstance().sync();
    //String newCookie = cookieManager.getCookie(url);驗證是否將cookie同步進去
    //KLog.i("newCookie: " + newCookie);
}

這里有幾點要注意:

1.順序。

// 1.先初始化WebView,各種設置setting,webViewClient和chromeClient等
initWebView();
// 2.再獲得,并同步cookie
// MyCookieJar.getInstance().getCookies()可以換成ApiManager.getInstance().getCookies()等
syncCookies(url, MyCookieJar.getInstance().getCookies());
// 3.最后加載url
mWebView.loadUrl(url);

2.cookie的添加時,最好是一個cookie,set一次,最好不要自己拼接,否則關于domain,path,逗號,分號等等的問題會讓人欲仙欲死。還有說法是用String不行,要用StringBuilder。所以,盡量不自己拼。

4.3 Ajax跨域訪問時,Cookie帶不過去的解決方法

問題:Native已經登錄,cookie可以設置進去,但是網頁進行了復雜的ajax操作(我也不知道什么操作),cookie帶不過去,到指定頁面還得登錄。5.0以下正常

解決:經過排查,發現高版本時問題出在ajax跳轉時,是Js對Cookie的操作,不經過WebView,正常的WebView設置沒有作用。看了很多博客,還是在StackOverFlow上發現解決方法。一行代碼

final CookieManager cookieManager = CookieManager.getInstance();
cookieManager.setAcceptCookie(true);
/**
 * 設置webView支持JS的Cookie的調用,5.0以上才要設置
 */
// 大致意思:mWebView接收第三方對Cookie的操作,也就是支持Js對cookie的操作
cookieManager.setAcceptThirdPartyCookies(mWebView, true);

5. 棘手問題

5.1 Alert劫持

Alert劫持:Alert只會彈出一次,并且WebView會卡死。重新加載都不行,必須殺死進程,重新打開App

解決方法很簡單,在自定義的onJsAlert方法中加一行代碼

@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
    ...

    result.confirm();// 不加這行代碼,會造成Alert劫持:Alert只會彈出一次,并且WebView會卡死
  
    return true;
}

6. 其他

6.1 帶進度條的WebView(TODO)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容