目錄介紹
- 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 整體加載時間變長,消耗的流量也對應的真多。
- 影響頁面加載速度的因素有非常多,在對 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); }
- 這種方式其實實作也很簡單,使用的頻次也很高,上面介紹到了 WebViewClient ,其中有個回調接口 shouldOverrideUrlLoading (WebView view, String url)) ,就是利用這個攔截 url,然後解析這個 url 的協定,如果發現是我們預先約定好的協定就開始解析參數,執行相應的邏輯。注意這個方法在 API24 版本已經廢棄了,需要使用 shouldOverrideUrlLoading (WebView view, WebResourceRequest request)) 替代,使用方法很類似,我們這裡就使用 shouldOverrideUrlLoading (WebView view, String url)) 方法來介紹一下:
- 第三種方式:利用 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%之間可以。
- WebViewClient.onProgressChanged()這個方法在dom樹渲染的過程中會回調多次,每次都會告訴我們目前加載的進度。
- 清除緩存資料的方法有哪些?
//清除網頁通路留下的緩存 //由于核心緩存是全局的是以這個方法不僅僅針對webview而是針對整個應用程式. Webview.clearCache(true); //清除目前webview通路的曆史記錄//隻會webview通路曆史記錄裡的所有記錄除了目前通路記錄 Webview.clearHistory(); //這個api僅僅清除自動完成填充的表單資料,并不會清除WebView存儲到本地的資料 Webview.clearFormData();
- 具體可以看這篇文章: https://www.jianshu.com/p/127c80f62655
- 微信裡的文章頁面,可以選擇“在浏覽器打開”。現在很多應用都内嵌了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 字段設定為正常的過期日期,則到期後再資料庫更新時會删除該條資料。
- 可以設定Cookie的生效時間字段名為: expires 或 max-age。
- 下面列出幾個有用的接口:
- 擷取某個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>  我是他的内容</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,這樣編碼統一就不會亂了。别的編碼我也沒有試過。
- data:是要加載的資料類型,但在資料裡面不能出現英文字元:'#', '%', '' , '?' 這四個字元,如果有的話可以用 %23, %25, %27, %3f,這些字元來替換,在平時測試時,你的資料時,你的資料裡含有這些字元,但不會出問題,當出問題時,你可以替換下。
-
String html = new String("<h3>我是loadData() 的标題</h3><p>  我是他的内容</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頁面進度條,完全無耦合,操作簡單,極大提高使用者體驗;