現在 Android 應用中都會多多少少嵌入一些 H5 頁面,在 Android SDK 中有一個名為 WebView 的組件,它是一個 webkit 內核的高性能瀏覽器,在 Android 應用中的 H5 頁面用它展示是非常合適的。本篇文章就介紹一下 WebView 相關的一些知識,相關代碼放在 GitHub 的 WebViewPractice 的工程里。
本篇文章主要介紹以下幾點:
- WebView 的簡單使用
- WebViewClient
- WebChromeClient
- WebViewClient 和 WebChromeClient 的區別
- Js 和 Java 代碼的交互
- WebView 的緩存
- WebView 引起的內存泄露
1. WebView 的簡單使用
如果只是想簡單地使用 WebView,通過下面幾步即可展示一般功能需求的 H5 頁面。
在
AndroidManifest.xml
申請網絡權限-
在 XML 布局文件中添加
WebView
控件,并在 Java 代碼中得到該 WebView 的對象<!-- activity_main.xml --> <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/ll_webview_root" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> ...... <WebView android:id="@+id/webview" android:layout_width="match_parent" android:layout_height="match_parent"/> ...... </LinearLayout>
// MainActivity.java @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); WebView webView = (WebView) findViewById(R.id.webview); webView.getSettings().setJavaScriptEnabled(true); webView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); return true; } @TargetApi(21) @Override public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { view.loadUrl(request.getUrl().toString()); return true; } }); webView.loadUrl("https://ke.youdao.com"); }
-
如果訪問的頁面中含有 JavaScript 的代碼,則需要設置 WebView 支持 Javascript。
webView.getSettings().setJavaScriptEnabled(true);
-
如果頁面中含有連接,點擊鏈接如果想繼續在當前瀏覽器中瀏覽網頁,則需要重寫 WebView 的 WebViewClient 對象。
webView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); return true; } @TargetApi(21) @Override public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { view.loadUrl(request.getUrl().toString()); return true; } });
如果不重寫 WebView 的對象 WebViewClient,點擊頁面中的連接則會在手機系統自帶的瀏覽器中打開新的鏈接。
2. WebViewClient
在第一節中已經簡單使用過 WebViewClient 這個類,還有其他一些常用的方法可以重寫并使用,其含義如下所示:
webView.setWebViewClient(new WebViewClient() {
// 給應用接管處理某些 url 請求的機會,返回 true 則攔截該請求,返回 false 不攔截,已經被廢棄
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
// 給應用接管處理某些 url 請求的機會,返回 true 則攔截該請求,返回 false 不攔截,是上面方法的替代方法
// WebResourceRequest 中含有請求的 url,請求方法和請求頭等信息
@TargetApi(21)
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
view.loadUrl(request.getUrl().toString());
return true;
}
// 頁面開始加載時回調
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
}
// 頁面結束加載時回調
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
}
// 在加載 url 對應的資源時會回調此方法
@Override
public void onLoadResource(WebView view, String url) {
super.onLoadResource(view, url);
}
// 在加載 url 對應資源時會回調此方法,不同的是,可以通過返回值控制加載的數據。此方法已被廢棄
// 若返回 null,WebView 會正常加載該資源
// 若返回 WebResourceResponse 類型的對象,則 WebView 會使用該對象
// 需要注意的是,此方法不在 UI 線程中被調用
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
return super.shouldInterceptRequest(view, url);
}
// 是上面方法的替代方法,使用方法和上面方法一致
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
return super.shouldInterceptRequest(view, request);
}
// 加載資源出錯時會被回調的方法
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
super.onReceivedError(view, request, error);
}
// 加載資源時 HTTP 請求出錯會回調此方法
@Override
public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
super.onReceivedHttpError(view, request, errorResponse);
}
// 請求 HTTPS 資源出錯時會回調此方法
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
super.onReceivedSslError(view, handler, error);
}
// 攔截瀏覽器中的按鍵事件
// 若返回 true,則攔截按鍵事件
// 若返回 false,則由 WebView 處理該事件
@Override
public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {
return super.shouldOverrideKeyEvent(view, event);
}
// 當頁面的縮放比例發生變化時會回調此方法
@Override
public void onScaleChanged(WebView view, float oldScale, float newScale) {
super.onScaleChanged(view, oldScale, newScale);
}
});
3. WebChromeClient
同 WebViewClient 一樣,通過為 WebView 設置 WebChromeClient 對象,并重寫其中的一些方法可以對 WebView 的一些行為進行控制。
webView.setWebChromeClient(new WebChromeClient() {
// 網頁的加載進度
@Override
public void onProgressChanged(WebView view, int newProgress) {
L.i("onProgressChanged " + newProgress);
super.onProgressChanged(view, newProgress);
}
// 接收到網頁的 title
@Override
public void onReceivedTitle(WebView view, String title) {
L.i("onReceivedTitle " + title);
super.onReceivedTitle(view, title);
}
// 接收到網頁的 icon
@Override
public void onReceivedIcon(WebView view, Bitmap icon) {
super.onReceivedIcon(view, icon);
}
@Override
public void onReceivedTouchIconUrl(WebView view, String url, boolean precomposed) {
super.onReceivedTouchIconUrl(view, url, precomposed);
}
// 當 H5 頁面中點擊播放的 flash video 的全屏按鈕時,會調用這個方法
@Override
public void onShowCustomView(View view, CustomViewCallback callback) {
super.onShowCustomView(view, callback);
}
// 與 onShowCustomView() 對應的取消全屏時會調用的方法
@Override
public void onHideCustomView() {
super.onHideCustomView();
}
@Override
public void onShowCustomView(View view, int requestedOrientation, CustomViewCallback callback) {
super.onShowCustomView(view, requestedOrientation, callback);
}
// http://www.cnblogs.com/ufreedom/p/4229590.html
// 當創建新的 Window 時會調用此方法
@Override
public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
return super.onCreateWindow(view, isDialog, isUserGesture, resultMsg);
}
// 和 onCreateWindow() 方法對應的,關閉 Window
@Override
public void onCloseWindow(WebView window) {
super.onCloseWindow(window);
}
// 當 WebView 獲取焦點時會調用此方法
@Override
public void onRequestFocus(WebView view) {
super.onRequestFocus(view);
}
// 在 Js 代碼中彈出 Alert 窗口時會調用此方法
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
return super.onJsAlert(view, url, message, result);
}
// 在 Js 代碼中彈出 Confirm 窗口時會調用此方法
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
return super.onJsConfirm(view, url, message, result);
}
// 在 Js 代碼中彈出 Prompt 窗口時會調用此方法
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
return super.onJsPrompt(view, url, message, defaultValue, result);
}
// 在 Js 加載之前會調用此方法
@Override
public boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result) {
return super.onJsBeforeUnload(view, url, message, result);
}
// 打印 Js 中的日志 console 信息,被廢棄
@Override
public void onConsoleMessage(String message, int lineNumber, String sourceID) {
super.onConsoleMessage(message, lineNumber, sourceID);
}
// 打印 Js 中的日志 console 信息,被廢棄
@Override
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
return super.onConsoleMessage(consoleMessage);
}
});
4. WebViewClient 和 WebChromeClient 的區別
WebViewClient 主要幫助 WebView 處理一些網絡請求方面的行為和操作,比如:各種資源的請求、請求資源出現錯誤、HTTPS請求出現錯誤的通知等。
WebChromeClient 主要幫助 WebView 處理一些 Javascript 相關的一些細節,比如:各種對話框的彈出、Js 中日志信息的打印、Window 的創建和關閉、以及 title 和 icon 的接收等。
5. Js 和 Java 代碼的交互
Java 代碼和 Js 代碼額相互調用已經老生常談了,現在也有一些很優秀的開源框架,使用起來非常的方便,但是其原理都是一樣的,下面就介紹一下 Js 代碼和 Java 代碼是如何進行交互的。
5.1 Js 代碼調用 Java 代碼
private class InnerClass {
private Context mContext = null;
public InnerClass(Context context) {
mContext = context;
}
@JavascriptInterface
public void toastMessage(String msg) {
Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();
}
}
public class JSBridgeActivity extends AppCompatActivity {
private WebView webView = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_jsbridge);
initWebView();
}
private void initWebView() {
webView = (WebView) findViewById(R.id.webView);
webView.getSettings().setJavaScriptEnabled(true);
webView.addJavascriptInterface(new InnerClass(JSBridgeActivity.this), "innerClass");
webView.loadUrl("file:///android_asset/demo.html");
webView.setWebChromeClient(new WebChromeClient());
}
}
- 有名為
InnerClass
的類,其中的方法toastMessage(String msg)
被@JavascriptInterface
注解所修飾 - 設置 WebView 對象支持 JavaScript,并且調用
addJavascriptInterface(Object object, String name)
方法,將InnerClass
的對象傳入 WebView 對象中 - 在 WebView 加載頁面的 Js 代碼中,即可通過
name
調用InnerClass
對象的toastMessage(String msg)
方法,如下所示:
function testPrompt(){
window.innerClass.toastMessage("jsbridge");
}
5.2 Java 代碼調用 Js 代碼
假如有如下的 Js 方法,如下所示:
function testConfirm(){
var temp = "temp";
confirm(temp);
}
在 Java 中可以通過如下方法調用該 Js 方法:
webView.loadUrl("javascript:testConfirm()");
注意
- 在 Java 代碼中調用 Js 代碼彈出
alert
、confirm
和prompt
對話框的時候,需要給 WebView 設置 WebChromeClient 對象才可以正常的彈出對話框,否則不會有效果。 - Java 調用 Js 的代碼需要在主線程中調用才會生效
- 在 Activity 的 onCreate() 方法中直接調用 WebView.loadUrl() 方法是不會生效的,需要在WebView 加載完成之后再調用 Js 的代碼,才會生效。
6. WebView 的緩存
6.1 CacheMode 緩存模式
通過 WebView 的 WebSettings 對象可以設置 WebView 的 CacheMode 緩存策略,共有如下幾個值可供設置:
- LOAD_DEFAULT:默認設置,如果有本地緩存,且緩存有效未過期,則直接使用本地緩存,否則加載網絡數據
- LOAD_NORMAL:已被廢棄,同 LOAD_DEFAULT 效果一樣
- LOAD_CACHE_ELSE_NETWORK:如果存在緩存時,不論緩存是否過期,都使用緩存;若緩存不存在,則從網絡獲取數據
- LOAD_NO_CACHE:不使用緩存,只從網絡獲取數據
- LOAD_CACHE_ONLY:不從網絡獲取數據,只使用緩存數據
6.2 WebView 緩存路徑
我測試用的手機是小米 note,Android 系統是 4.4,WebView
緩存的文件存在于 /data/data/${package_name}/app_webview/cache/
路徑下
6.3 H5 緩存機制
H5 的緩存機制共有 5 種緩存機制,分別是:
- 瀏覽器緩存機制:主要通過 HTTP 的協議頭中的
Cache-Control
(或Expires
)和Last-Modified
(或Etag
)字段控制控制文件的緩存。 - Application Cache(AppCache)緩存機制:
AppCache
緩存是對瀏覽器緩存的補充,不可替代。 - Dom Storage 緩存機制:Dom Storage 提供 5MB 大小的緩存空間,以鍵值對的形式存取文件。
- Web SQL Database 緩存機制:H5 提供了基于 SQL 的數據庫存儲機制,用于存儲一些結構化的數據。
- Indexed Database 緩存機制:不同于 SQL 數據庫,屬于 NoSQL 數據庫。Indexed Database 存儲的數據是以鍵值對的形式存儲數據的。
共有這 5 種緩存機制,值得詳細說明,在此就不再做過多的說明,爭取后續單獨寫一篇博客介紹緩存機制。
7. WebView 引起的內存泄露
在 Activity
中使用 WebView
有可能會出現內存泄露的情況,Android 5.1 WebView內存泄漏分析 和 【Android】 WebView內存泄漏優化之路 這兩篇文章已經介紹的很詳細。
在 Android 5.1
中,如果 WebView
的 destroy()
方法在 onDetachedFromWindow()
方法之前被執行,則會出現 WebView
持有 Activity
的引用,從而導致內存泄露。
為了避免 WebView
引起內存泄露,有兩個比較有效的辦法:
- 使用代碼 New 一個
WebView
對象,而不是在 xml 文件中靜態寫入一個WebView
。 - 在
Activity
的onDestory()
方法中,主動的將WebView
從父容器中移除并銷毀該WebView
對象,如下所示:
@Override
protected void onDestroy() {
super.onDestroy();
if (webView != null) {
ViewParent viewParent = webView.getParent();
if (viewParent != null) {
((ViewGroup) viewParent).removeView(webView);
}
webView.getSettings().setJavaScriptEnabled(false);
webView.clearHistory();
webView.clearView();
webView.removeAllViews();
webView.destroy();
}
}
本文中涉及到的自定義類的源碼都在 Github 上的 WebViewPractice 工程中。
參考資料:
Android 5.1 WebView內存泄漏分析 -- 永遠即等待
Android:手把手教你構建 WebView 的緩存機制 & 資源預加載方案 -- Carson_Ho