天天看點

x5開源庫後續知識點,會讓你的goBack失效,但canGoBAck是可以使用的。于是就會産生傳回按鈕生效,但不能傳回的情況。

目錄介紹

  • 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/yangchong211/YCWebView
  • 該後續知識點,幾乎包含了實際開發中絕大多數的問題,再次學習和鞏固webView,希望這篇文章對你有用……更多内容,可以看我的開源項目,如果覺得給你帶來一些收獲,麻煩star一下,這也可以增加開發者開源項目的動力!

  • 在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 整體加載時間變長,消耗的流量也對應的真多。

  • 第一種方式: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)代碼

  • 第一種方式:通過 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);
    }           
    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 對話框方法可以傳回任意類型的傳回值,缺點就是協定的制定比較麻煩,需要記錄詳細的文檔,但是不會存在第二種方法的漏洞問題。

  • 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)判斷是否可以回退。

  • 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已經解析了标簽,這時候注入一定是成功的。在WebViewClient.onProgressChanged()實作js注入有幾個需要注意的地方:
      • 1 上文提到的多次注入控制,使用了boolean值變量控制
      • 2 重新加載一個URL之前,需要重置boolean值變量,讓重新加載後的頁面再次注入js
      • 3 如果做過本地js,css等緩存,則先判斷本地是否存在,若存在則加載本地,否則加載網絡js
      • 4 注入的進度門檻值可以自由定制,理論上10%-100%都是合理的,不過建議使用了75%到90%之間可以。

  • 清除緩存資料的方法有哪些?
    //清除網頁通路留下的緩存
    //由于核心緩存是全局的是以這個方法不僅僅針對webview而是針對整個應用程式.
    Webview.clearCache(true);
    
    //清除目前webview通路的曆史記錄//隻會webview通路曆史記錄裡的所有記錄除了目前通路記錄
    Webview.clearHistory();
    
    //這個api僅僅清除自動完成填充的表單資料,并不會清除WebView存儲到本地的資料
    Webview.clearFormData();           

  • 微信裡的文章頁面,可以選擇“在浏覽器打開”。現在很多應用都内嵌了WebView,那是否可以使自己的應用作為第三方浏覽器打開此文章呢?
  • 在Manifest檔案中,給想要接收跳轉的Activity添加配置:
    <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。
         * 否則新的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"。

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

  • 至于原因暫時沒有找到,解決方案如下所示
    /**
     * 當縮放改變的時候會調用該方法
     * @param view                              view
     * @param oldScale                          之前的縮放比例
     * @param newScale                          現在縮放比例
     */
    @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
     * @param url                               url連結
     */
    @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
     * @param failingUrl                        失敗連結
     */
    @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
        }
    }           

  • 4.0以上的系統我們開啟硬體加速後,WebView渲染頁面更加快速,拖動也更加順滑。但有個副作用就是,當WebView視圖被整體遮住一塊,然後突然恢複時(比如使用SlideMenu将WebView從側邊滑出來時),這個過渡期會出現白塊同時界面閃爍。解決這個問題的方法是在過渡期前将WebView的硬體加速臨時關閉,過渡期後再開啟
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        webview.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
    }           

  • webView加載一些别人的url時候,有時候會發生證書認證錯誤的情況,這時候我們希望能夠正常的呈現頁面給使用者,我們需要忽略證書錯誤,需要調用WebViewClient類的onReceivedSslError方法,調用handler.proceed()來忽略該證書錯誤。
    /**
     * 在加載資源時通知主機應用程式發生SSL錯誤
     * 作用:處理https請求
     * @param view                              view
     * @param handler                           handler
     * @param error                             error
     */
    @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);    //可做其他處理
        }
    }           

  • 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();
    }           

  • 建立連接配接/伺服器處理;在頁面請求的資料傳回之前,主要有以下過程耗費時間。
    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。靜态資源同理,最好與用戶端的資源域名保持一緻。

  • 用戶端内的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 {
                //合法網址
            }
        }
    }           
  • 設定白名單操作其實和過濾廣告是一個意思,這裡你可以放一些合法的網址允許通路。

  • 在有些手機你如果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);
        }
    }           

  • 提前顯示進度條不是提升性能 , 但是對使用者體驗來說也是很重要的一點 , 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) {
    
        }
    };           

  • WebView 預設開啟密碼儲存功能 mWebView.setSavePassword(true),如果該功能未關閉,在使用者輸入密碼時,會彈出提示框,詢問使用者是否儲存密碼,如果選擇”是”,密碼會被明文保到 /data/data/com.package.name/databases/webview.db 中,這樣就有被盜取密碼的危險,是以需要通過 WebSettings.setSavePassword(false) 關閉密碼儲存提醒功能。
    • 具體代碼操作如下所示
    /設定是否開啟密碼儲存功能,不建議開啟,預設已經做了處理,存在盜取密碼的危險
    mX5WebView.setSavePassword(false);           

  • 進化史如下所示
    • 從Android4.4系統開始,Chromium核心取代了Webkit核心。
    • 從Android5.0系統開始,WebView移植成了一個獨立的apk,可以不依賴系統而獨立存在和更新。
    • 從Android7.0 系統開始,如果使用者手機裡安裝了 Chrome , 系統優先選擇 Chrome 為應用提供 WebView 渲染。
    • 從Android8.0系統開始,預設開啟WebView多程序模式,即WebView運作在獨立的沙盒程序中。

  • 第一次打開Web面 ,使用WebView加載頁面的時候特别慢,第二次打開就能明顯的感覺到速度有提升,為什麼?
    • 是因為在你第一次加載頁面的時候 WebView 核心并沒有初始化 ,是以在第一次加載頁面的時候需要耗時去初始化WebView核心 。
    • 提前初始化WebView核心 ,例如如下把它放到了Application裡面去初始化 , 在頁面裡可以直接使用該WebView,這種方法可以比較有效的減少WebView在App中的首次打開時間。當使用者通路頁面時,不需要初始化WebView的時間。
    • 但是這樣也有不好的地方,額外的記憶體消耗。頁面間跳轉需要清空上一個頁面的痕迹,更容易記憶體洩露。

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

  • 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);這樣就可以控制視訊的暫停和繼續播放了。

  • 偶發情況,擷取不到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/ad22b2649fba

  • 常見的用法是在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
     * @param failingUrl                                        failingUrl
     */
    @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);
        }
    }           

  • 視訊播放寬度或者圖檔寬度比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);           

  • 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注解的函數。這樣就可以增強安全性。

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

  • h5頁面為何要設定cookie,主要是避免網頁重複登入,作用是記錄使用者登入資訊,下次進去不需要重複登入。
  • 代碼裡怎麼設定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 callback)
    • 清除所有Cookie:CookieManager.getInstance().removeAllCookies(ValueCallback callback)
    • Cookie持久化:CookieManager.getInstance().flush()
    • 針對某個主機設定Cookie:CookieManager.getInstance().setCookie(String url, String value)

  • 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);           

  • 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);
    }           

  • 如果webView.getSettings().setAllowFileAccess(boolean)設定為true,則會面臨該問題;該漏洞是通過WebView對Javascript的延時執行和html檔案替換産生的。
    • 解決方案是禁止WebView頁面打開本地檔案,即:webView.getSettings().setAllowFileAccess(false);
    • 或者更直接的禁止使用JavaScript:webView.getSettings().setJavaScriptEnabled(false);

  • 問題描述
    • 當 WebView 嵌套在 ScrollView 裡面的時候,如果 WebView 先加載了一個高度很高的網頁,然後加載了一個高度很低的網頁,就會造成 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方法進行連接配接
     * @param webView                       webview
     */
    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());
            }
        }
    }           

  • 在有些需求中會有一些吸頂的元素,例如導覽列,購買按鈕等;當頁面滾動超出元素高度後,元素吸附在螢幕頂部。在WebView中成了難題:在頁面滾動期間,Scroll Event不觸發。不僅如此,WebView在滾動期間還有各種限定:
    • setTimeout和setInterval不觸發。
    • GIF動畫不播放。
    • 很多回調會延遲到頁面停止滾動之後。
    • background-position: fixed不支援。
  • 這些限制讓WebView在滾動期間很難有較好的體驗。這些限制大部分是不可突破的,但至少對于吸頂功能還是可以做一些支援,解決方法:
    • 在Android上,監聽touchMove事件可以在滑動期間做元素的position切換(慣性運動期間就無效了)。
  • 參考美團技術文章

  • 由于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檢測,頁面靜态頁打包下載下傳等等方式,具體如何選擇還要根據具體的場景抉擇。

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

  • getScrollY()方法傳回的是目前可見區域的頂端距整個頁面頂端的距離,也就是目前内容滾動的距離.
  • getHeight()或者getBottom()方法都傳回目前WebView 這個容器的高度
  • getContentHeight 傳回的是整個html的高度,但并不等同于目前整個頁面的高度,因為WebView有縮放功能,是以目前整個頁面的高度實際上應該是原始html 的高度再乘上縮放比例. 是以,更正後的結果,準确的判斷方法應該是:
    if(WebView.getContentHeight*WebView.getScale() == (webview.getHeight()+WebView.getScrollY())){
        //已經處于底端
    }           

  • 可以通過使用 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");           

  • 專業叙述
    • 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 項目位址