之前我在Android中使用WebView與JS交互全解析一文中,介紹了通過Webview和JS的交互方式,但Webview這個控件簡直是讓人又愛又恨,各種你想不到的錯誤在各種奇怪的手機上,各種不一樣的版本里,所以我想通過這篇博客總結Webview開發中的不得不注意的一些坑。
1.WebView的內存泄露問題
問題描述:
webview內存泄露的情況還是很嚴重的,尤其是當你加載的頁面比較龐大的時候。
解決方案:
1) 展示webview的activity可以另開一個進程,這樣就能和我們app的主進程分開了,即使webview產生了oom崩潰等問題也不會影響到主程序,如何實現呢,其實很簡單,在Androidmanifest.xml的activity標籤里加上Android:process=”packagename.web”就可以了,並且當這個 進程結束時,請手動調用System.exit(0)。
2) 如果實在不想用開額外進程的方式解決webview 內存泄露的問題,那麼下面的方法很大程度上可以避免這種情況
public void releaseAllWebViewCallback() {
if (android.os.Build.VERSION.SDK_INT < 16) {
try {
Field field = WebView.class.getDeclaredField("mWebViewCore");
field = field.getType().getDeclaredField("mBrowserFrame");
field = field.getType().getDeclaredField("sConfigCallback");
field.setAccessible(true);
field.set(null, null);
} catch (NoSuchFieldException e) {
if (BuildConfig.DEBUG) {
e.printStackTrace();
}
} catch (IllegalAccessException e) {
if (BuildConfig.DEBUG) {
e.printStackTrace();
}
}
} else {
try {
Field sConfigCallback = Class.forName("android.webkit.BrowserFrame").getDeclaredField("sConfigCallback");
if (sConfigCallback != null) {
sConfigCallback.setAccessible(true);
sConfigCallback.set(null, null);
}
} catch (NoSuchFieldException e) {
if (BuildConfig.DEBUG) {
e.printStackTrace();
}
} catch (ClassNotFoundException e) {
if (BuildConfig.DEBUG) {
e.printStackTrace();
}
} catch (IllegalAccessException e) {
if (BuildConfig.DEBUG) {
e.printStackTrace();
}
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
在webview的 destroy方法裏 調用這個方法就行了。
2.慎重在shouldoverrideurlloading中返回true
當設置了WebviewClient時,在shouldoverrideurlloading中如果不需要對url進行攔截做處理,而是簡單的繼續加載此網址,則建議採用返回false的方式而不是loadUrl的方式進行加載網址。
1) 當請求的方式是”POST”方式時這個回調是不會通知的。
2) 因爲如果採用loadUrl的方式進行加載,那麼對於加載有跳轉的網址時,進行webview.goBack就會特別麻煩。
例如加載鏈接如下:
A1->(A2->A3->A4)->A5 括號內爲跳轉
如果採用return false的方式,那麼在goBack的時候,可以從第二步直接回到A1網頁。從A5回到A1只需要執行兩次goBack
而如果採用的是loadUrl,則沒辦法直接從第二步回到A網頁。因爲loadUrl把第二步的每個跳轉都認爲是一個新的網頁加載,因此從A5回到A1需要執行四次goBack
只有當不需要加載網址而是攔截做其他處理,如攔截tel:xxx等特殊url做撥號處理的時候,才應該返回true。
3.getSettings().setBuiltInZoomControls(true) 引發的crash。
問題描述:
這個方法調用以後 如果你觸摸屏幕 彈出的提示框還沒消失的時候 你如果activity結束了 就會報錯了。3.0以上 4.4以下很多手機會出現這種情況
解決方案:
在activity的onDestroy方法裏手動的將webiew設置成 setVisibility(View.GONE)
4.onPageFinished 函數的問題
問題描述:
你永遠無法確定當WebView調用這個方法的時候,網頁內容是否真的加載完畢了。當前正在加載的網頁產生跳轉的時候這個方法可能會被多次調用,多數開發者都是參考的http://stackoverflow.com/questions/3149216/how-to-listen-for-a-webview-finishing-loading-a-url-in-android 這個上面的高票答案,但其中列舉的解決方法並不完美。
解決方案:
當你的WebView需要加載各種各樣的網頁並且需要在頁面加載完成時採取一些操作的話,可能WebChromeClient.onProgressChanged()比WebViewClient.onPageFinished()都要靠譜一些。如果哪位大神有更好的解決方法,歡迎留言。
5.WebView後臺耗電問題。
問題描述:
當你的程序調用了WebView加載網頁,WebView會自己開啓一些線程,如果你沒有正確地將WebView銷燬的話,這些殘餘的線程會一直在後臺運行,由此導致你的應用程序耗電量居高不下。
解決方案:
在Activity.onDestroy()中直接調用System.exit(0),使得應用程序完全被移出虛擬機,這樣就不會有任何問題了。
6.後臺無法釋放js 導致耗電
問題描述:
在有的手機裏,你如果webview加載的html裏 有一些js 一直在執行比如動畫之類的東西,如果此刻webview 掛在了後臺,這些資源是不會被釋放 用戶也無法感知,導致一直佔有cpu 耗電特別快。
解決方案:
在Activity的onstop和onresume裏分別把setJavaScriptEnabled();給設置成false和true。
7.怎麼用網頁的標題來設置自己的標題欄?
WebChromeClient mWebChromeClient = new WebChromeClient() {
@Override
public void onReceivedTitle(WebView view, String title) {
super.onReceivedTitle(view, title);
txtTitle.setText(title);
}
};
mWedView.setWebChromeClient(mWebChromeClient());
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
但是發現在部分的手機上,當通過webview.goBack()回退的時候,並沒有觸發onReceiveTitle(),這樣會導致標題仍然是之前子頁面的標題,沒有切換回來.
這裏可以分兩種情況去處理:
1) 可以確定webview中子頁面只有二級頁面,沒有更深的層次,這裏只需要判斷當前頁面是否爲初始的主頁面,可以goBack的話,只要將標題設置回來即可.
2)webview中可能有多級頁面或者以後可能增加多級頁面,這種情況處理起來要複雜一些:
因爲正常順序加載的情況onReceiveTitle是一定會觸發的,所以就需要自己來維護webview loading的一個url棧及url與title的映射關係。那麼就需要一個ArrayList來保持加載過的url,一個HashMap保存url及對應的title。
正常順序加載時,將url和對應的title保存起來,webview回退時,移除當前url並取出將要回退到的web 頁的url,找到對應的title進行設置即可。
這裏還要說一點,當加載出錯的時候,比如無網絡,這時onReceiveTitle中獲取的標題爲 找不到該網頁,因此建議當觸發onReceiveError時,不要使用獲取到的title.
8.怎麼隱藏縮放控件?
if (DeviceUtils.hasHoneycomb()) {
mWebView.getSettings().setDisplayZoomControls(false);
}
最好把這兩句加上
mWebView.getSettings().setSupportZoom(true);
mWebView.getSettings().setBuiltInZoomControls(true);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
9.怎麼知道WebView是否已經滾動到頁面底端?
if (mWebView.getContentHeight() * mWebView.getScale()
== (mWebView.getHeight() + mWebView.getScrollY())) {
}
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
附上官網對幾個方法的註釋
getContentHeight() @return the height of the HTML content
getScale() @return the current scale
getHeight() @return The height of your view
getScrollY() @return The top edge of the displayed part of your view, in pixels.
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
10.怎麼清理cache 和歷史記錄?
mWebView.clearCache(true);
mWebView.clearHistory();
- 1
- 2
- 3
- 1
- 2
- 3
11.怎麼清理Cookie?
CookieSyncManager.createInstance(this);
CookieSyncManager.getInstance().startSync();
CookieManager.getInstance().removeSessionCookie();
- 1
- 2
- 3
- 1
- 2
- 3
12.爲什麼打包之後JS調用失敗?
原因:沒在proguard-rules.pro中添加混淆吧
-keep class con.demo.activity.web.utils.JsShareInterface {
public void share(java.lang.String);
}
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
13.WebView頁面中播放了音頻,退出Activity後音頻仍然在播放
需要在Activity的onDestory()中調用以下方法
1. webView.destroy();
- 1
- 1
但可能會出現報錯:
10-10 15:01:11.402: E/ViewRootImpl(7502): sendUserActionEvent() mView == null
10-10 15:01:26.818: E/webview(7502): java.lang.Throwable: Error: WebView.destroy() called while still attached!
10-10 15:01:26.818: E/webview(7502): at android.webkit.WebViewClassic.destroy(WebViewClassic.java:4142)
10-10 15:01:26.818: E/webview(7502): at android.webkit.WebView.destroy(WebView.java:707)
10-10 15:01:26.818: E/webview(7502): at com.didi.taxi.ui.webview.OperatingWebViewActivity.onDestroy(OperatingWebViewActivity.java:236)
10-10 15:01:26.818: E/webview(7502): at android.app.Activity.performDestroy(Activity.java:5543)
10-10 15:01:26.818: E/webview(7502): at android.app.Instrumentation.callActivityOnDestroy(Instrumentation.java:1134)
10-10 15:01:26.818: E/webview(7502): at android.app.ActivityThread.performDestroyActivity(ActivityThread.java:3619)
10-10 15:01:26.818: E/webview(7502): at android.app.ActivityThread.handleDestroyActivity(ActivityThread.java:3654)
10-10 15:01:26.818: E/webview(7502): at android.app.ActivityThread.access$1300(ActivityThread.java:159)
10-10 15:01:26.818: E/webview(7502): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1369)
10-10 15:01:26.818: E/webview(7502): at android.os.Handler.dispatchMessage(Handler.java:99)
10-10 15:01:26.818: E/webview(7502): at android.os.Looper.loop(Looper.java:137)
10-10 15:01:26.818: E/webview(7502): at android.app.ActivityThread.main(ActivityThread.java:5419)
10-10 15:01:26.818: E/webview(7502): at java.lang.reflect.Method.invokeNative(Native Method)
10-10 15:01:26.818: E/webview(7502): at java.lang.reflect.Method.invoke(Method.java:525)
10-10 15:01:26.818: E/webview(7502): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1187)
10-10 15:01:26.818: E/webview(7502): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
10-10 15:01:26.818: E/webview(7502): at dalvik.system.NativeStart.main(Native Method)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
webview調用destory時,webview仍綁定在Activity上.這是由於自定義webview構建時傳入了該Activity的context對象,因此需要先從父容器中移除webview,然後再銷燬webview:
rootLayout.removeView(webView);
webView.destroy();
- 1
- 2
- 3
- 1
- 2
- 3
14.處理WebView中的非超鏈接請求(如Ajax請求)
有時候需要加上請求頭,但是非超鏈接的請求,沒有辦法再shouldOverrinding中攔截並用webView.loadUrl(String url,HashMap headers)方法添加請求頭
目前用了一個臨時的辦法解決:
首先需要在url中加特殊標記/協議, 如在onWebViewResource方法中攔截對應的請求,然後將要添加的請求頭,以get形式拼接到url末尾
在shouldInterceptRequest()方法中,可以攔截到所有的網頁中資源請求,比如加載JS,圖片以及Ajax請求等等
@SuppressLint("NewApi")
@Override
public WebResourceResponse shouldInterceptRequest(WebView view,String url) {
// 非超鏈接(如Ajax)請求無法直接添加請求頭,現拼接到url末尾,這裏拼接一個imei作爲示例
String ajaxUrl = url;
// 如標識:req=ajax
if (url.contains("req=ajax")) {
ajaxUrl += "&imei=" + imei;
}
return super.shouldInterceptRequest(view, ajaxUrl);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
15.屏蔽webview長按事件,因爲webview長按時將會調用系統的複製控件
mWebView.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
return true;
}
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
16.在頁面中先顯示圖片
@Override
public void onLoadResource(WebView view, String url) {
mEventListener.onWebViewEvent(CustomWebView.this, OnWebViewEventListener.EVENT_ON_LOAD_RESOURCE, url);
if (url.indexOf(".jpg") > 0) {
hideProgress(); //請求圖片時即顯示頁面
mEventListener.onWebViewEvent(CustomWebView.this, OnWebViewEventListener.EVENT_ON_HIDE_PROGRESS, view.getUrl());
}
super.onLoadResource(view, url);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
17.爲WebView自定義錯誤顯示界面
覆寫WebViewClient中的onReceivedError()方法:
/**
* 顯示自定義錯誤提示頁面,用一個View覆蓋在WebView
*/
protected void showErrorPage() {
LinearLayout webParentView = (LinearLayout)mWebView.getParent();
initErrorPage();
while (webParentView.getChildCount() > 1) {
webParentView.removeViewAt(0);
}
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LayoutParams.FILL_PARENT,LayoutParams.FILL_PARENT);
webParentView.addView(mErrorView, 0, lp);
mIsErrorPage = true;
}
protected void hideErrorPage() {
LinearLayout webParentView = (LinearLayout)mWebView.getParent();
mIsErrorPage = false;
while (webParentView.getChildCount() > 1) {
webParentView.removeViewAt(0);
}
}
protected void initErrorPage() {
if (mErrorView == null) {
mErrorView = View.inflate(this, R.layout.online_error, null);
Button button = (Button)mErrorView.findViewById(R.id.online_error_btn_retry);
button.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
mWebView.reload();
}
});
mErrorView.setOnClickListener(null);
}
}
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
<span style="white-space:pre"> </span>
<span style="white-space:pre"> </span>mErrorView.setVisibility(View.VISIBLE);
<span style="white-space:pre"> </span>super.onReceivedError(view, errorCode, description, failingUrl);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
最後提一下WebView的一些小技巧:
1.webview的創建也是有技巧的,最好不要在layout.xml中使用webview,可以通過一個viewgroup容器,使用代碼動態往容器裏addview(webview),這樣可以在onDestory()裏銷燬掉webview及時清理內存,另外需要注意創建webview需要使用applicationContext而不是activity的context,銷燬時不再佔有activity對象,這個大家應該都知道了,最後離開的時候需要及時銷燬webview,onDestory()中應該先從viewgroup中remove掉webview,再調用webview.removeAllViews();webview.destory();
創建
ll = new LinearLayout(getApplicationContext());
ll.setOrientation(LinearLayout.VERTICAL);
wv = new WebView(getApplicationContext());
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
銷燬
@Override
rotected void onDestroy() {
ll.removeAllViews();
wv.stopLoading();
wv.removeAllViews();
wv.destroy();
wv = null;
ll = null;
super.onDestroy();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
2.另外很多人 不知道webview 實際上有自己一套完整的cookie機制的,利用好這個 可以大大增加對客戶端的訪問速度。
實際上cookie就是存放在這個表裏的。
很多人都想要一個效果:網頁更新cookie 設置完cookie以後 不刷新頁面即可生效。這個在2.3以下和2.3以上要實現的方法不太一樣,不過現在的安卓版本已經基本沒有2.3的啦
public void updateCookies(String url, String value) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) { // 2.3及以下
CookieSyncManager.createInstance(getContext().getApplicationContext());
}
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.setAcceptCookie(true);
cookieManager.setCookie(url, value);
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) {
CookieSyncManager.getInstance().sync();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
3.進一步的優化,activity被動被殺之後,最好能夠保存webview狀態,這樣用戶下次打開時就看到之前的狀態了,嗯,就這麼幹,webview支持saveState(bundle)和restoreState(bundle)方法,所以就簡單了,看看代碼吧:
保存狀態:
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
wv.saveState(outState);
Log.e(TAG, "save state...");
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
恢復狀態:
在activity的onCreate(bundle savedInstanceState)裏,這麼調用:
if(null!=savedInstanceState){
wv.restoreState(savedInstanceState);
Log.i(TAG, "restore state");
}else{
wv.loadUrl("http://3g.cn");
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
4.WebView 圖片延遲加載:
有些頁面如果包含網絡圖片,在移動設備上我們等待加載圖片的時間可能會很長,所以我們需要讓圖片延時加載,這樣不影響我們加載頁面的速度:
定義變量:
boolean blockLoadingNetworkImage=false;
- 1
- 1
在WebView初始化的時候設置:
blockLoadingNetworkImage = true;
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
//返回值是true的時候控制去WebView打開,爲false調用系統瀏覽器或第三方瀏覽器
return true;
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
if (!blockLoadingNetworkImage){
webView.getSettings().setBlockNetworkImage(true);
}
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
if (blockLoadingNetworkImage){
webView.getSettings().setBlockNetworkImage(false);
}
}
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27