在學(xué)習(xí)WebView的時(shí)候就知道了WebView會(huì)出現(xiàn)很多稀奇古怪的問(wèn)題,真碰上的時(shí)候還是焦頭爛額,很多問(wèn)題的解決方案要在網(wǎng)上找很久很久很久。這里做了稍微全面的總結(jié)。
劃重點(diǎn):
1.內(nèi)存泄露的解決方法
2.Native獲得的cookie同步到WebView中
3.API5.0以上Ajax跨域訪問(wèn)無(wú)法攜帶cookie的問(wèn)題
4.Alert劫持問(wèn)題
1. 內(nèi)存泄露
關(guān)于內(nèi)存泄漏,想要徹底解決,最好的方法是當(dāng)你要用webview的時(shí)候,另外單獨(dú)開一個(gè)進(jìn)程(如何單開進(jìn)程請(qǐng)自行搜索) 去使用webview 并且當(dāng)這個(gè) 進(jìn)程結(jié)束時(shí),請(qǐng)手動(dòng)調(diào)用System.exit(0)。 但是這種情況又會(huì)有另外的問(wèn)題,就是進(jìn)程間的通信。
不單開進(jìn)程時(shí):
1.1 JS無(wú)法釋放,WebView在執(zhí)行JS時(shí)被關(guān)閉,這些JS資源會(huì)無(wú)法釋放,一直耗電,一直占CPU,解決方法是在onStop和onResume里分別把setJavaScriptEnabled();給設(shè)置成false和true。
@Override
protected void onResume() {
mWebView.getSettings().setJavaScriptEnabled(true);
super.onResume();
}
@Override
protected void onStop() {
mWebView.getSettings().setJavaScriptEnabled(false);
super.onStop();
}
1.2在onDestroy中對(duì)webView的銷毀做處理,下面是我覺(jué)得比較好的方式(主要是處理的比較全面)
if (mWebView != null) {
// 如果先調(diào)用destroy()方法,則會(huì)命中if (isDestroyed()) return;這一行代碼,需要先onDetachedFromWindow(),再
// destory()
ViewParent parent = mWebView.getParent();
if (parent != null) {
((ViewGroup) parent).removeView(mWebView);
}
mWebView.stopLoading();
// 退出時(shí)調(diào)用此方法,移除綁定的服務(wù),否則某些特定系統(tǒng)會(huì)報(bào)錯(cuò)
mWebView.getSettings().setJavaScriptEnabled(false);
mWebView.clearHistory();
mWebView.clearAnimation();
mWebView.clearView();
mWebView.removeAllViews();
try {
mWebView.destroy();
} catch (Throwable ex) {
}
2. WebView的各種設(shè)置
2.1 首先是創(chuàng)建
最好不要在XML中直接添加WebView,而是預(yù)留一個(gè)FrameLayout,代碼中創(chuàng)建WebView,添加到FrameLayout中。
這樣能解決很多內(nèi)存泄露的問(wèn)題。
mWebViewContainer = (FrameLayout) findViewById(R.id.web_view_container);
mWebView = new WebView(WebViewActivity.this);
mWebViewContainer.addView(mWebView, new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT));
2.2 然后是settings,N多種方法
WebSettings webSettings = webView.getSettings();
//設(shè)置了這個(gè)屬性后我們才能在 WebView 里與我們的 Js 代碼進(jìn)行交互,對(duì)于 WebApp 是非常重要的,默認(rèn)是 false,
//因此我們需要設(shè)置為 true,這個(gè)本身會(huì)有漏洞,具體的下面我會(huì)講到
webSettings.setJavaScriptEnabled(true);
//設(shè)置 JS 是否可以打開 WebView 新窗口
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
//WebView 是否支持多窗口,如果設(shè)置為 true,需要重寫
//WebChromeClient#onCreateWindow(WebView, boolean, boolean, Message) 函數(shù),默認(rèn)為 false
webSettings.setSupportMultipleWindows(true);
//這個(gè)屬性用來(lái)設(shè)置 WebView 是否能夠加載圖片資源,需要注意的是,這個(gè)方法會(huì)控制所有圖片,包括那些使用 data URI 協(xié)議嵌入
//的圖片。使用 setBlockNetworkImage(boolean) 方法來(lái)控制僅僅加載使用網(wǎng)絡(luò) URI 協(xié)議的圖片。需要提到的一點(diǎn)是如果這
//個(gè)設(shè)置從 false 變?yōu)?true 之后,所有被內(nèi)容引用的正在顯示的 WebView 圖片資源都會(huì)自動(dòng)加載,該標(biāo)識(shí)默認(rèn)值為 true。
webSettings.setLoadsImagesAutomatically(false);
//標(biāo)識(shí)是否加載網(wǎng)絡(luò)上的圖片(使用 http 或者 https 域名的資源),需要注意的是如果 getLoadsImagesAutomatically()
//不返回 true,這個(gè)標(biāo)識(shí)將沒(méi)有作用。這個(gè)標(biāo)識(shí)和上面的標(biāo)識(shí)會(huì)互相影響。
webSettings.setBlockNetworkImage(true);
//顯示W(wǎng)ebView提供的縮放控件
webSettings.setDisplayZoomControls(true);
webSettings.setBuiltInZoomControls(true);
//推薦使用。打開 WebView 的 storage 功能,這樣 JS 的 localStorage,sessionStorage 對(duì)象才可以使用
webSettings.setDomStorageEnabled(true);
//推薦打開。設(shè)置是否啟動(dòng) WebView 的DB API,默認(rèn)值為 false
webSettings.setDatabaseEnabled(true);
webSettings.setDatabasePath(Utils.getContext().getDir("WebDb",MODE_PRIVATE).getPath());
//打開 WebView 自帶的的 LBS 功能,這樣 JS 的 geolocation 對(duì)象才可以使用,注意manifest中的權(quán)限
webSettings.setGeolocationEnabled(true);
webSettings.setGeolocationDatabasePath("");
//設(shè)置是否打開 WebView 表單數(shù)據(jù)的保存功能
webSettings.setSaveFormData(true);
//設(shè)置 WebView 的默認(rèn) userAgent 字符串
webSettings.setUserAgentString("");
// 不推薦使用。設(shè)置緩存,是否開啟,緩存模式,緩存大小,緩存路徑
webSettings.setAppCacheEnabled(true);
webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
webSettings.setAppCacheMaxSize(10 * 1024 * 1024);
webSettings.setAppCachePath(Utils.getContext().getExternalCacheDir().getAbsolutePath());
// 這兩個(gè)一起設(shè)置,表明WebView會(huì)自適應(yīng)手機(jī)屏幕的寬度
webSettings.setUseWideViewPort(true);
webSettings.setLoadWithOverviewMode(true);
//設(shè)置 WebView 的字體,可以通過(guò)這個(gè)函數(shù),改變 WebView 的字體,默認(rèn)字體為 "sans-serif"
webSettings.setStandardFontFamily("");
//設(shè)置 WebView 字體的大小,默認(rèn)大小為 16
webSettings.setDefaultFontSize(20);
//設(shè)置 WebView 支持的最小字體大小,默認(rèn)為 8
webSettings.setMinimumFontSize(12);
//設(shè)置文本的縮放倍數(shù),默認(rèn)為 100
webSettings.setTextZoom(2);
// 允許高版本http和https混合訪問(wèn)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
// 支持PC上的Chrome調(diào)試WebView,具體方法請(qǐng)百度
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// 只支持Debug模式下調(diào)試
if (0 != (getApplicationInfo().flags &= ApplicationInfo.FLAG_DEBUGGABLE)) {
WebView.setWebContentsDebuggingEnabled(true);
}
}
2.3 然后是WebViewClient和ChromeClient的設(shè)置
WebViewClient主要幫助WebView處理各種通知、請(qǐng)求事件
WebChromeClient主要輔助WebView處理Javascript的對(duì)話框、網(wǎng)站圖標(biāo)、網(wǎng)站title、加載進(jìn)度等
2.3.1 WebViewClient的常用設(shè)置
-
-
shouldOverrideUrlLoading方法:在點(diǎn)擊請(qǐng)求的是鏈接時(shí)會(huì)調(diào)用此方法。返回false表明交給系統(tǒng)處理,我們自己不干預(yù),正常情況下這么做;返回true,表明在這里攔截了WebView加載的事件,具體如何處理自己看。
一般情況下
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @Override public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { view.loadUrl(request.getUrl().toString()); return true; } @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); return true; } -
-
2.onReceivedSslError:處理https請(qǐng)求。2.1以上版本,前提setJavaScriptEnabled(true);
@Override public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { //handler.cancel(); 默認(rèn)的處理方式,WebView變成空白頁(yè) handler.proceed();//接受證書 //handleMessage(Message msg); 其他處理 }
-
3.onLoadResource:加載資源時(shí)調(diào)用,每個(gè)資源(比如圖片,js,CSS等)都會(huì)調(diào)用一次。
@Override public void onLoadResource(WebView view, String url) -
4.onPageStarted和onPageStarted:頁(yè)面加載開始和結(jié)束后會(huì)調(diào)用。
@Override public void onPageStarted(WebView view, String url, Bitmap favicon) @Override public void onPageFinished(WebView view, String url) -
5.shouldInterceptRequest:同樣是加載資源時(shí)調(diào)用,但是這里WebView可以加載本地的資源提供給內(nèi)核,若本地處理返回?cái)?shù)據(jù),內(nèi)核不從網(wǎng)絡(luò)上獲取數(shù)據(jù)。從API 11時(shí)引入, API21更新重載方法。加載本地資源使用方法請(qǐng)看 這篇博客 的 常用資源預(yù)加載 。
//API 22添加的重載方法 @Override public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) @Override public WebResourceResponse shouldInterceptRequest(WebView view, String url)
因?yàn)樯厦娴牟┛捅粍h除了,在這里寫一下如何加載本地資源,包括Js文件,圖片文件等等
這里舉例:需要加載本地的Js文件jsTest.js和icon.png。注意,這里需要把文件放在src/main/assests下
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
String url = request.getUrl().toString();
if (!TextUtils.isEmpty(url) && url.contains("jsTest.js")) {
return editResponse("jsTest.js");
} else if (!TextUtils.isEmpty(url) && url.contains("icon.png")) {
return editResponse("icon.png");
}
return super.shouldInterceptRequest(view, request);
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
// 因?yàn)榧虞d每個(gè)圖片,Js資源都要請(qǐng)求一次網(wǎng)絡(luò)。所以這種判斷這種資源是否在本地,如果在本地,就直接返回本地的資源,不再去網(wǎng)上取
if (url.contains("jsTest.js")) {
return editResponse("jsTest.js");
} else if (url.contains("icon.png")) {
return editResponse("icon.png");
}
return super.shouldInterceptRequest(view, url);
}
/**
* 自定義的得到src/main/assests下資源文件的方法
*/
private WebResourceResponse editResponse(String fileName) {
try {
return new WebResourceResponse("application/x-javascript", "utf-8", getAssets().open(fileName));
} catch (IOException e) {
e.printStackTrace();
}
//需處理特殊情況
return null;
}
2.3.2 ChromeClient的常用設(shè)置
1.onJsAlert/onJsConfirm/onJsPrompt方法,對(duì)應(yīng)JS的對(duì)話框,確認(rèn)框,輸入框。JS彈出對(duì)話框等時(shí),會(huì)調(diào)用對(duì)應(yīng)的方法,我們?cè)诜椒ㄖ袕棾鯝lertDialog等,顯示相應(yīng)信息即可
-
2.onProgressChanged:通知應(yīng)用程序當(dāng)前網(wǎng)頁(yè)加載的進(jìn)度
@Override public void onProgressChanged(WebView view, int newProgress) -
3.onReceivedTitle:獲取網(wǎng)頁(yè)title標(biāo)題。
獲取標(biāo)題的時(shí)間主要取決于網(wǎng)頁(yè)前段設(shè)置標(biāo)題的位置,一般設(shè)置在頁(yè)面加載前面,可以較早調(diào)用到這個(gè)函數(shù)
@Override public void onReceivedTitle(WebView view, String title)
-
4.H5播放器全屏和去全屏方法
//有H5視頻,按下全屏播放時(shí)調(diào)用的方法 @Override public void onShowCustomView(View view, CustomViewCallback callback) // 對(duì)應(yīng)的取消全屏方法 @Override public void onHideCustomView()
-
5.設(shè)置WebView視頻未播放時(shí)默認(rèn)顯示占位圖。關(guān)于WebView中的視頻播放的相關(guān)知識(shí),請(qǐng)點(diǎn)這里
@Override public Bitmap getDefaultVideoPoster()
3. Native與JS的相互調(diào)用
這個(gè)網(wǎng)上有很多資源了,選擇一個(gè)我覺(jué)得比較全面的博客:Android:你要的WebView與 JS 交互方式 都在這里了。
這里只說(shuō)重點(diǎn):
Js調(diào)用Android現(xiàn)在一般是用WebView的addJavascriptInterface()方式,當(dāng)然因?yàn)锳ndroid版本造成的漏洞不能忽視
Android調(diào)用Js,毫無(wú)疑問(wèn),evaluateJavascript()和loadUrl結(jié)合使用,根據(jù)版本判斷
常見(jiàn)錯(cuò)誤:
-
1.線程錯(cuò)誤。Js調(diào)Android時(shí)發(fā)生,Android調(diào)時(shí)也經(jīng)常發(fā)生,因?yàn)檎{(diào)了Js,很多情況還是會(huì)回調(diào)Android。報(bào)錯(cuò)信息大致為Js線程必須一致等。很好解決,Android中在UI線程即可。但是推薦WebView.post()的方式,不推薦runOnUiThread()方式
mWebView.post(new Runnable() { @Override public void run() { // TODO } });
4. Cookie問(wèn)題
4.1 WebView是有自己的Cookie系統(tǒng)的
WebView會(huì)在本地維護(hù)每次會(huì)話的cookie(保存在data/data/package_name/app_WebView/Cookies.db)。當(dāng)WebView加載URL的時(shí)候,WebView會(huì)從本地讀取該URL對(duì)應(yīng)的cookie,并攜帶該cookie與服務(wù)器進(jìn)行通信。
WebView通過(guò)android.webkit.CookieManager類來(lái)維護(hù)cookie。CookieManager是WebView的cookie管理類。
4.2 Native獲得Cookie,設(shè)置給WebView如何做。
很多時(shí)候我們需要將在native中的 登陸的狀態(tài) 同步到H5中避免再次登陸,這就需要用到對(duì)Cookie的管理。
問(wèn)題分解:
1.在OkHttp(假定使用的是OkHttp,其他的方式獲得cookie更簡(jiǎn)單)中獲得cookie,并儲(chǔ)存;
2.取得cookie并設(shè)置給WebView。
解決:
4.2.1 Okhttp中獲得cookie。
獲得cookie很簡(jiǎn)單,只需在OkHttpClient的構(gòu)建過(guò)程中加一行代碼。
List<Cookie> mCookies;
mOkHttpClient = new OkHttpClient.Builder()
...
// 下面就是對(duì)OkHttp的cookie的處理
.cookieJar(new CookieJar() {
// 對(duì)服務(wù)器返回的cookie的處理的方法
// 參數(shù)url和cookie就是cookie對(duì)應(yīng)的url和cookie的值
@Override
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
// 對(duì)cookie的處理,一般是存到內(nèi)存中,使其他地方可以獲得
mCookies = cookies;
}
// 發(fā)送請(qǐng)求時(shí)的cookie的處理,返回的List<Cookie>即請(qǐng)求的cookie
@Override
public List<Cookie> loadForRequest(HttpUrl url) {
return mCookies;
//return null;
}
})
.build();
當(dāng)然,也可以新建一個(gè)類,實(shí)現(xiàn)CookieJar,專門處理cookie問(wèn)題。這里只是給出了最簡(jiǎn)單的對(duì)cookie的處理,對(duì)于持久化等,就是另外的問(wèn)題了。
4.2.2 將cookie同步到WebView中
private void syncCookies(String url, List<Cookie> cookies) {
// 一些前提設(shè)置
CookieSyncManager.createInstance(this);
final CookieManager cookieManager = CookieManager.getInstance();
cookieManager.setAcceptCookie(true);
/**
* 設(shè)置webView支持JS的Cookie的調(diào)用,5.0以上才要設(shè)置
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cookieManager.setAcceptThirdPartyCookies(mWebView, true);
}
//cookieManager.removeAllCookie();
// 向WebView中添加Cookie,
for (Cookie cookie : cookies) {
cookieManager.setCookie(url, cookie.toString());
}
// 刷新,同步
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
CookieManager.getInstance().flush();
}
CookieSyncManager.getInstance().sync();
//String newCookie = cookieManager.getCookie(url);驗(yàn)證是否將cookie同步進(jìn)去
//KLog.i("newCookie: " + newCookie);
}
這里有幾點(diǎn)要注意:
1.順序。
// 1.先初始化WebView,各種設(shè)置setting,webViewClient和chromeClient等
initWebView();
// 2.再獲得,并同步cookie
// MyCookieJar.getInstance().getCookies()可以換成ApiManager.getInstance().getCookies()等
syncCookies(url, MyCookieJar.getInstance().getCookies());
// 3.最后加載url
mWebView.loadUrl(url);
2.cookie的添加時(shí),最好是一個(gè)cookie,set一次,最好不要自己拼接,否則關(guān)于domain,path,逗號(hào),分號(hào)等等的問(wèn)題會(huì)讓人欲仙欲死。還有說(shuō)法是用String不行,要用StringBuilder。所以,盡量不自己拼。
4.3 Ajax跨域訪問(wèn)時(shí),Cookie帶不過(guò)去的解決方法
問(wèn)題:Native已經(jīng)登錄,cookie可以設(shè)置進(jìn)去,但是網(wǎng)頁(yè)進(jìn)行了復(fù)雜的ajax操作(我也不知道什么操作),cookie帶不過(guò)去,到指定頁(yè)面還得登錄。5.0以下正常
解決:經(jīng)過(guò)排查,發(fā)現(xiàn)高版本時(shí)問(wèn)題出在ajax跳轉(zhuǎn)時(shí),是Js對(duì)Cookie的操作,不經(jīng)過(guò)WebView,正常的WebView設(shè)置沒(méi)有作用??戳撕芏嗖┛?,還是在StackOverFlow上發(fā)現(xiàn)解決方法。一行代碼
final CookieManager cookieManager = CookieManager.getInstance();
cookieManager.setAcceptCookie(true);
/**
* 設(shè)置webView支持JS的Cookie的調(diào)用,5.0以上才要設(shè)置
*/
// 大致意思:mWebView接收第三方對(duì)Cookie的操作,也就是支持Js對(duì)cookie的操作
cookieManager.setAcceptThirdPartyCookies(mWebView, true);
5. 棘手問(wèn)題
5.1 Alert劫持
Alert劫持:Alert只會(huì)彈出一次,并且WebView會(huì)卡死。重新加載都不行,必須殺死進(jìn)程,重新打開App
解決方法很簡(jiǎn)單,在自定義的onJsAlert方法中加一行代碼
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
...
result.confirm();// 不加這行代碼,會(huì)造成Alert劫持:Alert只會(huì)彈出一次,并且WebView會(huì)卡死
return true;
}