x5開源庫後續知識點

目錄介紹

  • 01.基礎使用目錄介紹

    • 1.0.1 常用的基礎介紹
    • 1.0.2 Android調用Js
    • 1.0.3 Js調用Android
    • 1.0.4 WebView.loadUrl(url)流程
    • 1.0.5 js的調用時機分析
    • 1.0.6 清除緩存數據方式有哪些
    • 1.0.7 如何使用DeepLink
    • 1.0.8 應用被作爲第三方瀏覽器打開
  • 02.優化彙總目錄介紹

    • 2.0.1 視頻全屏播放按返回頁面被放大
    • 2.0.2 加快加載webView中的圖片資源
    • 2.0.3 自定義加載異常error的狀態頁面
    • 2.0.4 WebView硬件加速導致頁面渲染閃爍
    • 2.0.5 WebView加載證書錯誤
    • 2.0.6 web音頻播放銷燬後還有聲音
    • 2.0.7 DNS採用和客戶端API相同的域名
    • 2.0.8 如何設置白名單操作
    • 2.0.9 後臺無法釋放js導致發熱耗電
    • 2.1.0 可以提前顯示加載進度條
    • 2.1.1 WebView密碼明文存儲漏洞優化
  • 03.問題彙總目錄介紹

    • 3.0.0 WebView進化史介紹
    • 3.0.1 提前初始化WebView必要性
    • 3.0.2 x5加載office資源
    • 3.0.3 WebView播放視頻問題
    • 3.0.4 無法獲取webView的正確高度
    • 3.0.5 使用scheme協議打開鏈接風險
    • 3.0.6 如何處理加載錯誤
    • 3.0.7 webView防止內存泄漏
    • 3.0.8 關於js注入時機修改
    • 3.0.9 視頻/圖片寬度超過屏幕
    • 3.1.0 如何保證js安全性
    • 3.1.1 如何代碼開啓硬件加速
    • 3.1.2 WebView設置Cookie
    • 3.1.4 webView加載網頁不顯示圖片
    • 3.1.5 繞過證書校驗漏洞
    • 3.1.6 allowFileAccess漏洞
    • 3.1.7 WebView嵌套ScrollView問題
    • 3.1.8 WebView中圖片點擊放大
    • 3.1.9 頁面滑動期間不渲染/執行
    • 3.2.0 被運營商劫持和注入問題
    • 3.2.1 解決資源加載緩慢問題
    • 3.2.2 判斷是否已經滾動到頁面底端
    • 3.2.3 使用loadData加載html亂碼
    • 3.2.4 WebView下載進度無法監聽
    • 3.2.5 webView出現302/303重定向

x5封裝庫YCWebView開源項目地址

  • https://github.com/yangchong2...
  • 該後續知識點,幾乎包含了實際開發中絕大多數的問題,再次學習和鞏固webView,希望這篇文章對你有用……更多內容,可以看我的開源項目,如果覺得給你帶來一些收穫,麻煩star一下,這也可以增加開發者開源項目的動力!

01.基礎使用目錄介紹

1.0.1 常用的基礎介紹

  • 在activity中最簡單的使用

    webview.loadUrl("http://www.baidu.com/");                    //加載web資源
    //webView.loadUrl("file:///android_asset/example.html");       //加載本地資源
    //這個時候發現一個問題,啓動應用後,自動的打開了系統內置的瀏覽器,解決這個問題需要爲webview設置 WebViewClient,並重寫方法:
    webview.setWebViewClient(new WebViewClient(){
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            view.loadUrl(url);
            //返回值是true的時候控制去WebView打開,爲false調用系統瀏覽器或第三方瀏覽器
            return true;
        }
        //還可以重寫其他的方法
    });
  • 那些因素影響頁面加載速度

    • 影響頁面加載速度的因素有非常多,在對 WebView 加載一個網頁的過程進行調試發現

      • 每次加載的過程中都會有較多的網絡請求,除了 web 頁面自身的 URL 請求
      • 有 web 頁面外部引用的JS、CSS、字體、圖片等等都是個獨立的http請求。這些請求都是串行的,這些請求加上瀏覽器的解析、渲染時間就會導致 WebView 整體加載時間變長,消耗的流量也對應的真多。

1.0.2 Android調用Js

  • 第一種方式:native 調用 js 的方法,方法爲:

    • 注意的是名字一定要對應上,要不然是調用不成功的,而且還有一點是 JS 的調用一定要在 onPageFinished 函數回調之後才能調用,要不然也是會失敗的。
    //java
    //調用無參方法
    mWebView.loadUrl("javascript:callByAndroid()");
    //調用有參方法
    mWebView.loadUrl("javascript:showData(" + result + ")");
    
    //javascript,下面是對應的js代碼
    <script type="text/javascript">
    
    function showData(result){
        alert("result"=result);
        return "success";
    }
    
    function callByAndroid(){
        console.log("callByAndroid")
        showElement("Js:無參方法callByAndroid被調用");
    }
    </script>
  • 第二種方式:

    • 如果現在有需求,我們要得到一個 Native 調用 Web 的回調怎麼辦,Google 在 Android4.4 爲我們新增加了一個新方法,這個方法比 loadUrl 方法更加方便簡潔,而且比 loadUrl 效率更高,因爲 loadUrl 的執行會造成頁面刷新一次,這個方法不會,因爲這個方法是在 4.4 版本才引入的,所以使用的時候需要添加版本的判斷:
    if (Build.VERSION.SDK_INT < 18) {
        mWebView.loadUrl(jsStr);
    } else {
        mWebView.evaluateJavascript(jsStr, new ValueCallback<String>() {
            @Override
            public void onReceiveValue(String value) {
                //此處爲 js 返回的結果
            }
        });
    }
  • 兩種方式的對比

    • 一般最常使用的就是第一種方法,但是第一種方法獲取返回的值比較麻煩,而第二種方法由於是在 4.4 版本引入的,所以侷限性比較大。
  • 注意問題

    • 記得添加ws.setJavaScriptEnabled(true)代碼

1.0.3 Js調用Android

  • 第一種方式:通過 addJavascriptInterface 方法進行添加對象映射

    • 這種是使用最多的方式了,首先第一步我們需要設置一個屬性:
    mWebView.getSettings().setJavaScriptEnabled(true);
    • 這個函數會有一個警告,因爲在特定的版本之下會有非常危險的漏洞,設置完這個屬性之後,Native需要定義一個類:

      • 在 API17 版本之後,需要在被調用的地方加上 @addJavascriptInterface 約束註解,因爲不加上註解的方法是沒有辦法被調用的
public class JSObject {
    private Context mContext;
    public JSObject(Context context) {
        mContext = context;
    }

    @JavascriptInterface
    public String showToast(String text) {
        Toast.show(mContext, text, Toast.LENGTH_SHORT).show();
        return "success";
    }
    
    /**
     * 前端代碼嵌入js:
     * imageClick 名應和js函數方法名一致
     *
     * @param src 圖片的鏈接
     */
    @JavascriptInterface
    public void imageClick(String src) {
        Log.e("imageClick", "----點擊了圖片");
    }
    
    /**
     * 網頁使用的js,方法無參數
     */
    @JavascriptInterface
    public void startFunction() {
        Log.e("startFunction", "----無參");
    }
}

//特定版本下會存在漏洞
mWebView.addJavascriptInterface(new JSObject(this), "yc逗比");
- JS 代碼調用
    - 這種方式的好處在於使用簡單明瞭,本地和 JS 的約定也很簡單,就是對象名稱和方法名稱約定好即可,缺點就是要提到的漏洞問題。
```
function showToast(){
    var result = myObj.showToast("我是來自web的Toast");
}

function showToast(){
    myObj.imageClick("圖片");
}

function showToast(){
    myObj.startFunction();
}
```
  • 第二種方式:利用 WebViewClient 接口回調方法攔截 url

    • 這種方式其實實現也很簡單,使用的頻次也很高,上面介紹到了 WebViewClient ,其中有個回調接口 shouldOverrideUrlLoading (WebView view, String url)) ,就是利用這個攔截 url,然後解析這個 url 的協議,如果發現是我們預先約定好的協議就開始解析參數,執行相應的邏輯。注意這個方法在 API24 版本已經廢棄了,需要使用 shouldOverrideUrlLoading (WebView view, WebResourceRequest request)) 替代,使用方法很類似,我們這裏就使用 shouldOverrideUrlLoading (WebView view, String url)) 方法來介紹一下:

      • 代碼很簡單,這個方法可以攔截 WebView 中加載 url 的過程,得到對應的 url,我們就可以通過這個方法,與網頁約定好一個協議,如果匹配,執行相應操作。
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    //假定傳入進來的 url = "js://openActivity?arg1=111&arg2=222",代表需要打開本地頁面,並且帶入相應的參數
    Uri uri = Uri.parse(url);
    String scheme = uri.getScheme();
    //如果 scheme 爲 js,代表爲預先約定的 js 協議
    if (scheme.equals("js")) {
          //如果 authority 爲 openActivity,代表 web 需要打開一個本地的頁面
        if (uri.getAuthority().equals("openActivity")) {
              //解析 web 頁面帶過來的相關參數
            HashMap<String, String> params = new HashMap<>();
            Set<String> collection = uri.getQueryParameterNames();
            for (String name : collection) {
                params.put(name, uri.getQueryParameter(name));
            }
            Intent intent = new Intent(getContext(), MainActivity.class);
            intent.putExtra("params", params);
            getContext().startActivity(intent);
        }
        //代表應用內部處理完成
        return true;
    }
    return super.shouldOverrideUrlLoading(view, url);
}
- JS 代碼調用
```
function openActivity(){
    document.location = "js://openActivity?arg1=111&arg2=222";
}
```
- 存在問題:這個代碼執行之後,就會觸發本地的 shouldOverrideUrlLoading 方法,然後進行參數解析,調用指定方法。這個方式不會存在第一種提到的漏洞問題,但是它也有一個很繁瑣的地方是,如果 web 端想要得到方法的返回值,只能通過 WebView 的 loadUrl 方法去執行 JS 方法把返回值傳遞回去,相關的代碼如下:
```
//java
mWebView.loadUrl("javascript:returnResult(" + result + ")");

//javascript
function returnResult(result){
    alert("result is" + result);
}
```
  • 第三種方式:利用 WebChromeClient 回調接口的三個方法攔截消息

    • 這個方法的原理和第二種方式原理一樣,都是攔截相關接口,只是攔截的接口不一樣:
    @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
        return super.onJsAlert(view, url, message, result);
    }
    
    @Override
    public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
        return super.onJsConfirm(view, url, message, result);
    }
    
    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        //假定傳入進來的 message = "js://openActivity?arg1=111&arg2=222",代表需要打開本地頁面,並且帶入相應的參數
        Uri uri = Uri.parse(message);
        String scheme = uri.getScheme();
        if (scheme.equals("js")) {
            if (uri.getAuthority().equals("openActivity")) {
                HashMap<String, String> params = new HashMap<>();
                Set<String> collection = uri.getQueryParameterNames();
                for (String name : collection) {
                    params.put(name, uri.getQueryParameter(name));
                }
                Intent intent = new Intent(getContext(), MainActivity.class);
                intent.putExtra("params", params);
                getContext().startActivity(intent);
                //代表應用內部處理完成
                result.confirm("success");
            }
            return true;
        }
        return super.onJsPrompt(view, url, message, defaultValue, result);
    }
    • 和 WebViewClient 一樣,這次添加的是WebChromeClient接口,可以攔截JS中的幾個提示方法,也就是幾種樣式的對話框,在 JS 中有三個常用的對話框方法:

      • onJsAlert 方法是彈出警告框,一般情況下在 Android 中爲 Toast,在文本里面加入n就可以換行;
      • onJsConfirm 彈出確認框,會返回布爾值,通過這個值可以判斷點擊時確認還是取消,true表示點擊了確認,false表示點擊了取消;
      • onJsPrompt 彈出輸入框,點擊確認返回輸入框中的值,點擊取消返回 null。
    • 但是這三種對話框都是可以本地攔截到的,所以可以從這裏去做一些更改,攔截這些方法,得到他們的內容,進行解析,比如如果是 JS 的協議,則說明爲內部協議,進行下一步解析然後進行相關的操作即可,prompt 方法調用如下所示:
    function clickprompt(){
        var result=prompt("js://openActivity?arg1=111&arg2=222");
        alert("open activity " + result);
    }
    • 需要注意的是 prompt 裏面的內容是通過 message 傳遞過來的,並不是第二個參數的 url,返回值是通過 JsPromptResult 對象傳遞。爲什麼要攔截 onJsPrompt 方法,而不是攔截其他的兩個方法,這個從某種意義上來說都是可行的,但是如果需要返回值給 web 端的話就不行了,因爲 onJsAlert 是不能返回值的,而 onJsConfirm 只能夠返回確定或者取消兩個值,只有 onJsPrompt 方法是可以返回字符串類型的值,操作最全面方便。
  • 以上三種方案的總結和對比

    • 以上三種方案都是可行的,在這裏總結一下
    • 第一種方式:是現在目前最普遍的用法,方便簡潔,但是唯一的不足是在 4.2 系統以下存在漏洞問題;
    • 第二種方式:通過攔截 url 並解析,如果是已經約定好的協議則進行相應規定好的操作,缺點就是協議的約束需要記錄一個規範的文檔,而且從 Native 層往 Web 層傳遞值比較繁瑣,優點就是不會存在漏洞,iOS7 之下的版本就是使用的這種方式。
    • 第三種方式:和第二種方式的思想其實是類似的,只是攔截的方法變了,這裏攔截了 JS 中的三種對話框方法,而這三種對話框方法的區別就在於返回值問題,alert 對話框沒有返回值,confirm 的對話框方法只有兩種狀態的返回值,prompt 對話框方法可以返回任意類型的返回值,缺點就是協議的制定比較麻煩,需要記錄詳細的文檔,但是不會存在第二種方法的漏洞問題。

1.0.4 WebView.loadUrl(url)流程

  • WebView.loadUrl(url)加載網頁做了什麼?

    • 加載網頁是一個複雜的過程,在這個過程中,我們可能需要執行一些操作,包括:
    • 加載網頁前,重置WebView狀態以及與業務綁定的變量狀態。WebView狀態包括重定向狀態(mTouchByUser)、前端控制的回退棧(mBackStep)等,業務狀態包括進度條、當前頁的分享內容、分享按鈕的顯示隱藏等。
    • 加載網頁前,根據不同的域拼接本地客戶端的參數,包括基本的機型信息、版本信息、登錄信息以及埋點使用的Refer信息等,有時候涉及交易、財產等還需要做額外的配置。
    • 開始執行頁面加載操作時,會回調WebViewClient.onPageStarted(webview,url,favicon)。在此方法中,可以重置重定向保護的變量(mRedirectProtected),當然也可以在頁面加載前重置,由於歷史遺留代碼問題,此處尚未省去優化。
    • 加載頁面的過程中,WebView會回調幾個方法。
    • 頁面加載結束後,WebView會回調幾個方法。
  • 加載頁面的過程中回調哪些方法?

    • WebChromeClient.onReceivedTitle(webview, title),用來設置標題。需要注意的是,在部分Android系統版本中可能會回調多次這個方法,而且有時候回調的title是一個url,客戶端可以針對這種情況進行特殊處理,避免在標題欄顯示不必要的鏈接。
    • WebChromeClient.onProgressChanged(webview, progress),根據這個回調,可以控制進度條的進度(包括顯示與隱藏)。一般情況下,想要達到100%的進度需要的時間較長(特別是首次加載),用戶長時間等待進度條不消失必定會感到焦慮,影響體驗。其實當progress達到80的時候,加載出來的頁面已經基本可用了。事實上,國內廠商大部分都會提前隱藏進度條,讓用戶以爲網頁加載很快。
    • WebViewClient.shouldInterceptRequest(webview, request),無論是普通的頁面請求(使用GET/POST),還是頁面中的異步請求,或者頁面中的資源請求,都會回調這個方法,給開發一次攔截請求的機會。在這個方法中,我們可以進行靜態資源的攔截並使用緩存數據代替,也可以攔截頁面,使用自己的網絡框架來請求數據。包括後面介紹的WebView免流方案,也和此方法有關。
    • WebViewClient.shouldOverrideUrlLoading(webview, request),如果遇到了重定向,或者點擊了頁面中的a標籤實現頁面跳轉,那麼會回調這個方法。可以說這個是WebView裏面最重要的回調之一,後面WebView與Native頁面交互一節將會詳細介紹這個方法。
    • WebViewClient.onReceivedError(webview,handler,error),加載頁面的過程中發生了錯誤,會回調這個方法。主要是http錯誤以及ssl錯誤。在這兩個回調中,我們可以進行異常上報,監控異常頁面、過期頁面,及時反饋給運營或前端修改。在處理ssl錯誤時,遇到不信任的證書可以進行特殊處理,例如對域名進行判斷,針對自己公司的域名“放行”,防止進入醜陋的錯誤證書頁面。也可以與Chrome一樣,彈出ssl證書疑問彈窗,給用戶選擇的餘地。
  • 加載頁面結束回調哪些方法

    • 會回調WebViewClient.onPageFinished(webview,url)。
    • 這時候可以根據回退棧的情況判斷是否顯示關閉WebView按鈕。通過mActivityWeb.canGoBackOrForward(-1)判斷是否可以回退。

1.0.5 js的調用時機分析

  • onPageFinished()或者onPageStarted()方法中注入js代碼

    • 做過WebView開發,並且需要和js交互,大部分都會認爲js在WebViewClient.onPageFinished()方法中注入最合適,此時dom樹已經構建完成,頁面已經完全展現出來。但如果做過頁面加載速度的測試,會發現WebViewClient.onPageFinished()方法通常需要等待很久纔會回調(首次加載通常超過3s),這是因爲WebView需要加載完一個網頁裏主文檔和所有的資源纔會回調這個方法。
    • 能不能在WebViewClient.onPageStarted()中注入呢?答案是不確定。經過測試,有些機型可以,有些機型不行。在WebViewClient.onPageStarted()中注入還有一個致命的問題——這個方法可能會回調多次,會造成js代碼的多次注入。
    • 從7.0開始,WebView加載js方式發生了一些小改變,官方建議把js注入的時機放在頁面開始加載之後
  • WebViewClient.onProgressChanged()方法中注入js代碼

    • WebViewClient.onProgressChanged()這個方法在dom樹渲染的過程中會回調多次,每次都會告訴我們當前加載的進度。

      • 在這個方法中,可以給WebView自定義進度條,類似微信加載網頁時的那種進度條
      • 如果在此方法中注入js代碼,則需要避免重複注入,需要增強邏輯。可以定義一個boolean值變量控制注入時機
    • 那麼有人會問,加載到多少才需要處理js注入邏輯呢?

      • 正是因爲這個原因,頁面的進度加載到80%的時候,實際上dom樹已經渲染得差不多了,表明WebView已經解析了<html>標籤,這時候注入一定是成功的。在WebViewClient.onProgressChanged()實現js注入有幾個需要注意的地方:
      • 1 上文提到的多次注入控制,使用了boolean值變量控制
      • 2 重新加載一個URL之前,需要重置boolean值變量,讓重新加載後的頁面再次注入js
      • 3 如果做過本地js,css等緩存,則先判斷本地是否存在,若存在則加載本地,否則加載網絡js
      • 4 注入的進度閾值可以自由定製,理論上10%-100%都是合理的,不過建議使用了75%到90%之間可以。

1.0.6 清除緩存數據方式有哪些

  • 清除緩存數據的方法有哪些?

    //清除網頁訪問留下的緩存
    //由於內核緩存是全局的因此這個方法不僅僅針對webview而是針對整個應用程序.
    Webview.clearCache(true);
    
    //清除當前webview訪問的歷史記錄//只會webview訪問歷史記錄裏的所有記錄除了當前訪問記錄
    Webview.clearHistory();
    
    //這個api僅僅清除自動完成填充的表單數據,並不會清除WebView存儲到本地的數據
    Webview.clearFormData();

1.0.7 如何使用DeepLink

1.0.8 應用被作爲第三方瀏覽器打開

  • 微信裏的文章頁面,可以選擇“在瀏覽器打開”。現在很多應用都內嵌了WebView,那是否可以使自己的應用作爲第三方瀏覽器打開此文章呢?
  • 在Manifest文件中,給想要接收跳轉的Activity添加<intent-filter>配置:

    <activity
        android:name=".X5WebViewActivity"
        android:configChanges="orientation|screenSize"
        android:hardwareAccelerated="true"
        android:launchMode="singleTask"
        android:screenOrientation="portrait"
        android:theme="@style/Theme.AppCompat.Light.NoActionBar">
        <!--需要添加下面的intent-filter配置-->
        <intent-filter tools:ignore="AppLinkUrlError">
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.DEFAULT" />
            <category android:name="android.intent.category.BROWSABLE" />
            <!--使用http,則只能打開http開頭的網頁-->
            <data android:scheme="https" />
        </intent-filter>
    </activity>
  • 然後在 X5WebViewActivity 中獲取相關傳遞數據。具體可以看lib中的X5WebViewActivity類代碼。

    public class X5WebViewActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_web_view);
            getIntentData();
            initTitle();
            initWebView();
            webView.loadUrl(mUrl);
           // 處理 作爲三方瀏覽器打開傳過來的值
            getDataFromBrowser(getIntent());
        }
    
       /**
         * 使用singleTask啓動模式的Activity在系統中只會存在一個實例。
         * 如果這個實例已經存在,intent就會通過onNewIntent傳遞到這個Activity。
     */
    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        getDataFromBrowser(intent);
    }

    /**
     * 作爲三方瀏覽器打開傳過來的值
     * Scheme: https
     * host: www.jianshu.com
     * path: /p/yc
     * url = scheme + "://" + host + path;
     */
    private void getDataFromBrowser(Intent intent) {
        Uri data = intent.getData();
        if (data != null) {
            try {
                String scheme = data.getScheme();
                String host = data.getHost();
                String path = data.getPath();
                String text = "Scheme: " + scheme + "\n" + "host: " + host + "\n" + "path: " + path;
                Log.e("data", text);
                String url = scheme + "://" + host + path;
                webView.loadUrl(url);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
```
  • 一些重點說明

    • 在微信中“通過瀏覽器”打開自己的應用,然後將自己的應用切到後臺。重複上面的操作,會一直創建應用的實例,這樣肯定是不好的,爲了避免這種情況我們設置啓動模式爲:launchMode="singleTask"。

02.優化彙總目錄介紹

2.0.1 視頻全屏播放按返回頁面被放大(部分手機出現)

  • 至於原因暫時沒有找到,解決方案如下所示

    /**
     * 當縮放改變的時候會調用該方法
     * @param view                              view
     * @param oldScale                          之前的縮放比例
 */
@Override
public void onScaleChanged(WebView view, float oldScale, float newScale) {
    super.onScaleChanged(view, oldScale, newScale);
    //視頻全屏播放按返回頁面被放大的問題
    if (newScale - oldScale > 7) {
        //異常放大,縮回去。
        view.setInitialScale((int) (oldScale / newScale * 100));
    }
}
```

2.0.2 加載webView中的資源時,加快加載的速度優化,主要是針對圖片

  • html代碼下載到WebView後,webkit開始解析網頁各個節點,發現有外部樣式文件或者外部腳本文件時,會異步發起網絡請求下載文件,但如果在這之前也有解析到image節點,那勢必也會發起網絡請求下載相應的圖片。在網絡情況較差的情況下,過多的網絡請求就會造成帶寬緊張,影響到css或js文件加載完成的時間,造成頁面空白loading過久。解決的方法就是告訴WebView先不要自動加載圖片,等頁面finish後再發起圖片加載。

    //初始化的時候設置,具體代碼在X5WebView類中
    if(Build.VERSION.SDK_INT >= KITKAT) {
        //設置網頁在加載的時候暫時不加載圖片
        ws.setLoadsImagesAutomatically(true);
    } else {
        ws.setLoadsImagesAutomatically(false);
    }
    
    /**
     * 當頁面加載完成會調用該方法
     * @param view                              view
 */
@Override
public void onPageFinished(WebView view, String url) {
    super.onPageFinished(view, url);
    //頁面finish後再發起圖片加載
    if(!webView.getSettings().getLoadsImagesAutomatically()) {
        webView.getSettings().setLoadsImagesAutomatically(true);
    }
}
```

2.0.3 自定義加載異常error的狀態頁面,比如下面這些方法中可能會出現error

  • 當WebView加載頁面出錯時(一般爲404 NOT FOUND),安卓WebView會默認顯示一個出錯界面。當WebView加載出錯時,會在WebViewClient實例中的onReceivedError(),還有onReceivedTitle方法接收到錯誤

    /**
     * 請求網絡出現error
     * @param view                              view
     * @param errorCode                         錯誤🐎
     * @param description                       description
 */
@Override
public void onReceivedError(WebView view, int errorCode, String description, String
        failingUrl) {
    super.onReceivedError(view, errorCode, description, failingUrl);
    if (errorCode == 404) {
        //用javascript隱藏系統定義的404頁面信息
        String data = "Page NO FOUND!";
        view.loadUrl("javascript:document.body.innerHTML=\"" + data + "\"");
    } else {
        if (webListener!=null){
            webListener.showErrorView();
        }
    }
}

// 向主機應用程序報告Web資源加載錯誤。這些錯誤通常表明無法連接到服務器。
// 值得注意的是,不同的是過時的版本的回調,新的版本將被稱爲任何資源(iframe,圖像等)
// 不僅爲主頁。因此,建議在回調過程中執行最低要求的工作。
// 6.0 之後
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
    super.onReceivedError(view, request, error);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        X5WebUtils.log("服務器異常"+error.getDescription().toString());
    }
    //ToastUtils.showToast("服務器異常6.0之後");
    //當加載錯誤時,就讓它加載本地錯誤網頁文件
    //mWebView.loadUrl("file:///android_asset/errorpage/error.html");
    if (webListener!=null){
        webListener.showErrorView();
    }
}

/**
 * 這個方法主要是監聽標題變化操作的
 * @param view                              view
 * @param title                             標題
 */
@Override
public void onReceivedTitle(WebView view, String title) {
    super.onReceivedTitle(view, title);
    if (title.contains("404") || title.contains("網頁無法打開")){
        if (webListener!=null){
            webListener.showErrorView();
        }
    } else {
        // 設置title
    }
}
```

2.0.4 WebView硬件加速導致頁面渲染閃爍

  • 4.0以上的系統我們開啓硬件加速後,WebView渲染頁面更加快速,拖動也更加順滑。但有個副作用就是,當WebView視圖被整體遮住一塊,然後突然恢復時(比如使用SlideMenu將WebView從側邊滑出來時),這個過渡期會出現白塊同時界面閃爍。解決這個問題的方法是在過渡期前將WebView的硬件加速臨時關閉,過渡期後再開啓

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        webview.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
    }

2.0.5 WebView加載證書錯誤

  • webView加載一些別人的url時候,有時候會發生證書認證錯誤的情況,這時候我們希望能夠正常的呈現頁面給用戶,我們需要忽略證書錯誤,需要調用WebViewClient類的onReceivedSslError方法,調用handler.proceed()來忽略該證書錯誤。

    /**
     * 在加載資源時通知主機應用程序發生SSL錯誤
     * 作用:處理https請求
     * @param view                              view
     * @param handler                           handler
 */
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
    super.onReceivedSslError(view, handler, error);
    if (error!=null){
        String url = error.getUrl();
        X5WebUtils.log("onReceivedSslError----異常url----"+url);
    }
    //https忽略證書問題
    if (handler!=null){
        //表示等待證書響應
        handler.proceed();
        // handler.cancel();      //表示掛起連接,爲默認方式
        // handler.handleMessage(null);    //可做其他處理
    }
}
```

2.0.6 web音頻播放銷燬後還有聲音

  • WebView頁面中播放了音頻,退出Activity後音頻仍然在播放,需要在Activity的onDestory()中調用

    @Override
    protected void onDestroy() {
        try {
            //有音頻播放的web頁面的銷燬邏輯
            //在關閉了Activity時,如果Webview的音樂或視頻,還在播放。就必須銷燬Webview
            //但是注意:webview調用destory時,webview仍綁定在Activity上
            //這是由於自定義webview構建時傳入了該Activity的context對象
            //因此需要先從父容器中移除webview,然後再銷燬webview:
            if (webView != null) {
                ViewGroup parent = (ViewGroup) webView.getParent();
                if (parent != null) {
                    parent.removeView(webView);
                }
                webView.removeAllViews();
                webView.destroy();
                webView = null;
            }
        } catch (Exception e) {
            Log.e("X5WebViewActivity", e.getMessage());
        }
        super.onDestroy();
    }

2.0.7 DNS採用和客戶端API相同的域名

  • 建立連接/服務器處理;在頁面請求的數據返回之前,主要有以下過程耗費時間。

    DNS
    connection
    服務器處理
  • DNS採用和客戶端API相同的域名

    • DNS會在系統級別進行緩存,對於WebView的地址,如果使用的域名與native的API相同,則可以直接使用緩存的DNS而不用再發起請求圖片。
    • 舉個簡單例子,客戶端請求域名主要位於api.yc.com,然而內嵌的WebView主要位於 i.yc.com。
    • 當我們初次打開App時:客戶端首次打開都會請求api.yc.com,其DNS將會被系統緩存。然而當打開WebView的時候,由於請求了不同的域名,需要重新獲取i.yc.com的IP。靜態資源同理,最好與客戶端的資源域名保持一致。

2.0.8 如何設置白名單操作

  • 客戶端內的WebView都是可以通過客戶端的某個schema打開的,而要打開頁面的URL很多都並不寫在客戶端內,而是可以由URL中的參數傳遞過去的。上面4.0.5 使用scheme協議打開鏈接風險已經說明了scheme使用的危險性,那麼如何避免這個問題了,設置運行訪問的白名單。或者當用戶打開外部鏈接前給用戶強烈而明顯的提示。具體操作如下所示:

    • 在onPageStarted開始加載資源的方法中,獲取加載url的host值,然後和本地保存的合法host做比較,這裏domainList是一個數組
    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
        super.onPageStarted(view, url, favicon);
        String host = Uri.parse(url).getHost();
        LoggerUtils.i("host:" + host);
        if (!BuildConfig.IS_DEBUG) {
            if (Arrays.binarySearch(domainList, host) < 0) {
                //不在白名單內,非法網址,這個時候給用戶強烈而明顯的提示
            } else {
                //合法網址
            }
        }
    }
  • 設置白名單操作其實和過濾廣告是一個意思,這裏你可以放一些合法的網址允許訪問。

2.0.9 後臺無法釋放js導致發熱耗電

  • 在有些手機你如果webView加載的html裏,有一些js一直在執行比如動畫之類的東西,如果此刻webView 掛在了後臺這些資源是不會被釋放用戶也無法感知。
  • 導致一直佔有cpu 耗電特別快,所以如果遇到這種情況,處理方式如下所示。大概意思就是在後臺的時候,會調用onStop方法,即此時關閉js交互,回到前臺調用onResume再開啓js交互。

    //在onStop裏面設置setJavaScriptEnabled(false);
    //在onResume裏面設置setJavaScriptEnabled(true)。
    @Override
    protected void onResume() {
        super.onResume();
        if (mWebView != null) {
            mWebView.getSettings().setJavaScriptEnabled(true);
        }
    }
    @Override
    protected void onStop() {
        super.onStop();
        if (mWebView != null) {
            mWebView.getSettings().setJavaScriptEnabled(false);
        }
    }

2.1.0 可以提前顯示加載進度條

  • 提前顯示進度條不是提升性能 , 但是對用戶體驗來說也是很重要的一點 , WebView.loadUrl("url") 不會立馬就回調 onPageStarted 或者 onProgressChanged 因爲在這一時間段,WebView 有可能在初始化內核,也有可能在與服務器建立連接,這個時間段容易出現白屏,白屏用戶體驗是很糟糕的 ,所以建議

    //正確
    pb.setVisibility(View.VISIBLE);
    mWebView.loadUrl("https://github.com/yangchong211/LifeHelper");
    
    //不太好
    @Override
    public void onPageStarted(WebView webView, String s, Bitmap bitmap) {
        super.onPageStarted(webView, s, bitmap);
        //設定加載開始的操作
        pb.setVisibility(View.VISIBLE);
    }
    
    //下面這個是監聽進度條進度變化的邏輯
    mWebView.getX5WebChromeClient().setWebListener(interWebListener);
    mWebView.getX5WebViewClient().setWebListener(interWebListener);
    private InterWebListener interWebListener = new InterWebListener() {
        @Override
        public void hindProgressBar() {
            pb.setVisibility(View.GONE);
        }
    
        @Override
        public void showErrorView() {
    
        }
    
        @Override
        public void startProgress(int newProgress) {
            pb.setProgress(newProgress);
        }
    
        @Override
        public void showTitle(String title) {
    
        }
    };

2.1.1 WebView密碼明文存儲漏洞優化

  • WebView 默認開啓密碼保存功能 mWebView.setSavePassword(true),如果該功能未關閉,在用戶輸入密碼時,會彈出提示框,詢問用戶是否保存密碼,如果選擇”是”,密碼會被明文保到 /data/data/com.package.name/databases/webview.db 中,這樣就有被盜取密碼的危險,所以需要通過 WebSettings.setSavePassword(false) 關閉密碼保存提醒功能。

    • 具體代碼操作如下所示
    /設置是否開啓密碼保存功能,不建議開啓,默認已經做了處理,存在盜取密碼的危險
    mX5WebView.setSavePassword(false);

03.問題彙總目錄介紹

3.0.0 WebView進化史介紹

  • 進化史如下所示

    • 從Android4.4系統開始,Chromium內核取代了Webkit內核。
    • 從Android5.0系統開始,WebView移植成了一個獨立的apk,可以不依賴系統而獨立存在和更新。
    • 從Android7.0 系統開始,如果用戶手機裏安裝了 Chrome , 系統優先選擇 Chrome 爲應用提供 WebView 渲染。
    • 從Android8.0系統開始,默認開啓WebView多進程模式,即WebView運行在獨立的沙盒進程中。

3.0.1 提前初始化WebView必要性

  • 第一次打開Web面 ,使用WebView加載頁面的時候特別慢,第二次打開就能明顯的感覺到速度有提升,爲什麼?

    • 是因爲在你第一次加載頁面的時候 WebView 內核並沒有初始化 ,所以在第一次加載頁面的時候需要耗時去初始化WebView內核 。
    • 提前初始化WebView內核 ,例如如下把它放到了Application裏面去初始化 , 在頁面裏可以直接使用該WebView,這種方法可以比較有效的減少WebView在App中的首次打開時間。當用戶訪問頁面時,不需要初始化WebView的時間。
    • 但是這樣也有不好的地方,額外的內存消耗。頁面間跳轉需要清空上一個頁面的痕跡,更容易內存泄露。

3.0.2 x5加載office資源

  • 關於加載word,pdf,xls等文檔文件注意事項:Tbs不支持加載網絡的文件,需要先把文件下載到本地,然後再加載出來
  • 還有一點要注意,在onDestroy方法中調用此方法mTbsReaderView.onStop(),否則第二次打開無法瀏覽。更多可以看FileReaderView類代碼!

3.0.3 WebView播放視頻問題

  • 1、此次的方案用到WebView,而且其中會有視頻嵌套,在默認的WebView中直接播放視頻會有問題, 而且不同的SDK版本情況還不一樣,網上搜索了下解決方案,在此記錄下. webView.getSettings.setPluginState(PluginState.ON);webView.setWebChromeClient(new WebChromeClient());
  • 2、然後在webView的Activity配置裏面加上: android:hardwareAccelerated="true"
  • 3、以上可以正常播放視頻了,但是webview的頁面都finish了居然還能聽 到視頻播放的聲音, 於是又查了下發現webview的onResume方法可以繼續播放,onPause可以暫停播放, 但是這兩個方法都是在Added in API level 11添加的,所以需要用反射來完成。
  • 4、停止播放:在頁面的onPause方法中使用:webView.getClass().getMethod("onPause").invoke(webView, (Object[])null);
  • 5、繼續播放:在頁面的onResume方法中使用:webView.getClass().getMethod("onResume").invoke(webView,(Object[])null);這樣就可以控制視頻的暫停和繼續播放了。

3.0.4 無法獲取webView的正確高度

  • 偶發情況,獲取不到webView的內容高度

    • 其中htmlString是一個HTML格式的字符串。
    webView.loadData(htmlString, "text/html", "utf-8");
    webView.setWebViewClient(new WebViewClient() {
        public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
            Log.d("yc", view.getContentheight() + "");
        }
    });
    • 這是因爲onPageFinished回調指的WebView已經完成從網絡讀取的字節數,這一點。在點onPageFinished被激發的頁面可能還沒有被解析。
  • 第一種解決辦法:提供onPageFinished()一些延遲

    webView.setWebViewClient(new WebViewClient() {
        @Override
        public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
            webView.postDelayed(new Runnable() {
                @Override
                public void run() {
                    int contentHeight = webView.getContentHeight();
                    int viewHeight = webView.getHeight();
                }
            }, 500);
        }
    });
  • 第二種解決辦法:使用js獲取內容高度,具體可以看這篇文章:https://www.jianshu.com/p/ad2...

3.0.5 使用scheme協議打開鏈接風險

  • 常見的用法是在APP獲取到來自網頁的數據後,重新生成一個intent,然後發送給別的組件使用這些數據。比如使用Webview相關的Activity來加載一個來自網頁的url,如果此url來自url scheme中的參數,如:yc://ycbjie:8888/from?load_url=http://www.taobao.com

    • 如果在APP中,沒有檢查獲取到的load_url的值,攻擊者可以構造釣魚網站,誘導用戶點擊加載,就可以盜取用戶信息。
    • 這個時候,別人非法篡改參數,於是將scheme協議改成yc://ycbjie:8888/from?load_url=http://www.doubi.com。這個時候點擊進去即可進入釣魚鏈接地址。
  • 使用建議

    • APP中任何接收外部輸入數據的地方都是潛在的攻擊點,過濾檢查來自網頁的參數。
    • 不要通過網頁傳輸敏感信息,有的網站爲了引導已經登錄的用戶到APP上使用,會使用腳本動態的生成URL Scheme的參數,其中包括了用戶名、密碼或者登錄態token等敏感信息,讓用戶打開APP直接就登錄了。惡意應用也可以註冊相同的URL Sechme來截取這些敏感信息。Android系統會讓用戶選擇使用哪個應用打開鏈接,但是如果用戶不注意,就會使用惡意應用打開,導致敏感信息泄露或者其他風險。
  • 解決辦法

    • 在內嵌的WebView中應該限制允許打開的WebView的域名,並設置運行訪問的白名單。或者當用戶打開外部鏈接前給用戶強烈而明顯的提示。具體操作可以看5.0.8 如何設置白名單操作方式。

3.0.6 如何處理加載錯誤(Http、SSL、Resource)

  • 對於WebView加載一個網頁過程中所產生的錯誤回調,大致有三種

    /**
     * 只有在主頁面加載出現錯誤時,纔會回調這個方法。這正是展示加載錯誤頁面最合適的方法。
     * 然而,如果不管三七二十一直接展示錯誤頁面的話,那很有可能會誤判,給用戶造成經常加載頁面失敗的錯覺。
     * 由於不同的WebView實現可能不一樣,所以我們首先需要排除幾種誤判的例子:
     *      1.加載失敗的url跟WebView裏的url不是同一個url,排除;
     *      2.errorCode=-1,表明是ERROR_UNKNOWN的錯誤,爲了保證不誤判,排除
     *      3failingUrl=null&errorCode=-12,由於錯誤的url是空而不是ERROR_BAD_URL,排除
     * @param webView                                           webView
     * @param errorCode                                         errorCode
     * @param description                                       description
 */
@Override
public void onReceivedError(WebView webView, int errorCode,
                            String description, String failingUrl) {
    super.onReceivedError(webView, errorCode, description, failingUrl);
    // -12 == EventHandle.ERROR_BAD_URL, a hide return code inside android.net.http package
    if ((failingUrl != null && !failingUrl.equals(webView.getUrl())
            && !failingUrl.equals(webView.getOriginalUrl())) /* not subresource error*/
            || (failingUrl == null && errorCode != -12) /*not bad url*/
            || errorCode == -1) { //當 errorCode = -1 且錯誤信息爲 net::ERR_CACHE_MISS
        return;
    }
    if (!TextUtils.isEmpty(failingUrl)) {
        if (failingUrl.equals(webView.getUrl())) {
            //做自己的錯誤操作,比如自定義錯誤頁面
        }
    }
}

/**
 * 只有在主頁面加載出現錯誤時,纔會回調這個方法。這正是展示加載錯誤頁面最合適的方法。
 * 然而,如果不管三七二十一直接展示錯誤頁面的話,那很有可能會誤判,給用戶造成經常加載頁面失敗的錯覺。
 * 由於不同的WebView實現可能不一樣,所以我們首先需要排除幾種誤判的例子:
 *      1.加載失敗的url跟WebView裏的url不是同一個url,排除;
 *      2.errorCode=-1,表明是ERROR_UNKNOWN的錯誤,爲了保證不誤判,排除
 *      3failingUrl=null&errorCode=-12,由於錯誤的url是空而不是ERROR_BAD_URL,排除
 * @param webView                                           webView
 * @param webResourceRequest                                webResourceRequest
 * @param webResourceError                                  webResourceError
 */
@Override
public void onReceivedError(WebView webView, WebResourceRequest webResourceRequest,
                            WebResourceError webResourceError) {
    super.onReceivedError(webView, webResourceRequest, webResourceError);
}

/**
 * 任何HTTP請求產生的錯誤都會回調這個方法,包括主頁面的html文檔請求,iframe、圖片等資源請求。
 * 在這個回調中,由於混雜了很多請求,不適合用來展示加載錯誤的頁面,而適合做監控報警。
 * 當某個URL,或者某個資源收到大量報警時,說明頁面或資源可能存在問題,這時候可以讓相關運營及時響應修改。
 * @param webView                                           webView
 * @param webResourceRequest                                webResourceRequest
 * @param webResourceResponse                               webResourceResponse
 */
@Override
public void onReceivedHttpError(WebView webView, WebResourceRequest webResourceRequest,
                                WebResourceResponse webResourceResponse) {
    super.onReceivedHttpError(webView, webResourceRequest, webResourceResponse);
}

/**
 * 任何HTTPS請求,遇到SSL錯誤時都會回調這個方法。
 * 比較正確的做法是讓用戶選擇是否信任這個網站,這時候可以彈出信任選擇框供用戶選擇(大部分正規瀏覽器是這麼做的)。
 * 有時候,針對自己的網站,可以讓一些特定的網站,不管其證書是否存在問題,都讓用戶信任它。
 * 坑:有時候部分手機打開頁面報錯,絕招:讓自己網站的所有二級域都是可信任的。
 * @param webView                                           webView
 * @param sslErrorHandler                                   sslErrorHandler
 * @param sslError                                          sslError
 */
@Override
public void onReceivedSslError(WebView webView, SslErrorHandler sslErrorHandler, SslError sslError) {
    super.onReceivedSslError(webView, sslErrorHandler, sslError);
    //判斷網站是否是可信任的,與自己網站host作比較
    if (WebViewUtils.isYCHost(webView.getUrl())) {
        //如果是自己的網站,則繼續使用SSL證書
        sslErrorHandler.proceed();
    } else {
        super.onReceivedSslError(webView, sslErrorHandler, sslError);
    }
}
```


3.0.7 webView防止內存泄漏

3.0.9 視頻/圖片寬度超過屏幕

  • 視頻播放寬度或者圖片寬度比webView設置的寬度大,超過屏幕:這個時候可以設置ws.setLoadWithOverviewMode(false);
  • 另外一種讓圖片不超出屏幕範圍的方法,可以用的是css

    <script type="text/javascript">
       var tables = document.getElementsByTagName("img");  //找到table標籤
         for(var i = 0; i<tables.length; i++){  // 逐個改變
                tables[i].style.width = "100%";  // 寬度改爲100%
                 tables[i].style.height = "auto";
         }
    </script>
  • 通過webView的setting屬性設置

    // 網頁內容的寬度是否可大於WebView控件的寬度
    ws.setLoadWithOverviewMode(false);

3.1.0 如何保證js安全性

  • Android和js如何通信

    • 爲了與Web頁面實現動態交互,Android應用程序允許WebView通過WebView.addJavascriptInterface接口向Web頁面注入Java對象,頁面Javascript腳本可直接引用該對象並調用該對象的方法。
    • 這類應用程序一般都會有類似如下的代碼:

      webView.addJavascriptInterface(javaObj, "jsObj");
    • 此段代碼將javaObj對象暴露給js腳本,可以通過jsObj對象對其進行引用,調用javaObj的方法。結合Java的反射機制可以通過js腳本執行任意Java代碼,相關代碼如下:

      • 當受影響的應用程序執行到上述腳本的時候,就會執行someCmd指定的命令。
      <script>
        function execute(cmdArgs) {
            return jsobj.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs);
        }
      
        execute(someCmd);
      </script>
  • addJavascriptInterface任何命令執行漏洞

    • 在webView中使用js與html進行交互是一個不錯的方式,但是,在Android4.2(16,包含4.2)及以下版本中,如果使用addJavascriptInterface,則會存在被注入js接口的漏洞;在4.2之後,由於Google增加了@JavascriptInterface,該漏洞得以解決。
  • @JavascriptInterface註解做了什麼操作

    • 之前,任何Public的函數都可以在JS代碼中訪問,而Java對象繼承關係會導致很多Public的函數都可以在JS中訪問,其中一個重要的函數就是getClass()。然後JS可以通過反射來訪問其他一些內容。通過引入 @JavascriptInterface註解,則在JS中只能訪問 @JavascriptInterface註解的函數。這樣就可以增強安全性。

3.1.1 如何代碼開啓硬件加速

  • 開啓軟硬件加速這個性能提升還是很明顯的,但是會耗費更大的內存 。直接調用代碼api即可完成,webView.setOpenLayerType(true);

3.1.2 WebView設置Cookie

  • h5頁面爲何要設置cookie,主要是避免網頁重複登錄,作用是記錄用戶登錄信息,下次進去不需要重複登錄。
  • 代碼裏怎麼設置Cookie,如下所示

    /**
 *
 * @param url               地址
 * @param cookieList        需要添加的Cookie值,以鍵值對的方式:key=value
 */
private void syncCookie (Context context , String url, ArrayList<String> cookieList) {
    //初始化
    CookieSyncManager.createInstance(context);
    //獲取對象
    CookieManager cookieManager = CookieManager.getInstance();
    cookieManager.setAcceptCookie(true);
    //移除
    cookieManager.removeSessionCookie();
    //添加
    if (cookieList != null && cookieList.size() > 0) {
        for (String cookie : cookieList) {
            cookieManager.setCookie(url, cookie);
        }
    }
    String cookies = cookieManager.getCookie(url);
    X5LogUtils.d("cookies-------"+cookies);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        cookieManager.flush();
    } else {
        CookieSyncManager.getInstance().sync();
    }
}
```
  • 在android裏面在調用webView.loadUrl(url)之前一句調用此方法就可以給WebView設置Cookie

    • 注:這裏一定要注意一點,在調用設置Cookie之後不能再設置,否則設置Cookie無效。該處需要校驗,爲何???
    webView.getSettings().setBuiltInZoomControls(true);  
    webView.getSettings().setJavaScriptEnabled(true);  
  • 還有跨域問題: 域A: test1.yc.com 域B: test2.yc.com

    • 那麼在域A生產一個可以使域A和域B都能訪問的Cookie就需要將Cookie的domain設置爲.yc.com;
    • 如果要在域A生產一個令域A不能訪問而域能訪問的Cookie就要將Cookie設置爲test2.yc.com。
  • Cookie的過期機制

    • 可以設置Cookie的生效時間字段名爲: expires 或 max-age。

      • expires:過期的時間點
      • max-age:生效的持續時間,單位爲秒。
    • 若將Cookie的 max-age 設置爲負數,或者 expires 字段設置爲過期時間點,數據庫更新後這條Cookie將從數據庫中被刪除。如果將Cookie的 max-age 和 expires 字段設置爲正常的過期日期,則到期後再數據庫更新時會刪除該條數據。
  • 下面列出幾個有用的接口:

    • 獲取某個url下的所有Cookie:CookieManager.getInstance().getCookie(url)
    • 判斷WebView是否接受Cookie:CookieManager.getInstance().acceptCookie()
    • 清除Session Cookie:CookieManager.getInstance().removeSessionCookies(ValueCallback<Boolean> callback)
    • 清除所有Cookie:CookieManager.getInstance().removeAllCookies(ValueCallback<Boolean> callback)
    • Cookie持久化:CookieManager.getInstance().flush()
    • 針對某個主機設置Cookie:CookieManager.getInstance().setCookie(String url, String value)

3.1.4 webView加載網頁不顯示圖片

  • webView從Lollipop(5.0)開始webView默認不允許混合模式, https當中不能加載http資源, 而開發的時候可能使用的是https的鏈接, 但是鏈接中的圖片可能是http的, 所以需要設置開啓。

    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
            mWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
    }
    mWebView.getSettings().setBlockNetworkImage(false);

3.1.5 繞過證書校驗漏洞

  • webviewClient中有onReceivedError方法,當出現證書校驗錯誤時,我們可以在該方法中使用handler.proceed()來忽略證書校驗繼續加載網頁,或者使用默認的handler.cancel()來終端加載。

    • 因爲我們使用了handler.proceed(),由此產生了該“繞過證書校驗漏洞”。如果確定所有頁面都能滿足證書校驗,則不必要使用handler.proceed()
    @SuppressLint("NewApi")
    @Override
    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
        //handler.proceed();// 接受證書
        super.onReceivedSslError(view, handler, error);
    }

3.1.6 allowFileAccess漏洞

  • 如果webView.getSettings().setAllowFileAccess(boolean)設置爲true,則會面臨該問題;該漏洞是通過WebView對Javascript的延時執行和html文件替換產生的。

    • 解決方案是禁止WebView頁面打開本地文件,即:webView.getSettings().setAllowFileAccess(false);
    • 或者更直接的禁止使用JavaScript:webView.getSettings().setJavaScriptEnabled(false);

3.1.7 WebView嵌套ScrollView問題

  • 問題描述

    • 當 WebView 嵌套在 ScrollView 裏面的時候,如果 WebView 先加載了一個高度很高的網頁,然後加載了一個高度很低的網頁,就會造成 WebView 的高度無法自適應,底部出現大量空白的情況出現。
  • 解決辦法

3.1.8 WebView中圖片點擊放大

  • 首先載入js

    //將js對象與java對象進行映射
    webView.addJavascriptInterface(new ImageJavascriptInterface(context), "imagelistener");
  • html加載完成之後,添加監聽圖片的點擊js函數,這個可以在onPageFinished方法中操作

    @Override
    public void onPageFinished(WebView view, String url) {
        X5LogUtils.i("-------onPageFinished-------"+url);
        //html加載完成之後,添加監聽圖片的點擊js函數
        //addImageClickListener();
        addImageArrayClickListener(webView);
    }
  • 具體看addImageArrayClickListener的實現方法。

    /**
     * android與js交互:
     * 首先我們拿到html中加載圖片的標籤img.
     * 然後取出其對應的src屬性
     * 循環遍歷設置圖片的點擊事件
     * 將src作爲參數傳給java代碼
     * 這個循環將所圖片放入數組,當js調用本地方法時傳入。
     * 當然如果採用方式一獲取圖片的話,本地方法可以不需要傳入這個數組
     * 通過js代碼找到標籤爲img的代碼塊,設置點擊的監聽方法與本地的openImage方法進行連接
 */
private void addImageArrayClickListener(WebView webView) {
    webView.loadUrl("javascript:(function(){" +
            "var objs = document.getElementsByTagName(\"img\"); " +
            "var array=new Array(); " +
            "for(var j=0;j<objs.length;j++){" +
            "    array[j]=objs[j].src; " +
            "}"+
            "for(var i=0;i<objs.length;i++)  " +
            "{"
            + "    objs[i].onclick=function()  " +
            "    {  "
            + "        window.imagelistener.openImage(this.src,array);  " +
            "    }  " +
            "}" +
            "})()");
}
```
  • 最後看看js的通信接口做了什麼

    public class ImageJavascriptInterface {
    
        private Context context;
        private String[] imageUrls;
    
        public ImageJavascriptInterface(Context context,String[] imageUrls) {
            this.context = context;
            this.imageUrls = imageUrls;
        }
    
        public ImageJavascriptInterface(Context context) {
            this.context = context;
        }
    
        /**
     */
    @android.webkit.JavascriptInterface
    public void openImage(String img , String[] imageUrls) {
        Intent intent = new Intent();
        intent.putExtra("imageUrls", imageUrls);
        intent.putExtra("curImageUrl", img);
//        intent.setClass(context, PhotoBrowserActivity.class);
        context.startActivity(intent);
        for (int i = 0; i < imageUrls.length; i++) {
            Log.e("圖片地址"+i,imageUrls[i].toString());
        }
    }
}
```

3.1.9 頁面滑動期間不渲染/執行

  • 在有些需求中會有一些吸頂的元素,例如導航條,購買按鈕等;當頁面滾動超出元素高度後,元素吸附在屏幕頂部。在WebView中成了難題:在頁面滾動期間,Scroll Event不觸發。不僅如此,WebView在滾動期間還有各種限定:

    • setTimeout和setInterval不觸發。
    • GIF動畫不播放。
    • 很多回調會延遲到頁面停止滾動之後。
    • background-position: fixed不支持。
  • 這些限制讓WebView在滾動期間很難有較好的體驗。這些限制大部分是不可突破的,但至少對於吸頂功能還是可以做一些支持,解決方法:

    • 在Android上,監聽touchMove事件可以在滑動期間做元素的position切換(慣性運動期間就無效了)。
  • 參考美團技術文章

3.2.0 被運營商劫持和注入問題

  • 由於WebView加載的頁面代碼是從服務器動態獲取的,這些代碼將會很容易被中間環節所竊取或者修改,其中最主要的問題出自地方運營商和一些WiFi。監測到的問題包括:

    • 無視通信規則強制緩存頁面。
    • header被篡改。
    • 頁面被注入廣告。
    • 頁面被重定向。
    • 頁面被重定向並重新iframe到新頁面,框架嵌入廣告。
    • HTTPS請求被攔截。
    • DNS劫持。
  • 針對頁面注入的行爲,有一些解決方案:

    • 1.使用CSP(Content Security Policy)
    • 2.HTTPS。

      • HTTPS可以防止頁面被劫持或者注入,然而其副作用也是明顯的,網絡傳輸的性能和成功率都會下降,而且HTTPS的頁面會要求頁面內所有引用的資源也是HTTPS的,對於大型網站其遷移成本並不算低。HTTPS的一個問題在於:一旦底層想要篡改或者劫持,會導致整個鏈接失效,頁面無法展示。這會帶來一個問題:本來頁面只是會被注入廣告,而且廣告會被CSP攔截,而採用了HTTPS後,整個網頁由於受到劫持完全無法展示。
      • 對於安全要求不高的靜態頁面,就需要權衡HTTPS帶來的利與弊了。
    • 3.App使用Socket代理請求

      • 如果HTTP請求容易被攔截,那麼讓App將其轉換爲一個Socket請求,並代理WebView的訪問也是一個辦法。
      • 通常不法運營商或者WiFi都只能攔截HTTP(S)請求,對於自定義的包內容則無法攔截,因此可以基本解決注入和劫持的問題。
      • Socket代理請求也存在問題:
      • 首先,使用客戶端代理的頁面HTML請求將喪失邊下載邊解析的能力;根據前面所述,瀏覽器在HTML收到部分內容後就立刻開始解析,並加載解析出來的外鏈、圖片等,執行內聯的腳本……而目前WebView對外並沒有暴露這種流式的HTML接口,只能由客戶端完全下載好HTML後,注入到WebView中。因此其性能將會受到影響。
      • 其次,其技術問題也是較多的,例如對跳轉的處理,對緩存的處理,對CDN的處理等等……稍不留神就會埋下若干大坑。
      • 此外還有一些其他的辦法,例如頁面的MD5檢測,頁面靜態頁打包下載等等方式,具體如何選擇還要根據具體的場景抉擇。

3.2.1 解決資源加載緩慢問題

  • 在資源預加載方面,其實也有很多種方式,下面主要列舉了一些:

    • 第一種方式是使用 WebView 自身的緩存機制:如果我們在 APP 裏面訪問一個頁面,短時間內再次訪問這個頁面的時候,就會感覺到第二次打開的時候順暢很多,加載速度比第一次的時間要短,這個就是因爲 WebView 自身內部會做一些緩存,只要打開過的資源,他都會試着緩存到本地,第二次需要訪問的時候他直接從本地讀取,但是這個讀取其實是不太穩定的東西,關掉之後,或者說這種緩存失效之後,系統會自動把它清除,我們沒辦法進行控制。基於這個 WebView 自身的緩存,有一種資源預加載的方案就是,我們在應用啓動的時候可以開一個像素的 WebView ,事先去訪問一下我們常用的資源,後續打開頁面的時候如果再用到這些資源他就可以從本地獲取到,頁面加載的時間會短一些。
    • 第二種方案是,自己去構建和管理緩存:把這些需要預加載的資源放在 APP 裏面,可能是預先放進去的,也可能是後續下載的,問題在於前端這些頁面怎麼去緩存,兩個方案,第一種是前端可以在 H5 打包的時候把裏面的資源 URL 進行替換,這樣可以直接訪問本地的地址;第二種是客戶端可以攔截這些網頁發出的所有請求做替換。
    • 具體可以看美團的技術文章:美團大衆點評 Hybrid 化建設

3.2.2 判斷是否已經滾動到頁面底端

  • getScrollY()方法返回的是當前可見區域的頂端距整個頁面頂端的距離,也就是當前內容滾動的距離.
  • getHeight()或者getBottom()方法都返回當前WebView 這個容器的高度
  • getContentHeight 返回的是整個html的高度,但並不等同於當前整個頁面的高度,因爲WebView有縮放功能,所以當前整個頁面的高度實際上應該是原始html 的高度再乘上縮放比例. 因此,更正後的結果,準確的判斷方法應該是:

    if(WebView.getContentHeight*WebView.getScale() == (webview.getHeight()+WebView.getScrollY())){
        //已經處於底端
    }

3.2.3 使用loadData加載html亂碼

  • 可以通過使用 WebView.loadData(String data, String mimeType, String encoding)) 方法來加載一整個 HTML 頁面的一小段內容,第一個就是我們需要 WebView 展示的內容,第二個是我們告訴 WebView 我們展示內容的類型,一般,第三個是字節碼,但是使用的時候,這裏會有一些坑

    • 明明已經指定了編碼格式爲 UTF-8,加載卻還會出現亂碼……
    String html = new String("<h3>我是loadData() 的標題</h3><p>&nbsp&nbsp我是他的內容</p>");
    webView.loadData(html, "text/html", "UTF-8");
  • 使用loadData()或 loadDataWithBaseURL()加載一段HTML代碼片段

    • data:是要加載的數據類型,但在數據裏面不能出現英文字符:'#', '%', '' , '?' 這四個字符,如果有的話可以用 %23, %25, %27, %3f,這些字符來替換,在平時測試時,你的數據時,你的數據裏含有這些字符,但不會出問題,當出問題時,你可以替換下。

      • %,會報找不到頁面錯誤,頁面全是亂碼。亂碼樣式見符件。
      • ,會讓你的goBack失效,但canGoBAck是可以使用的。於是就會產生返回按鈕生效,但不能返回的情況。

      • 和? 我在轉換時,會報錯,因爲它會把當作轉義符來使用,如果用兩級轉義,也不生效,我是對它無語了。
    • 我們在使用loadData時,就意味着需要把所有的非法字符全部轉換掉,這樣就會給運行速度帶來很大的影響,因爲在使用時,在頁面stytle中會使用很多%號。頁面的數據越多,運行的速度就會越慢。
    • data中,有人會遇到中文亂碼問題,解決辦法:參數傳"utf-8",頁面的編碼格式也必須是utf-8,這樣編碼統一就不會亂了。別的編碼我也沒有試過。
  • 解決辦法

    String html = new String("<h3>我是loadData() 的標題</h3><p>&nbsp&nbsp我是他的內容</p>");
    webView.loadData(html, "text/html;charset=UTF-8", "null");

3.2.4 WebView下載進度無法監聽

3.2.5 webView出現302/303重定向

  • 專業敘述

    • 302重定向又稱之爲302代表暫時性轉移
  • 網絡解釋

    • 重定向是網頁製作中的一個知識,幾個例子跟你說明,假設你現在所處的位置是一個論壇的登錄頁面,你填寫了帳號,密碼,點擊登陸,如果你的帳號密碼正確,就自動跳轉到論壇的首頁,不正確就返回登錄頁;這裏的自動跳轉,就是重定向的意思。或者可以說,重定向就是,在網頁上設置一個約束條件,條件滿足,就自動轉入到其它網頁、網址 。比如,你輸入一個網站鏈接,一般可以直接進入網站,如果出現錯誤,則又跳轉到另外一個網頁。
  • 舉個例子

    • 敘述下這種問題的情況,就是WebView首先加載A鏈接,然後在WebView上點擊一個B鏈接進行加載,B鏈接會自動跳轉到C鏈接,這個時候調用WebView的goback方法,會返回到加載B鏈接,但是B鏈接又會跳轉到C鏈接,從而導致沒法返回到A鏈接界面(當然也有朋友說快速的按兩次返回鍵-也就是連續觸發了兩次goback可以返回到A鏈接,但並不是所有用戶都懂這個,而且操作上也很噁心。),這就是重定向問題。
  • 實現WebView的滑動監聽和優雅處理回退棧問題

    • WebView能否知道某個url是不是301/302呢?當然知道,WebView能夠拿到url的請求信息和響應信息,根據header裏的code很輕鬆就可以實現,事實正是如此,交給WebView來處理重定向(return false),這時候按返回鍵,是可以正常地回到重定向之前的那個頁面的。(PS:從上面的章節可知,WebView在5.0以後是一個獨立的apk,可以單獨升級,新版本的WebView實現肯定處理了重定向問題)
    • 但是,業務對url攔截有需求,肯定不能把所有的情況都交給系統WebView處理。爲了解決url攔截問題,本文引入了另一種思想——通過用戶的touch事件來判斷重定向。具體可以看項目lib中的ScrollWebView!

04.關於參考

05.關於x5開源庫YCWebView

5.0.1 前沿說明

  • 基於騰訊x5封源庫,提高webView開發效率,大概要節約你百分之六十的時間成本。該案例支持處理js的交互邏輯且無耦合、同時暴露進度條加載進度、可以監聽異常error狀態、支持視頻播放並且可以全頻、支持加載word,xls,ppt,pdf,txt等文件文檔、發短信、打電話、發郵件、打開文件操作上傳圖片、喚起原生App、x5庫爲最新版本,功能強大。

5.0.2 該庫功能和優勢

  • 提高webView開發效率,大概要節約你百分之六十的時間成本,一鍵初始化操作;
  • 支持處理js的交互邏輯,方便快捷,並且無耦合,操作十分簡單;
  • 暴露進度條加載進度,結束,以及異常狀態(分多種狀態:無網絡,404,onReceivedError,sslError異常等)listener給開發者;
  • 支持視頻播放,可以切換成全頻播放視頻,可旋轉屏幕,暴露視頻操作監聽listener給開發者;
  • 集成了騰訊x5的WebView,最新版本,功能強大;
  • 支持打開文件的操作,比如打開相冊,然後選中圖片上傳,兼容版本(5.0);
  • 支持加載word,xls,ppt,pdf,txt等文件文檔,使用方法十分簡單;
  • 支持設置仿微信加載H5頁面進度條,完全無耦合,操作簡單,極大提高用戶體驗;

5.0.3 項目地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章