*本文介紹自己在使用WebView的過程中遇到的一些問題的解決方法和對WebView的一些優化實踐*
*瀏覽器緩存知識介紹:*
瀏覽器緩存之 Expires , max-age, Etag , Last-Modified (其中Expires,max-age是客戶端在這個時間之前不去向服務器端發送請求驗證資源是否有更新,
Etag, Last-Modified是服務器決定是否需要返回資源,未更新的資源不需要返回)
Expires
http/1.0中定義的header,是最基礎的瀏覽器緩存處理,表示資源在一定時間內從瀏覽器的緩存中獲取資源,不需要請求服務器驗證獲取資源,從而達到快速獲取資源,緩解服務器壓力的目的。
在response的header中的格式為:Expires: Thu, 01 Dec 1994 16:00:00 GMT (必須是GMT格式)
應用:
1、可以在html頁面中添加<meta http-equiv="Expires" content="Thu, 01 Dec 1994 16:00:00"/> 來給頁面設置緩存時間。
2、對于圖片、css等文件則需要在IIS或者apache等運行容器中進行規則配置來讓容器在請求資源的時候添加在responese的header中。
max-age
Cache-Control中設置資源在本地緩存時間的一個值,單位為:秒(s),其他值還有private、no-cache、must-revalidate等
Last-modified
望文知義,根據這個詞條的直譯應該是上次修改(時間),通過修改服務器端的文件后再請求,發現response的header中的Last-modified改變了
更新原理:
1、在瀏覽器首次請求某個資源時,服務器端返回的狀態碼是200 (ok),內容是你請求的資源,同時有一個Last-Modified的屬性標記(Reponse Header),標識此文件在服務期端最后被修改的時間,格式:Last-Modified:Tue, 24 Feb 2009 08:01:04 GMT
2、瀏覽器第二次請求該資源時,根據HTTP協議的規定,瀏覽器會向服務器傳送If-Modified-Since報頭(Request Header),詢問該文件是否在指定時間之后有被修改過,格式為:If-Modified-Since:Tue, 24 Feb 2009 08:01:04 GMT
3、如果服務器端的資源沒有變化,則服務器返回304狀態碼(Not Modified),內容為空,這樣就節省了傳輸數據量。當服務器端代碼發生改變,則服務器返回200狀態碼(ok),內容為請求的資源,和第一次請求資源時類似。從而保證在資源沒有修改時不向客戶端重復發出資源,也保證當服務器有變化時,客戶端能夠及時得到最新的資源。
注:如果If-Modified-Since的時間比服務器當前時間(當前的請求時間request_time)還晚,會認為是個非法請求
ETag
http/1.1 中增加的header,HTTP協議規格說明定義ETag為“被請求變量的實體值” 。另一種說法是,ETag是一個可以與Web資源關聯的記號(token)。典型的Web資源可以一個Web頁,但也可能是JSON或XML文檔。服務器單獨負責判斷記號是什么及其含義,并在HTTP響應頭中將其傳送到客戶端。
ETag的格式
不同類型的Web服務器生成ETag的策略以及生成的格式是不同的:
1、apache1.3和2.x的Etag格式是:inode-size-timestamp。
2、IIS5.0和6.0的Etag格式為Filetimestamp:Changenumber。
更新原理:
1、當瀏覽器首次請求資源的時候,服務器會返回200的狀態碼(ok),內容為請求的資源,同時response header會有一個ETag標記,該標記是服務器端根據容器(IIS或者Apache等等)中配置的ETag生成策略生成的一串唯一標識資源的字符串,ETag格式為 ETag:"856247206"
2、當瀏覽器第2次請求該資源時,瀏覽器會在傳遞給服務器的request中添加If-None-Match報頭,詢問服務器改文件在上次獲取后是否修改了,報頭格式:If-None-Match:"856246825"
3、服務器在獲取到瀏覽器的請求后,會根據請求的資源查找對應的ETag,將當前服務器端指定資源對應的Etag與request中的If-None-Match進行對比,如果相同,說明資源沒有修改,服務器返回304狀態碼(Not Modified),內容為空;如果對比發現不相同,則返回200狀態碼,同時將新的Etag添加到返回瀏覽器的response中。
幾者之間的關系
Expires 與max-age
Expires存在HTTP 1.0 版本, 標識本地緩存的截止時間,允許客戶端在這個時間之前不去向服務器端發送請求驗證資源是否有更新
max-age是HTTP 1.1版本新增的, 標識資源可以在本地緩存多少秒,存儲的是更新間隔。
Expires 的一個缺點就是,返回的到期時間是服務器端的時間,這樣存在一個問題,如果瀏覽器所在機器的時間與服務器的時間相差很大,那么誤差就很大,所以在HTTP 1.1版開始,使用Cache-Control: max-age替代。
注: 如果max-age和Expires同時存在,則被Cache-Control的max-age覆蓋。
Expires =max-age + “每次下載時的當前的request時間”
所以一旦重新下載的頁面后,expires就重新計算一次,但last-modified不會變化
Last-Modified和Expires
使用Last-Modified標識在資源未修改時返回的response內容為空,可以節省一點帶寬,但是還是逃不掉發一個HTTP請求出去,需要瀏覽器連接一次服務器端。
而Expires標識卻使得瀏覽器干脆連HTTP請求都不用發,但是當用戶強制刷新的時候,就算URI設置了Expires,瀏覽器也會發一個HTTP請求給服務器端驗證資源的更新,所以,Last-Modified還是要用的,而且要和Expires一起用。
Etag和Expires
和 Last-Modified和Expires的情況類似,需要Expires控制請求的頻率,Etag在強制刷新時作為驗證資源是否更新
Last-Modified和Etag
分布式系統里多臺機器間文件的last-modified必須保持一致,以免負載均衡到不同機器導致比對失敗
分布式系統盡量關閉掉Etag(每臺機器生成的etag都會不一樣)
Last-Modified和ETags請求的http報頭一起使用,服務器首先產生Last-Modified/Etag標記,服務器可在稍后使用它來判斷頁面是否已經被修改,來決定文件是否繼續緩存
過程如下:
1.客戶端請求一個頁面(A)。
2.服務器返回頁面A,并在給A加上一個Last-Modified/ETag。
3.客戶端展現該頁面,并將頁面連同Last-Modified/ETag一起緩存。
4.客戶再次請求頁面A,并將上次請求時服務器返回的Last-Modified/ETag一起傳遞給服務器。
5.服務器檢查該Last-Modified或ETag,并判斷出該頁面自上次客戶端請求之后還未被修改,直接返回響應304和一個空的響應體。
注:
1、Last-Modified和Etag頭都是由WebServer發出的HttpReponse Header,WebServer應該同時支持這兩種頭。
2、WebServer發送完Last-Modified/Etag頭給客戶端后,客戶端會緩存這些頭;
3、客戶端再次發起相同頁面的請求時,將分別發送與Last-Modified/Etag對應的HttpRequestHeader:If-Modified-Since和If-None-Match。我們可以看到這兩個Header的值和WebServer發出的Last-Modified,Etag值完全一樣;
4、通過上述值到服務器端檢查,判斷文件是否繼續緩存;
從資源更新原理來看Last-Modified和Etag基本是類似的,那為什么http協議中要搞2個標識呢?
Last-Modified存在的問題:
1、在集群服務器上各個服務器上的文件時間可能不同。
2、如果用舊文件覆蓋新文件,因為時間更前,瀏覽器不會請求這個更舊的文件。
3、時間精度為s級,對文件修改精度有嚴格要求的場景不能滿足
為什么使用Etag請求頭?
Etag 主要為了解決 Last-Modified 無法解決的一些問題。
1、一些文件也許會周期性的更改,但是他的內容并不改變(僅僅改變的修改時間),這個時候我們并不希望客戶端認為這個文件被修改了,而重新GET;
2、某些文件修改非常頻繁,比如在秒以下的時間內進行修改,(比方說1s內修改了N次),If-Modified-Since能檢查到的粒度是s級的,這種修改無法判斷(或者說UNIX記錄MTIME只能精確到秒)
3、某些服務器不能精確的得到文件的最后修改時間;
為此,HTTP/1.1引入了 Etag(Entity Tags).Etag僅僅是一個和文件相關的標記,可以是一個版本標記,比如說v1.0.0或者說"2e681a-6-5d044840"這么一串看起來很神秘的編碼。但是HTTP/1.1標準并沒有規定Etag的內容是什么或者說要怎么實現,唯一規定的是Etag需要放在""內。尤其是在做斷點下載/續傳時,表現得比較明顯
頁面加載速度優化
影響頁面加載速度的因素有非常多,我們在對 WebView 加載一個網頁的過程進行調試發現,每次加載的過程中都會有較多的網絡請求,除了 web 頁面自身的 URL 請求,還會有 web 頁面外部引用的JS、CSS、字體、圖片等等都是個獨立的 http 請求。這些請求都是串行的,這些請求加上瀏覽器的解析、渲染時間就會導致 WebView 整體加載時間變長,消耗的流量也對應的真多。接下來我們就來說說幾種優化方案來是怎么解決這個問題的。
選擇合適的 WebView 緩存
WebView 緩存看似就是開啟幾個開關的問題,但是要弄懂這幾種緩存機制還是很有深度。下圖是騰訊某工程師總結六種 H5 常用的緩存機制的優勢及適用場景。
瀏覽器緩存機制:
主要前端負責,Android 端不需要進行特別的配置。
Dom Storage(Web Storage)存儲機制:
配合前端使用,使用時需要打開 DomStorage 開關。
WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setDomStorageEnabled(true);
Web SQL Database 存儲機制:
雖然已經不推薦使用了,但是為了兼容性,還是提供下 Android 端使用的方法
WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setDatabaseEnabled(true);
final String dbPath = getApplicationContext().getDir("db",Context.MODE_PRIVATE).getPath();
webSettings.setDatabasePath(dbPath)
Application Cache 存儲機制
Application Cache(簡稱 AppCache)似乎是為支持 Web App 離線使用而開發的緩存機制。它的緩存機制類似于瀏覽器的緩存(Cache-Control 和 Last-Modified)機制,都是以文件為單位進行緩存,且文件有一定更新機制。但 AppCache 是對瀏覽器緩存機制的補充,不是替代。
不過根據官方文檔,AppCache 已經不推薦使用了,標準也不會再支持。現在主流的瀏覽器都是還支持 AppCache的,以后就不太確定了。同樣給出 Android 端啟用 AppCache 的代碼。
WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setAppCacheEnabled(true);
final String cachePath = getApplicationContext().getDir("cache",Context.MODE_PRIVATE).getPath();
webSettings.setAppCachePath(cachePath);
webSettings.setAppCacheMaxSize(510241024);
Indexed Database 存儲機制
IndexedDB 也是一種數據庫的存儲機制,但不同于已經不再支持的 Web SQL Database。IndexedDB 不是傳統的關系數據庫,可歸為 NoSQL 數據庫。IndexedDB 又類似于 Dom Storage 的 key-value 的存儲方式,但功能更強大,且存儲空間更大。
Android 在4.4開始加入對 IndexedDB 的支持,只需打開允許 JS 執行的開關就好了。
WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setJavaScriptEnabled(true);
File System API
File System API 是 H5 新加入的存儲機制。它為 Web App 提供了一個虛擬的文件系統,就像 Native App 訪問本地文件系統一樣。由于安全性的考慮,這個虛擬文件系統有一定的限制。Web App 在虛擬的文件系統中,可以進行文件(夾)的創建、讀、寫、刪除、遍歷等操作。很可惜到目前,Android 系統的 WebView 還不支持 File System API。
簡單的介紹完了上面六種 H5 常用的緩存模式,想必大家能對 Android WebView 所支持的緩存模式有個粗略的了解。如果想和前端更好的配合使用 Android WebView 所支持的緩存,建議看下這篇文章《H5 緩存機制淺析 移動端 Web 加載性能優化》
*常用資源預加載:*
上面介紹的緩存技術,能優化二次啟動 WebView 的加載速度,那首次加載 H5 頁面的速度該怎么優化呢?上面分析了一次加載過程會有許多外部依賴的 JS、CSS、圖片等資源需要下載,那我們能不能提前將這些資源下載好,等H5 加載時直接替換呢?
好在從 API 11(Android 3.0)開始,WebView 引入了 shouldInterceptRequest 函數,這個函數有兩種重載。
public WebResourceResponse shouldInterceptRequest(WebView webView, String url) 從 API 11 引入,API 21 廢棄
public WebResourceResponse shouldInterceptRequest (WebView view, WebResourceRequest request) 從 API 21 開始引入
考慮到目前大多數 App 還要支持 API 14,所以還是使用 shouldInterceptRequest (WebView view, String url) 為例。
WebView mWebView = (WebView) findViewById(R.id.webview);
mWebView.setWebViewClient(new WebViewClient() {
@Override
public WebResourceResponse shouldInterceptRequest(WebView webView, final String url) {
WebResourceResponse response = null;
// 檢查該資源是否已經提前下載完成。我采用的策略是在應用啟動時,用戶在 wifi 的網絡環境下 // 提前下載 H5 頁面需要的資源。
boolean resDown = JSHelper.isURLDownValid(url);
if (resDown) {
jsStr = JsjjJSHelper.getResInputStream(url);
if (url.endsWith(".png")) {
response = getWebResourceResponse(url, "image/png", ".png");
} else if (url.endsWith(".gif")) {
response = getWebResourceResponse(url, "image/gif", ".gif");
} else if (url.endsWith(".jpg")) {
response = getWebResourceResponse(url, "image/jepg", ".jpg");
} else if (url.endsWith(".jepg")) {
response = getWebResourceResponse(url, "image/jepg", ".jepg");
} else if (url.endsWith(".js") && jsStr != null) {
response = getWebResourceResponse("text/javascript", "UTF-8", ".js");
} else if (url.endsWith(".css") && jsStr != null) {
response = getWebResourceResponse("text/css", "UTF-8", ".css");
} else if (url.endsWith(".html") && jsStr != null) {
response = getWebResourceResponse("text/html", "UTF-8", ".html");
}
}
// 若 response 返回為 null , WebView 會自行請求網絡加載資源。
return response;
}
});
private WebResourceResponse getWebResourceResponse(String url, String mime, String style) {
WebResourceResponse response = null;
try {
response = new WebResourceResponse(mime, "UTF-8", new FileInputStream(new File(getJSPath() + TPMD5.md5String(url) + style)));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return response;
}
public String getJsjjJSPath() {
String splashTargetPath = JarEnv.sApplicationContext.getFilesDir().getPath() + "/JS";
if (!TPFileSysUtil.isDirFileExist(splashTargetPath)) {
TPFileSysUtil.createDir(splashTargetPath);
}
return splashTargetPath + "/";
}
**
*1:常用 JS 本地化及延遲加載***
**
**資源等文件(不需要更新)本地存儲,在需要的時候直接從本地獲取。哪些資源需要我們去存儲在本地呢,當然是一些不會被更新的資源,例如圖片文件,js文件,css文件,比預加載更粗暴的優化方法是直接將常用的 JS 腳本本地化,直接打包放入 apk 中。比如 H5 頁面獲取用戶信息,設置標題等通用方法,就可以直接寫入一個 JS 文件,放入 asserts 文件夾,在 WebView 調用了onPageFinished() 方法后進行加載。需要注意的是,在該 JS 文件中需要寫入一個 JS 文件載入完畢的事件,這樣前端才能接受都愛 JS 文件已經種植完畢,可以調用 JS 中的方法了。 附上一段本地化的 JS 代碼。
javascript: ;
(function() {
try{
window.JSBridge = {
'invoke': function(name) {
var args = [].slice.call(arguments, 1),
callback = args.pop(),
params, obj = this[name];
if (typeof callback !== 'function') {
params = callback;
callback = function() {}
} else {
params = args[0]
} if (typeof obj !== 'object' || typeof obj.func !== 'function') {
callback({
'err_msg': 'system:function_not_exist'
});
return
}
obj.callback = callback;
obj.params = params;
obj.func(params)
},
'on': function(event, callback) {
var obj = this['on' + event];
if (typeof obj !== 'object') {
callback({
'err_msg': 'system:function_not_exist'
});
retrun
}
if (typeof callback !== 'undefined') obj.callback = callback
},
'login': {
'func': function(params) {
prompt("login", JSON.stringify(params))
},
'params': {},
'callback': function(res) {}
},
'settitle': {
'func': function(params) {
prompt("settitle",JSON.stringify(params))
},
'params': {},
'callback': function(res) {}
},
}catch(e){
alert('demo.js error:'+e);
}
var readyEvent = document.createEvent('Events');
readyEvent.initEvent('JSBridgeReady', true, true);
document.dispatchEvent(readyEvent)
})();
關于 JS 延遲加載
Android 的 OnPageFinished 事件會在 Javascript 腳本執行完成之后才會觸發。如果在頁面中使 用JQuery,會在處理完 DOM 對象,執行完 $(document).ready(function() {}); 事件自會后才會渲染并顯示頁面。而同樣的頁面在 iPhone 上卻是載入相當的快,因為 iPhone 是顯示完頁面才會觸發腳本的執行。所以我們這邊的解決方案延遲 JS 腳本的載入,這個方面的問題是需要Web前端工程師幫忙優化的。
**
*2:使用第三方 WebView 內核***
WebView 的兼容性一直也是困擾我們 Android 開發者的一個大問題,不說 Android 4.4 版本 Google 使用了Chromium 替代 Webkit 作為 WebView 內核,就看看國內眾多的第三方 ROM 都有可能會對原生的 WebView 做出修改,這時候如果出現兼容問題,是非常難定位到問題和解決的。
在一次使用微信瀏覽訂閱公眾號文章的過程中,發現微信的 H5 頁面有一行 『QQ 瀏覽器 X5 內核提供技術支持』。順著這個線索我就找到了騰訊瀏覽服務。發現騰訊已經把這個功能開放了,而且集成的 SDK 很小只有212 KB。這是很驚人的,通過介紹才發現這個 SDK 是可以共享微信和手機 QQ 的 X5 內核。這就很方便了,作為國內市場最不可或缺的兩個 App,我們能只需要集成一個很小的 SDK 就可以共享使用 X5 內核了,不得不說騰訊還是很有想法的。
簡單摘錄些功能亮點,想必能讓大家高潮一番。詳細內容大家可以直接到騰訊瀏覽服務看看,我相信不會讓你們失望的。
網頁瀏覽能力
Web頁面crash率降低75%
頁面打開速度提升35%
流量節省60%
閱讀模式
去除網頁中廣告等雜質
優化文章的閱讀體驗
文件打開能力
包括會話頁的互傳文件及郵件中附件
支持doc、ppt、xls、pdf等辦公格式
支持jpg、gif、png、bmp等圖片格式
支持zip、rar等壓縮文件
支持mp3、mp4、RMVB等音視頻格式
視頻菜單能力
支持屏幕調節等常規視頻菜單功能
靈活切換全屏&小窗功能
*3:加快HTML網頁裝載完成的速度*
默認情況html代碼下載到WebView后,webkit開始解析網頁各個節點,發現有外部樣式文件或者外部腳本文件時,會異步發起網絡請求下載文件,但如果在這之前也有解析到image節點,那勢必也會發起網絡請求下載相應的圖片。在網絡情況較差的情況下,過多的網絡請求就會造成帶寬緊張,影響到css或js文件加載完成的時間,造成頁面空白loading過久。解決的方法就是告訴WebView先不要自動加載圖片,等頁面finish后再發起圖片加載。
故在WebView初始化時設置如下代碼:
public void int () {
if(Build.VERSION.SDK_INT >= 19) {
webView.getSettings().setLoadsImagesAutomatically(true);
} else {
webView.getSettings().setLoadsImagesAutomatically(false);
}
}
同時在WebView的WebViewClient實例中的onPageFinished()方法添加如下代碼:
@Override
public void onPageFinished(WebView view, String url) {
if(!webView.getSettings().getLoadsImagesAutomatically()) {
webView.getSettings().setLoadsImagesAutomatically(true);
}
}
從上面的代碼,可以看出我們對系統API在19以上的版本作了兼容。因為4.4以上系統在onPageFinished時再恢復圖片加載時,如果存在多張圖片引用的是相同的src時,會只有一個image標簽得到加載,因而對于這樣的系統我們就先直接加載。
**
*4:自定義出錯界面***
當WebView加載頁面出錯時(一般為404 NOT FOUND),安卓WebView會默認顯示一個賣萌的出錯界面。但我們怎么能不讓用戶發現原來我使用的是網頁應用呢,我們期望的是用戶在網頁上得到是如原生般應用的體驗,那就先要從干掉這個默認出錯頁面開始。當WebView加載出錯時,我們會在WebViewClient實例中的onReceivedError()方法接收到錯誤,我們就在這里做些手腳:
@Override
public void onReceivedError (WebView view, int errorCode, String description, String failingUrl) {
super.onReceivedError(view, errorCode, description, failingUrl);
loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
mErrorFrame.setVisibility(View.VISIBLE);
}
從上面可以看出,我們先使用loadDataWithBaseURL清除掉默認錯誤頁內容,再讓我們自定義的View得到顯示(mErrorFrame為蒙在WebView之上的一個LinearLayout布局,默認為View.GONE)。
遠程網頁需訪問本地資源
當我們在WebView中加載出從web服務器上拿取的內容時,是無法訪問本地資源的,如assets目錄下的圖片資源,因為這樣的行為屬于跨域行為(Cross-Domain),而WebView是禁止的。解決這個問題的方案是把html內容先下載到本地,然后使用loadDataWithBaseURL加載html。這樣就可以在html中使用 file:///android_asset/xxx.png 的鏈接來引用包里面assets下的資源了。示例如下:
private void loadWithAccessLocal(final String htmlUrl) {
new Thread(new Runnable() {
public void run() {
try {
final String htmlStr = NetService.fetchHtml(htmlUrl);
if (htmlStr != null) {
TaskExecutor.runTaskOnUiThread(new Runnable() {
@Override
public void run() {
loadDataWithBaseURL(htmlUrl, htmlStr, "text/html", "UTF-8", "");
}
});
return;
}
} catch (Exception e) {
Log.e("Exception:" + e.getMessage());
}
TaskExecutor.runTaskOnUiThread(new Runnable() {
@Override
public void run() {
onPageLoadedError(-1, "fetch html failed");
}
});
}
}).start();
}
上面有幾點需要注意:
?從網絡上下載html的過程應放在工作線程中
?html下載成功后渲染出html的步驟應放在UI主線程,不然WebView會報錯
?html下載失敗則可以使用我們前面講述的方法來顯示自定義錯誤界面
5:WebView 導致的內存泄露
Android 中的 WebView 存在很大的兼容性問題,不僅僅是 Android 系統版本的不同對 WebView 產生很大的差異,另外不同的廠商出貨的 ROM 里面 WebView 也存在著很大的差異。更嚴重的是標準的 WebView 存在內存泄露的問題,看這里WebView causes memory leak - leaks the parent Activity。所以通常根治這個問題的辦法是為 WebView 開啟另外一個進程,通過 AIDL 與主進程進行通信,WebView 所在的進程可以根據業務的需要選擇合適的時機進行銷毀,從而達到內存的完整釋放。
這段話來自胡凱翻譯的 Google Android 內存優化之 OOM 。這里提到的讓 WebView 獨立運行在一個進程里,用完 WebView 后直接銷毀這個進程,即使內存泄露了,也不會影響到主進程。微信,手 Q 等 App 也采用了這個方案。但是這就涉及到了跨進程通訊,處理起來就比較麻煩。
另外個解決方案,就是使用自己封裝的 WebView,比如上面提到的 X5 內核,且使用 WebView 的時候,不在 XML 里面聲明,而是在代碼中直接 new 出來,傳入 application context 來防止 activity 引用被濫用。
WebView webView = new WebView(getContext().getApplicationContext();
webFrameLayout.addView(webView, 0);
在使用了這個方式后,基本上 90% 的 WebView 內存泄漏的問題便得以解決。
6:客戶端UI優化
怎么讓用戶看不到WebView加載前的白色頁面呢?首次加載后頁面的跳轉可以用上面的步驟進行優化,可以提供給用戶一個很好的體驗,那加載的第一頁呢?我們需要WebView預加載頁面,這個該怎么做到的呢?下面提供兩種方法:
ViewPager,將歡迎頁面與WebView頁面一起放進ViewPager中,設置預加載頁面個數,使WebView所在頁面可以預加載,在加載完畢的時候切換到WebView所在頁面。
FrameLayout,將歡迎頁面與WebView頁面的布局合在一起,顯示在一個頁面內,起始隱藏WebView布局,待WebView加載完畢,隱藏歡迎布局,顯示WebView布局。
使用FrameLayout簡單一些,兩種方法都是需要對WebChromeClient的onProgressChanged進行監聽,加載完畢進行頁面切換,如下:
webView.setWebChromeClient(new WebChromeClient() {
@Override
public void onProgressChanged(WebView view, int newProgress) {
super.onProgressChanged(view, newProgress);
if (newProgress >= 100) {
// 切換頁面
}
}
});
*7:WebView獨立進程*
有效增大App的運存,減少由webview引起的內存泄露對主進程內存的占用。
避免WebView的Crash影響App主進程的運行。
擁有對WebView獨立進程操控權。
WebView進程與其他進程通訊的方式
把webview獨立進程之后會發現,埋點功能和接收主進程數據都不正常了,這里就涉及到進程間通訊的問題了;
進程通訊無非就是那幾種,aidl,messager,content provider,廣播;
在這里就不再復述了,我是采用廣播的方式來做的。
*8:WebView硬件加速導致頁面渲染閃爍*
4.0以上的系統我們開啟硬件加速后,WebView渲染頁面更加快速,拖動也更加順滑。但有個副作用就是,當WebView視圖被整體遮住一塊,
然后突然恢復時(比如使用SlideMenu將WebView從側邊滑出來時),這個過渡期會出現白塊同時界面閃爍。
解決這個問題的方法是在過渡期前將WebView的硬件加速臨時關閉,過渡期后再開啟,代碼如下:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { webview.setLayerType(View.LAYER_TYPE_SOFTWARE, null);}
9:webview的配置
WebSettings用于管理WebView狀態配置,當WebView第一次被創建時,WebView包含著一個默認的配置,這些默認的配置將通過get方法返回,通過WebView中的getSettings方法獲得一個WebSettings對象,如果一個WebView被銷毀,在WebSettings中所有回調方法將拋出IllegalStateException異常。
1、setSupportZoom(boolean support)
設置WebView是否支持使用屏幕控件或手勢進行縮放,默認是true,支持縮放。
getSettings.setSupportZoom(false);
2、setMediaPlaybackRequiresUserGesture(boolean require)
設置WebView是否通過手勢觸發播放媒體,默認是true,需要手勢觸發。
getSettings.setMediaPlaybackRequiresUserGesture(false);
3、setBuiltInZoomControls(boolean enabled)
設置WebView是否使用其內置的變焦機制,該機制集合屏幕縮放控件使用,默認是false,不使用內置變焦機制。
getSettings.setBuiltInZoomControls(true);
4、setDisplayZoomControls(boolean enabled)
設置WebView使用內置縮放機制時,是否展現在屏幕縮放控件上,默認true,展現在控件上。
getSettings.setDisplayZoomControls(false);
5、setAllowFileAccess(boolean allow)
設置在WebView內部是否允許訪問文件,默認允許訪問。
getSettings.setAllowFileAccess(false);
6、setAllowContentAccess(boolean allow)
設置WebView是否使用其內置的變焦機制,該機制結合屏幕縮放控件使用,默認是false,不使用內置變焦機制。
getSettings.setAllowContentAccess(false);
7、setLoadWithOverviewMode(boolean overview)
設置WebView是否使用預覽模式加載界面。
getSettings.setLoadWithOverviewMode(false);
8、setSaveFormData(boolean save)
設置WebView是否保存表單數據,默認true,保存數據。
getSettings.setSaveFormData(false);
9、setTextZoom(int textZoom)
設置WebView中加載頁面字體變焦百分比,默認100,整型數。
getSettings.setTextZoom(100);
10、setAcceptThirdPartyCookies(boolean accept)
設置WebView訪問第三方Cookies策略,參考CookieManager提供的方法:setShouldAcceptThirdPartyCookies。
getSettings.setAcceptThirdPartyCookies(false);
11、setUseWideViewPort(boolean use)
設置WebView是否使用viewport,當該屬性被設置為false時,加載頁面的寬度總是適應WebView控件寬度;當被設置為true,當前頁面包含viewport屬性標簽,在標簽中指定寬度值生效,如果頁面不包含viewport標簽,無法提供一個寬度值,這個時候該方法將被使用。
getSettings.setUseWideViewPort(false);
12、setSupportMultipleWindows(boolean support)
設置WebView是否支持多屏窗口,參考WebChromeClient#onCreateWindow,默認false,不支持。
getSettings.setSupportMultipleWindows(true);
13、setLayoutAlgorithm(LayoutAlgorithm l)
設置WebView底層的布局算法,參考LayoutAlgorithm#NARROW_COLUMNS,將會重新生成WebView布局
getSettings.setLayoutAlgorithm(LayoutAlgorithm l);
14、setStandardFontFamily(String font)
設置WebView標準字體庫字體,默認字體“sans-serif”。
getSettings.setStandardFontFamily("sans-serif");
15、setFixedFontFamily(String font)
設置WebView固定的字體庫字體,默認“monospace”。
getSettings.setFixedFontFamily("monospace");
16、setSansSerifFontFamily(String font)
設置WebView Sans SeriFontFamily字體庫字體,默認“sans-serif”。
getSettings.setSansSerifFontFamily("sans-serif");
17、setSerifFontFamily(String font)
設置WebView seri FontFamily字體庫字體,默認“sans-serif”。
getSettings.setSansSerifFontFamily("sans-serif");
18、setCursiveFontFamily(String font)
設置WebView字體庫字體,默認“cursive”
getSettings.setCursiveFontFamily("cursive");
19、setFantasyFontFamily(String font)
設置WebView字體庫字體,默認“fantasy”。
getSettings.setFantasyFontFamily("fantasy");
20、setMinimumFontSize(int size)
設置WebView字體最小值,默認值8,取值1到72
getSettings.setMinimumFontSize(8);
21、setMinimumLogicalFontSize(int size)
設置WebView邏輯上最小字體值,默認值8,取值1到72
getSettings.setMinimumLogicalFontSize(8);
22、setDefaultFontSize(int size)
設置WebView默認值字體值,默認值16,取值1到72
getSettings.setDefaultFontSize(16);
23、setDefaultFixedFontSize(int size)
設置WebView默認固定的字體值,默認值16,取值1到72
getSettings.setDefaultFixedFontSize(16);
24、setLoadsImagesAutomatically(boolean flag)
設置WebView是否加載圖片資源,默認true,自動加載圖片
getSettings.setLoadsImagesAutomatically(false);
25、setBlockNetworkImage(boolean flag)
設置WebView是否以http、https方式訪問從網絡加載圖片資源,默認false
getSettings.setBlockNetworkImage(true);
26、setBlockNetworkLoads(boolean flag)
設置WebView是否從網絡加載資源,Application需要設置訪問網絡權限,否則報異常
getSettings.setBlockNetworkLoads(true);
27、setJavaScriptEnabled(boolean flag)
設置WebView是否允許執行JavaScript腳本,默認false,不允許
getSettings.setJavaScriptEnabled(true);
28、setAllowUniversalAccessFromFileURLs(boolean flag)
設置WebView運行中的腳本可以是否訪問任何原始起點內容,默認true
getSettings.setAllowUniversalAccessFromFileURLs(false);
29、setAllowFileAccessFromFileURLs(boolean flag)
設置WebView運行中的一個文件方案被允許訪問其他文件方案中的內容,默認值true
getSettings.setAllowFileAccessFromFileURLs(false);
30、setGeolocationDatabasePath(String databasePath)
設置WebView保存地理位置信息數據路徑,指定的路徑Application具備寫入權限
getSettings.setGeolocationDatabasePath(String path);
31、setAppCacheEnabled(boolean flag)
設置Application緩存API是否開啟,默認false,設置有效的緩存路徑參考setAppCachePath(String path)方法
getSettings.setAppCacheEnabled(true);
32、setAppCachePath(String appCachePath)
設置當前Application緩存文件路徑,Application Cache API能夠開啟需要指定Application具備寫入權限的路徑
getSettings.setAppCachePath(String appCachePath);
33、setDatabaseEnabled(boolean flag)
設置是否開啟數據庫存儲API權限,默認false,未開啟,可以參考setDatabasePath(String path)
getSettings.setDatabaseEnabled(false);
34、setDomStorageEnabled(boolean flag)
設置是否開啟DOM存儲API權限,默認false,未開啟,設置為true,WebView能夠使用DOM storage API
getSettings.setDomStorageEnabled(true);
35、setGeolocationEnabled(boolean flag)
設置是否開啟定位功能,默認true,開啟定位
getSettings.setGeolocationEnabled(false);
36、setJavaScriptCanOpenWindowsAutomatically(boolean flag)
設置腳本是否允許自動打開彈窗,默認false,不允許
getSettings.setJavaScriptCanOpenWindowsAutomatically(true);
37、setDefaultTextEncodingName(String encoding)
設置WebView加載頁面文本內容的編碼,默認“UTF-8”。
getSettings.setDefaultTextEncodingName("UTF-8");
38、setUserAgentString(String ua)
設置WebView代理字符串,如果String為null或為空,將使用系統默認值
getSettings.setUserAgentString(String ua);
39、setNeedInitialFocus(boolean flag)
設置WebView是否需要設置一個節點獲取焦點當被回調的時候,默認true
getSettings.setNeedInitialFocus(false);
40、setCacheMode(int mode)
重寫緩存被使用到的方法,該方法基于Navigation Type,加載普通的頁面,將會檢查緩存同時重新驗證是否需要加載,如果不需要重新加載,將直接從緩存讀取數據,允許客戶端通過指定LOAD_DEFAULT、LOAD_CACHE_ELSE_NETWORK、LOAD_NO_CACHE、LOAD_CACHE_ONLY其中之一重寫該行為方法,默認值LOAD_DEFAULT
getSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
LOAD_CACHE_ONLY: 不使用網絡,只讀取本地緩存數據
LOAD_DEFAULT: 根據cache-control決定是否從網絡上取數據。
LOAD_CACHE_NORMAL: API level 17中已經廢棄, 從API level 11開始作用同LOAD_DEFAULT模式
LOAD_NO_CACHE: 不使用緩存,只從網絡獲取數據.
LOAD_CACHE_ELSE_NETWORK,只要本地有,無論是否過期,或者no-cache,都使用緩存中的數據。
如:www.taobao.com的cache-control為no-cache,在模式LOAD_DEFAULT下,無論如何都會從網絡上取數據,如果沒有網絡,就會出現錯誤頁面;在LOAD_CACHE_ELSE_NETWORK模式下,無論是否有網絡,只要本地有緩存,都使用緩存。本地沒有緩存時才從網絡上獲取。
www.360.com.cn的cache-control為max-age=60,在兩種模式下都使用本地緩存數據。
根據以上兩種模式,建議緩存策略為,判斷是否有網絡,有的話,使用LOAD_DEFAULT,無網絡時,使用LOAD_CACHE_ELSE_NETWORK。
41、setMixedContentMode(int mode)
設置當一個安全站點企圖加載來自一個不安全站點資源時WebView的行為,android.os.Build.VERSION_CODES.KITKAT默認為MIXED_CONTENT_ALWAYS_ALLOW,android.os.Build.VERSION_CODES#LOLLIPOP默認為MIXED_CONTENT_NEVER_ALLOW,取值其中之一:MIXED_CONTENT_NEVER_ALLOW、MIXED_CONTENT_ALWAYS_ALLOW、MIXED_CONTENT_COMPATIBILITY_MODE.
getSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
下面貼上我自己的配置代碼:
WebSettings settings = webview.getSettings();
settings.setJavaScriptEnabled(true);//啟用js
settings.setJavaScriptCanOpenWindowsAutomatically(true);//js和android交互
String cacheDirPath = PathCommonDefines.WEBVIEW_CACHE;
settings.setAppCachePath(cacheDirPath); //設置緩存的指定路徑
settings.setAllowFileAccess(true); // 允許訪問文件
settings.setAppCacheEnabled(true); //設置H5的緩存打開,默認關閉
settings.setUseWideViewPort(true);//設置webview自適應屏幕大小
settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NARROW_COLUMNS);//設置,可能的話使所有列的寬度不超過屏幕寬度
settings.setLoadWithOverviewMode(true);//設置webview自適應屏幕大小
settings.setDomStorageEnabled(true);//設置可以使用localStorage
settings.setSupportZoom(false);//關閉zoom按鈕
settings.setBuiltInZoomControls(false);//關閉zoom
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
webview.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
webview.setWebViewClient(new WebViewClient() {
@Override public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); return false; }
@Override public void onLoadResource(WebView view, String url) { }
@Override public void onPageFinished(WebView view, String url) { } });
*10:html5跳原生界面*
**
**網頁跳原生界面的方法有很多種,比如js調java方法,或者是通過uri scheme啦,也可以通過自己解析url來做。
在這兒,考慮到兼容性,攔截的是url,并且在清單文件中自定義了scheme~
webview.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { parserURL(url); //解析url,如果存在有跳轉原生界面的url規則,則跳轉原生。 return super.shouldOverri deUrlLoading(view, url); } @Override public void onPageFinished(WebView view, String url) { super.onPageFinished (view, url); } @Override public void onLoadResource(WebView view, String url) { super.onLoadResource(view, url); } });
清單文件中,聲明一下 就可以在自帶瀏覽器通過uri scheme跳到本app頁面了,這個activity作為各個頁面的分發頁面,通過 這個界面解析數據決定接下來要跳轉哪個頁面:
<activity
android:name=".ui.webview.CommWebviewActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:process=":webview"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateHidden">
<intent-filter>
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<action android:name="android.intent.action.VIEW" />
<data
android:host="xxxx.com"
android:scheme="kingp2p" />
</intent-filter>
</activity>
Android中處理網頁時我們必然用到WebView,這里我們有這樣一個需求,我們想讓WebView在處理網絡請求的時候將某些請 求攔截替換成某些特殊的資源。具體一點兒說,在WebView加載 http://m.sogou.com 時,會加載一個logo圖片,我們的需 求就是將這個logo圖片換成另一張圖片。
shouldOverrideUrlLoading(攔截url加載,除資源請求的url) shouldInterceptRequest(攔截所有url請求)
shouldInterceptRequest:在每一次請求資源時,都會通過這個函數來回調,比如超鏈接、JS文件、CSS文件、圖片等,
也就是說瀏覽器中每一次請求資源時,都會回調回來,無論任何資源!
但是必須注意的是shouldInterceptRequest函數是在非UI線程中執行的,在其中不能直接做UI操作,如果需要做UI操作,則需要利用Handler來實現
好在Android中的WebView比較強大,從API 11(Android 3.0)開始, shouldInterceptRequest被引入就是為了解決這一類的問題。
shouldInterceptRequest這個回調可以通知主程序WebView處理的資源(css,js,image等)請求,并允許主程序進行處理后返回數據。如果主程序返回的數據為null,WebView會自行請求網絡加載資源,否則使用主程序提供的數據。注意這個回調發生在非UI線程中,所以進行UI系統相關的操作是不可以的。
shouldInterceptRequest有兩種重載。
public WebResourceResponse shouldInterceptRequest (WebView view, String url) 從API 11開始引入,API 21棄用
public WebResourceResponse shouldInterceptRequest (WebView view, WebResourceRequest request) 從API 21開始引入
本次例子暫時使用第一種,即shouldInterceptRequest (WebView view, String url)。
示例代碼
WebView webView = new WebView(this);
webView.setWebViewClient(new WebViewClient() {
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
Log.i(LOGTAG, "shouldInterceptRequest url=" + url + ";threadInfo" + Thread.currentThread());
WebResourceResponse response = null;
if (url.contains("logo")) {
try {
InputStream localCopy = getAssets().open("droidyue.png");
response = new WebResourceResponse("image/png", "UTF-8", localCopy);}
catch (IOException e) {
e.printStackTrace();}}return response;}});
}
setContentView(webView);
webView.loadUrl("http://m.sogou.com");
}
其中WebResourceResponse需要設定三個屬性,MIME類型,數據編碼,數據(InputStream流形式)。
shouldOverrideUrlLoading(攔截url加載,除資源請求的url) shouldInterceptRequest(攔截所有url請求)
shouldOverrideUrlLoading:
這個函數會在加載超鏈接時回調過來
對網頁中超鏈接按鈕的響應。當按下某個連接時WebViewClient會調用這個方法,并傳遞參數:按下的url。
通過重寫shouldOverrideUrlLoading,可以實現對網頁中超鏈接的攔截;
webView.setWebViewClient(new WebViewClient(){
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url); //在當前的webview中跳轉到新的url
return true;
} });
shouldoverrideurlloading返回true表明點擊網頁里面的鏈接還是在當前的webview里跳轉,不跳到瀏覽器那邊。
這個方法在手動調用WebView.loadUrl(url);時是不會被調用的,再進一步點擊才會走這個方法。
返回值是boolean類型,表示是否屏蔽WebView繼續加載URL的默認行為,因為這個函數是WebView加載URL前回調的,所以如果我們return true,則WebView接下來就不會再加載這個URL了,所有處理都需要在WebView中操作,包含加載。如果我們return false,則系統就認為上層沒有做處理,接下來還是會繼續加載這個URL的。在利用shouldOverrideUrlLoading來攔截URL時,如果return true,則會屏蔽系統默認的顯示URL結果的行為,不需要處理的URL也需要調用loadUrl()來加載進WebVIew,不然就會出現白屏;如果return false,則系統默認的加載URL行為是不會被屏蔽的,所以一般建議大家return false,我們只關心我們關心的攔截內容,對于不攔截的內容,讓系統自己來處理即可。
WebChromeClient與WebViewClient的區別
WebView的內部實現并不是完全使用Chrome的內核,而是部分使用Chome內核,其它都是與Chrome不相同的;
WebViewClient:在影響View的事件到來時,會通過WebViewClient中的方法回調通知用戶
WebChromeClient:當影響瀏覽器的事件到來時,就會通過WebChromeClient中的方法回調通知用法。
通過上面的對比,我們發現WebViewClient和WebChromeClient都是針對不同事件的回調,而google將這些回調進行分類集合,就產生了WebViewClient、WebChromeClient這兩個大類,其中管理著針對不同類型的回調而已。
在程序中,我們只需要加上mWebView.setWebChromeClient(new WebChromeClient());就可以實現confrim()、alert()、prompt()的彈出效果了
如:mWebView.setWebViewClient(new WebViewClient()); mWebView.setWebChromeClient(new WebChromeClient());
如果需要使網頁中的confrim()、alert()、prompt()函數生效,需要設置WebChromeClient!
除了alert,prompt,confirm以外,其它時候都不需要強制設置WebChromeClient
在使用onJsAlert來攔截alert對話框時,如果不需要再彈出alert對話框,一定要return true;在return false以后,會依然調用系統的默認處理來彈出對話框的
*11:LoadData()與loadDataWithBaseURL()*
**
**通過loadUrl()來加載本地頁面和在線地址的方式需要很長的加載時間,而LoadData()與loadDataWithBaseURL(),它們不是用來加載整個頁面文件的,而是用來加載一段代碼片的
比如可以先通過http將網頁HTML數據從服務器下載回來,在通過這兩個方法就可以很快把頁面加載出來,沒有很長的訪問網絡cDN解析等時間
在使用loadData時,在數據里面不能出現英文字符:’#’, ‘%’, ‘\’ , ‘?’ 這四個字符,如果有的話可以用 %23, %25,%27, %3f,這些字符來替換
Android給我們提供了一個專門用來轉碼的函數:URLEncoder.encode(String s, String charsetName) ,它能將沖突的字符進行轉義,然后再傳給webview,這樣webview在加載時就不會有沖突了,如:mWebView.loadData(URLEncoder.encode(summary, "utf-8"), "text/html", "utf-8");
loadData()應該是不能加載圖片的,為了防止字符沖突,在傳遞loadData的數據時,必須使用URLEncoder.encode()函數來轉義,頁面的編碼格式必須與代碼中傳參的編碼格式一致,不然會導致亂碼
public void loadDataWithBaseURL(String baseUrl, String data,String mimeType, String encoding, String historyUrl)
參數意義如下:
String baseUrl:基準URL,不需要可以傳null,它的意思是,如果data中的url是相對地址,則就會加上基準url來拼接出完整的地址,比如baseUrl是http://img6.ph.126.net,data中有個Img標簽,它的內容是:<img src='hBiG96B8egigBULxUWcOpA==/109212290980771276.jpg'>,很明顯src的地址不是本地地址也不是在線地址,那它就是一個相對地址,所以加上baseUrl以后才是它的完整地址:http://img6.ph.126.net/hBiG96B8egigBULxUWcOpA==/109212290980771276.jpg
String mimeType:MIME類型
String encoding:編碼方式
String historyUrl:當前的歷史記錄所要存儲的值。如果不需要可以傳Null,loadDataWithBaseURL它本身并不會向歷史記錄中存儲數據,要想實現歷史記錄,需要我們自己來實現;有關歷史記錄的實現方式是比較復雜的,歷史記錄是以Key/value的方式存儲在一個historyList里的,當前進后退時,會用Key來取出對應的value值來加載進webview中。而Key就是這里的baseUrl,Value就是這里的historyUrl;history所指向的必須是一個頁面,并且頁面存在于SD卡中或程序中(assets);
如果網頁地址是絕對地址,本地文件地址也是絕對地址,而另外一個是相對地址的情況下,絕對的http地址,通過file指定的本地地址,對于這兩類的絕對地址,baseUrl是不起作用的,而對于第三個相對地址,是會啟用baseUrl的來拼接完整地址的。
這兩種方法,我建議使用后者,雖然loadData的歷史記錄不需要我們自己來實現,但在使用時,加載上后者比前者快一到兩倍。
另外loadData不能加載圖片,而loadDataWithBaseURL是可以加載圖片的
*總結:*
**
**1:圖片延遲加載:圖片的網絡請求會造成帶寬緊張,影響到css或js文件加載完成的時間,造成頁面空白loading過久
2: 常用資源預加載,shouldInterceptRequest直接替換
3:常用資源的緩存shouldInterceptRequest攔截保存
4:合理使用http緩存策略:Expires , max-age, Etag , Last-Modified (其中Expires,max-age是客戶端在這個時間 之前 不去向服務器端發送請求驗證資源是否有更新, Etag, Last-Modified是服務器決定是否需要返回資源,未更新的資源不需要返回)
5:getSettings.setCacheMode建議緩存策略為,判斷是否有網絡,有的話,使用LOAD_DEFAULT,無網絡時,使用LOAD_ CACHE_ELSE_NETWORK。
6:使用JSBridge,支持異步回調
7:將HTML以接口的形式返回,webView用loadDataWithBaseURL加載HTML,并且可以解決跨域行為的問題
8:使用第三方 WebView 內核 如騰訊的X5 內核
9:WebView 導致的內存泄露:讓 WebView 獨立運行在一個進程里減少由webview引起的內存泄露對主進程內存的占用, 且不在 XML 里面聲明,而是在代碼中直接 new 出來,傳入 application context 來防止 activity 引用被濫用
10:WebView.HitTestResult 這個函數可獲取到我們點擊的目標類型來得到當前點擊的資源是什么,可實現長按下載圖片
11:支持URLscheme和魔窗來完成應用內和應用間和微信中頁面的跳轉和恢復
12:還有就是具體的業務邏輯的優化