Android WebView 詳解

ezy發表於2017-08-29

reezy.me/p/20170515/…

  1. 相關API 1.1. 相關類介紹 1.2. WebView 1.3. WebSettings 1.4. WebViewClient 1.5. WebChromeClient
  2. 回撥順序
  3. 視口(viewport)
  4. 管理 Cookies
  5. 快取(Cache)
  6. 預載入(Preload)
  7. 與Javascript互動
  8. 地理位置(Geolocation)
  9. 彈框(alert/confirm/prompt/onbeforeunload)
  10. 全屏(Fullscreen)
  11. 記憶體洩漏
  12. 參考

1.相關API

1.1.相關類介紹

  • WebResourceRequest 新增於API21,封裝了一個Web資源的請求資訊,包含:請求地址,請求方法,請求頭,是否主框架,是否使用者點選,是否重定向
  • WebResourceResponse 封裝了一個Web資源的響應資訊,包含:響應資料流,編碼,MIME型別,API21後新增了響應頭,狀態碼與狀態描述
  • WebResourceError 新增於API23,封裝了一個Web資源的錯誤資訊,包含錯誤碼和描述
  • CookieManager 管理用於WebView的cookies。。
  • WebViewDatabase 儲存與管理以下幾類瀏覽資料:
    • 表單自動填充的的使用者名稱與密碼
    • HTTP認證的使用者名稱與密碼
    • 曾經輸入過的文字(比如自動完成)
  • WebStorage 用於管理WebView提供的JS儲存API,比如Application Cache API,Web SQL Database API,HTML5 Web Storage API
  • GeolocationPermissions 用於管理WebView的JS Geolocation API
  • HttpAuthHandler 表示一個HTTP認證請求,提供了方法操作(proceed/cancel)請求
  • SslErrorHandler 表示一個處理SSL錯誤的請求,提供了方法操作(proceed/cancel)請求
  • ClientCertRequest 表示一個證照請求,提供了方法操作(proceed/cancel/ignore)請求
  • JsResult 用於處理底層JS發起的請求,為客戶端提供一些方法指明應進行的操作,比如確認或取消。

1.2.WebView

基本

// 獲取當前頁面的URL
public String getUrl();
// 獲取當前頁面的原始URL(重定向後可能當前url不同)
// 就是http headers的Referer引數,loadUrl時為null
public String getOriginalUrl();
// 獲取當前頁面的標題
public String getTitle();
// 獲取當前頁面的favicon
public Bitmap getFavicon();
// 獲取當前頁面的載入進度
public int getProgress();

// 通知WebView核心網路狀態
// 用於設定JS屬性`window.navigator.isOnline`和產生HTML5事件`online/offline`
public void setNetworkAvailable(boolean networkUp)

// 設定初始縮放比例
public void setInitialScale(int scaleInPercent)複製程式碼

載入網頁

// 載入URL指定的網頁
public void loadUrl(String url);
// 攜帶http headers載入URL指定的網頁
public void loadUrl(String url, Map<String, String> additionalHttpHeaders);
// 使用POST請求載入指定的網頁
public void postUrl(String url, byte[] postData);
// 重新載入當前網頁
public void reload();

// 載入內容
public void loadData(String data, String mimeType, String encoding);
// 使用baseUrl載入內容
public void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl);
複製程式碼

Javascript

// 注入Javascript物件
public void addJavascriptInterface(Object object, String name);
// 移除已注入的Javascript物件,下次載入或重新整理頁面時生效
public void removeJavascriptInterface(String name);

// 對傳入的JS表示式求值,通過resultCallback返回結果
// 此函式新增於API19,必須在UI執行緒中呼叫,回撥也將在UI執行緒
public void evaluateJavascript(String script, ValueCallback<String> resultCallback)
複製程式碼

導航(前進後退)

// 複製一份BackForwardList
public WebBackForwardList copyBackForwardList();
// 是否可後退
public boolean canGoBack();
// 是否可前進
public boolean canGoForward();
// 是否可前進/後退steps頁,大於0表示前進小於0表示後退
public boolean canGoBackOrForward(int steps);

// 後退一頁
public void goBack();
// 前進一頁
public void goForward();
// 前進/後退steps頁,大於0表示前進小於0表示後退
public void goBackOrForward(int steps);

// 清除當前webview訪問的歷史記錄
public void clearHistory();
複製程式碼

網頁查詢功能

// 設定網頁查詢結果回撥
public void setFindListener(FindListener listener);
// 非同步執行查詢網頁內包含的字串並設定高亮,查詢結果會回撥.
public void findAllAsync (String find);
// 查詢下一個匹配的字串
public void findNext (boolean forward);
// 清除網頁查詢的高亮匹配字串
public void clearMatches();
複製程式碼

截圖/翻頁/縮放

// 儲存網頁(.html)到指定檔案
public void saveWebArchive(String filename);
// 儲存網頁(.html)到檔案
public void saveWebArchive(String basename, boolean autoname, ValueCallback<String> callback)// 上翻一頁,即向上滾動WebView高度的一半
public void pageUp(boolean top);
// 下翻一頁,即向下滾動WebView高度的一半
public void pageDown(boolean bottom);

// 縮放
public void zoomBy(float factor);
// 放大
public boolean zoomIn();
// 縮放
public boolean zoomOut(); 
複製程式碼

其它

// 清除網頁快取,由於核心快取是全域性的因此這個方法不僅僅針對webview而是針對整個應用程式
public void clearCache(boolean includeDiskFiles);
// 清除自動完成填充的表單資料
public void clearFormData();
// 清除SSL偏好
public void clearSslPreferences();

// 查詢文件中是否有圖片,查詢結果將被髮送到msg.getTarget()
// 如果包含圖片,msg.arg1 為1,否則為0
public void documentHasImages(Message msg);

// 請求最近輕叩(tapped)的 錨點/影像 元素的URL,查詢結果將被髮送到msg.getTarget()
// msg.getData()中的url是錨點的href屬性,title是錨點的文字,src是影像的src
public void requestFocusNodeHref(Message msg);

// 請求最近觸控(touched)的 影像元素的URL,查詢結果將被髮送到msg.getTarget()
// msg.getData()中的url是影像連結
public void requestImageRef(Message msg) 


// 清除證照請求偏好,新增於API21
// 在WebView收到`android.security.STORAGE_CHANGED` Intent時會自動清除
public static void clearClientCertPreferences(Runnable onCleared)

// 開啟網頁內容(js,css,html...)除錯模式,新增於API19
public static void setWebContentsDebuggingEnabled(boolean enabled) 
複製程式碼

1.3.WebSettings

WebSettings settings = web.getSettings();

// 儲存(storage)
// 啟用HTML5 DOM storage API,預設值 false
settings.setDomStorageEnabled(true); 
// 啟用Web SQL Database API,這個設定會影響同一程式內的所有WebView,預設值 false
// 此API已不推薦使用,參考:https://www.w3.org/TR/webdatabase/
settings.setDatabaseEnabled(true);  
// 啟用Application Caches API,必需設定有效的快取路徑才能生效,預設值 false
// 此API已廢棄,參考:https://developer.mozilla.org/zh-CN/docs/Web/HTML/Using_the_application_cache
settings.setAppCacheEnabled(true); 
settings.setAppCachePath(context.getCacheDir().getAbsolutePath());

// 定位(location)
settings.setGeolocationEnabled(true);

// 是否儲存表單資料
settings.setSaveFormData(true);
// 是否當webview呼叫requestFocus時為頁面的某個元素設定焦點,預設值 true
settings.setNeedInitialFocus(true);  

// 是否支援viewport屬性,預設值 false
// 頁面通過`<meta name="viewport" ... />`自適應手機螢幕
settings.setUseWideViewPort(true);
// 是否使用overview mode載入頁面,預設值 false
// 當頁面寬度大於WebView寬度時,縮小使頁面寬度等於WebView寬度
settings.setLoadWithOverviewMode(true);
// 佈局演算法
settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL);

// 是否支援Javascript,預設值false
settings.setJavaScriptEnabled(true); 
// 是否支援多視窗,預設值false
settings.setSupportMultipleWindows(false);
// 是否可用Javascript(window.open)開啟視窗,預設值 false
settings.setJavaScriptCanOpenWindowsAutomatically(false);

// 資源訪問
settings.setAllowContentAccess(true); // 是否可訪問Content Provider的資源,預設值 true
settings.setAllowFileAccess(true);    // 是否可訪問本地檔案,預設值 true
// 是否允許通過file url載入的Javascript讀取本地檔案,預設值 false
settings.setAllowFileAccessFromFileURLs(false);  
// 是否允許通過file url載入的Javascript讀取全部資源(包括檔案,http,https),預設值 false
settings.setAllowUniversalAccessFromFileURLs(false);

// 資源載入
settings.setLoadsImagesAutomatically(true); // 是否自動載入圖片
settings.setBlockNetworkImage(false);       // 禁止載入網路圖片
settings.setBlockNetworkLoads(false);       // 禁止載入所有網路資源

// 縮放(zoom)
settings.setSupportZoom(true);          // 是否支援縮放
settings.setBuiltInZoomControls(false); // 是否使用內建縮放機制
settings.setDisplayZoomControls(true);  // 是否顯示內建縮放控制元件

// 預設文字編碼,預設值 "UTF-8"
settings.setDefaultTextEncodingName("UTF-8");
settings.setDefaultFontSize(16);        // 預設文字尺寸,預設值16,取值範圍1-72
settings.setDefaultFixedFontSize(16);   // 預設等寬字型尺寸,預設值16
settings.setMinimumFontSize(8);         // 最小文字尺寸,預設值 8
settings.setMinimumLogicalFontSize(8);  // 最小文字邏輯尺寸,預設值 8
settings.setTextZoom(100);              // 文字縮放百分比,預設值 100

// 字型
settings.setStandardFontFamily("sans-serif");   // 標準字型,預設值 "sans-serif"
settings.setSerifFontFamily("serif");           // 襯線字型,預設值 "serif"
settings.setSansSerifFontFamily("sans-serif");  // 無襯線字型,預設值 "sans-serif"
settings.setFixedFontFamily("monospace");       // 等寬字型,預設值 "monospace"
settings.setCursiveFontFamily("cursive");       // 手寫體(草書),預設值 "cursive"
settings.setFantasyFontFamily("fantasy");       // 幻想體,預設值 "fantasy"


if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    // 使用者是否需要通過手勢播放媒體(不會自動播放),預設值 true
    settings.setMediaPlaybackRequiresUserGesture(true);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    // 5.0以上允許載入http和https混合的頁面(5.0以下預設允許,5.0+預設禁止)
    settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    // 是否在離開螢幕時光柵化(會增加記憶體消耗),預設值 false
    settings.setOffscreenPreRaster(false);
}

if (isNetworkConnected(context)) {
    // 根據cache-control決定是否從網路上取資料
    settings.setCacheMode(WebSettings.LOAD_DEFAULT);
} else {
    // 沒網,離線載入,優先載入快取(即使已經過期)
    settings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
}

// deprecated
settings.setRenderPriority(WebSettings.RenderPriority.HIGH);
settings.setDatabasePath(context.getDir("database", Context.MODE_PRIVATE).getPath());
settings.setGeolocationDatabasePath(context.getFilesDir().getPath());
複製程式碼

通常大部分保持預設值就好了

WebSettings settings = web.getSettings();
// 快取(cache)
settings.setAppCacheEnabled(true);      // 預設值 false
settings.setAppCachePath(context.getCacheDir().getAbsolutePath());

// 儲存(storage)
settings.setDomStorageEnabled(true);    // 預設值 false
settings.setDatabaseEnabled(true);      // 預設值 false 
 
// 是否支援viewport屬性,預設值 false
// 頁面通過`<meta name="viewport" ... />`自適應手機螢幕
settings.setUseWideViewPort(true);
// 是否使用overview mode載入頁面,預設值 false
// 當頁面寬度大於WebView寬度時,縮小使頁面寬度等於WebView寬度
settings.setLoadWithOverviewMode(true);

// 是否支援Javascript,預設值false
settings.setJavaScriptEnabled(true);    

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    // 5.0以上允許載入http和https混合的頁面(5.0以下預設允許,5.0+預設禁止)
    settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
} 

if (isNetworkConnected(context)) {
    // 根據cache-control決定是否從網路上取資料
    settings.setCacheMode(WebSettings.LOAD_DEFAULT);
} else {
    // 沒網,離線載入,優先載入快取(即使已經過期)
    settings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
}
複製程式碼

1.4.WebViewClient

// 攔截頁面載入,返回true表示宿主app攔截並處理了該url,否則返回false由當前WebView處理
// 此方法在API24被廢棄,不處理POST請求
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    return false;
}

// 攔截頁面載入,返回true表示宿主app攔截並處理了該url,否則返回false由當前WebView處理
// 此方法新增於API24,不處理POST請求,可攔截處理子frame的非http請求
@TargetApi(Build.VERSION_CODES.N)
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
    return shouldOverrideUrlLoading(view, request.getUrl().toString());
}

// 此方法廢棄於API21,呼叫於非UI執行緒
// 攔截資源請求並返回響應資料,返回null時WebView將繼續載入資源
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
    return null;
}

// 此方法新增於API21,呼叫於非UI執行緒
// 攔截資源請求並返回資料,返回null時WebView將繼續載入資源
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
    return shouldInterceptRequest(view, request.getUrl().toString());
}

// 頁面(url)開始載入
public void onPageStarted(WebView view, String url, Bitmap favicon) {
}

// 頁面(url)完成載入
public void onPageFinished(WebView view, String url) {
}

// 將要載入資源(url)
public void onLoadResource(WebView view, String url) {
}

// 這個回撥新增於API23,僅用於主框架的導航
// 通知應用導航到之前頁面時,其遺留的WebView內容將不再被繪製。
// 這個回撥可以用來決定哪些WebView可見內容能被安全地回收,以確保不顯示陳舊的內容
// 它最早被呼叫,以此保證WebView.onDraw不會繪製任何之前頁面的內容,隨後繪製背景色或需要載入的新內容。
// 當HTTP響應body已經開始載入並體現在DOM上將在隨後的繪製中可見時,這個方法會被呼叫。
// 這個回撥發生在文件載入的早期,因此它的資源(css,和影像)可能不可用。
// 如果需要更細粒度的檢視更新,檢視 postVisualStateCallback(long, WebView.VisualStateCallback).
// 請注意這上邊的所有條件也支援 postVisualStateCallback(long ,WebView.VisualStateCallback)
public void onPageCommitVisible(WebView view, String url) {
}

// 此方法廢棄於API23
// 主框架載入資源時出錯
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
}

// 此方法新增於API23
// 載入資源時出錯,通常意味著連線不到伺服器
// 由於所有資源載入錯誤都會呼叫此方法,所以此方法應儘量邏輯簡單
@TargetApi(Build.VERSION_CODES.M)
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
    if (request.isForMainFrame()) {
        onReceivedError(view, error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString());
    }
}

// 此方法新增於API23
// 在載入資源(iframe,image,js,css,ajax...)時收到了 HTTP 錯誤(狀態碼>=400)
public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
}


// 是否重新提交表單,預設不重發
public void onFormResubmission(WebView view, Message dontResend, Message resend) {
    dontResend.sendToTarget();
}

// 通知應用可以將當前的url儲存在資料庫中,意味著當前的訪問url已經生效並被記錄在核心當中。
// 此方法在網頁載入過程中只會被呼叫一次,網頁前進後退並不會回撥這個函式。
public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) {
}

// 載入資源時發生了一個SSL錯誤,應用必需響應(繼續請求或取消請求)
// 處理決策可能被快取用於後續的請求,預設行為是取消請求
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
    handler.cancel();
}

// 此方法新增於API21,在UI執行緒被呼叫
// 處理SSL客戶端證照請求,必要的話可顯示一個UI來提供KEY。
// 有三種響應方式:proceed()/cancel()/ignore(),預設行為是取消請求
// 如果呼叫proceed()或cancel(),Webview 將在記憶體中儲存響應結果且對相同的"host:port"不會再次呼叫 onReceivedClientCertRequest
// 多數情況下,可通過KeyChain.choosePrivateKeyAlias啟動一個Activity供使用者選擇合適的私鑰
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) {
    request.cancel();
}

// 處理HTTP認證請求,預設行為是取消請求
public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
    handler.cancel();
}

// 通知應用有個已授權賬號自動登陸了
public void onReceivedLoginRequest(WebView view, String realm, String account, String args) {
}
// 給應用一個機會處理按鍵事件
// 如果返回true,WebView不處理該事件,否則WebView會一直處理,預設返回false
public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {
    return false;
}

// 處理未被WebView消費的按鍵事件
// WebView總是消費按鍵事件,除非是系統按鍵或shouldOverrideKeyEvent返回true
// 此方法在按鍵事件分派時被非同步呼叫
public void onUnhandledKeyEvent(WebView view, KeyEvent event) {
    super.onUnhandledKeyEvent(view, event);
}

// 通知應用頁面縮放係數變化
public void onScaleChanged(WebView view, float oldScale, float newScale) {
} 
複製程式碼

1.5.WebChromeClient

// 獲得所有訪問歷史專案的列表,用於連結著色。
public void getVisitedHistory(ValueCallback<String[]> callback) {
}

// <video /> 控制元件在未播放時,會展示為一張海報圖,HTML中可通過它的'poster'屬性來指定。
// 如果未指定'poster'屬性,則通過此方法提供一個預設的海報圖。
public Bitmap getDefaultVideoPoster() {
    return null;
}

// 當全屏的視訊正在緩衝時,此方法返回一個佔位檢視(比如旋轉的菊花)。
public View getVideoLoadingProgressView() {
    return null;
}

// 接收當前頁面的載入進度
public void onProgressChanged(WebView view, int newProgress) {
}

// 接收文件標題
public void onReceivedTitle(WebView view, String title) {
}

// 接收圖示(favicon)
public void onReceivedIcon(WebView view, Bitmap icon) {
}

// Android中處理Touch Icon的方案
// http://droidyue.com/blog/2015/01/18/deal-with-touch-icon-in-android/index.html
public void onReceivedTouchIconUrl(WebView view, String url, boolean precomposed) {
}

// 通知應用當前頁進入了全屏模式,此時應用必須顯示一個包含網頁內容的自定義View
public void onShowCustomView(View view, CustomViewCallback callback) {
}

// 通知應用當前頁退出了全屏模式,此時應用必須隱藏之前顯示的自定義View
public void onHideCustomView() {
}


// 顯示一個alert對話方塊
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
    return false;
}

// 顯示一個confirm對話方塊
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
    return false;
}

// 顯示一個prompt對話方塊
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
    return false;
}

// 顯示一個對話方塊讓使用者選擇是否離開當前頁面
public boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result) {
    return false;
}


// 指定源的網頁內容在沒有設定許可權狀態下嘗試使用地理位置API。
// 從API24開始,此方法只為安全的源(https)呼叫,非安全的源會被自動拒絕
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
}

// 當前一個呼叫 onGeolocationPermissionsShowPrompt() 取消時,隱藏相關的UI。
public void onGeolocationPermissionsHidePrompt() {
}

// 通知應用開啟新視窗
public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
    return false;
}

// 通知應用關閉視窗
public void onCloseWindow(WebView window) {
}

// 請求獲取取焦點
public void onRequestFocus(WebView view) {
}

// 通知應用網頁內容申請訪問指定資源的許可權(該許可權未被授權或拒絕)
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void onPermissionRequest(PermissionRequest request) {
    request.deny();
}

// 通知應用許可權的申請被取消,隱藏相關的UI。
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void onPermissionRequestCanceled(PermissionRequest request) {
}

// 為'<input type="file" />'顯示檔案選擇器,返回false使用預設處理
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
    return false;
}

// 接收JavaScript控制檯訊息
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
    return false;
} 
複製程式碼

2.回撥順序

頁面載入回撥順序:

shouldOverrideUrlLoading
onProgressChanged[10]
shouldInterceptRequest 
onProgressChanged[...]
onPageStarted
onProgressChanged[...]
onLoadResource 
onProgressChanged[...]
onReceivedTitle/onPageCommitVisible 
onProgressChanged[100]
onPageFinished
onReceivedIcon
複製程式碼

資源載入回撥:

shouldInterceptRequest() -> onLoadResource() 
複製程式碼

發生重定向時回撥:

onPageStarted() -> shouldOverrideUrlLoading()
複製程式碼

直接loadUrl的回撥:

// 無重定向
onPageStarted() -> onPageFinished()
// 有重定向,shouldOverrideUrlLoading 返回 true 時 onPageFinished 仍會執行
onPageStarted() -> redirection -> ... -> onPageFinished()
複製程式碼

使用者點選連結的回撥:

// shouldOverrideUrlLoading 返回 true 時不執行onPageStarted/onPageFinished
shouldOverrideUrlLoading() -> ...
// 無重定向
shouldOverrideUrlLoading() -> onPageStarted() -> onPageFinished()
// 有重定向
shouldOverrideUrlLoading() -> onPageStarted() -> redirection -> ... -> onPageFinished()
複製程式碼

後退/前進/重新整理 時回撥:

onPageStarted() -> onPageFinished() 
複製程式碼

關於 window.location

假設從A頁面跳轉到B頁面

  • 如果頁面B中直接輸出 window.location="http://example.com",那頁面B不會被加入回退棧,回退將直接回到A頁
  • 如果頁面B載入完成後,比如用setTimeout延遲了,那頁面B會被加入回退棧,當回退到頁面A時會再執行跳轉,這會導致回退功能看起來不正常,需要快速回退兩次才能回到A頁面

3.視口(viewport)

developer.android.com/guide/webap… developer.mozilla.org/en-US/docs/… developer.mozilla.org/zh-CN/docs/…

視口是一個為網頁提供繪圖區域的矩形。

你可以指定數個視口屬性,比如尺寸和初始縮放係數(initial scale)。其中最重要的是視口寬度,它定義了網頁水平方向的可用畫素總數(可用的CSS畫素數)。

多數 Android 上的網頁瀏覽器(包括 Chrome)設定預設視口為一個大尺寸(被稱為"wide viewport mode",寬約 980px)。
也有許多瀏覽器預設會盡可能縮小以顯示完整的視口寬度(被稱為"overview mode")。

// 是否支援viewport屬性,預設值 false
// 頁面通過`<meta name="viewport" ... />`自適應手機螢幕
// 當值為true且viewport標籤不存在或未指定寬度時使用 wide viewport mode
settings.setUseWideViewPort(true);
// 是否使用overview mode載入頁面,預設值 false
// 當頁面寬度大於WebView寬度時,縮小使頁面寬度等於WebView寬度
settings.setLoadWithOverviewMode(true);
複製程式碼

viewport 語法

<meta name="viewport"
      content="
          height = [pixel_value | "device-height"] ,
          width = [pixel_value | "device-width"] ,
          initial-scale = float_value ,
          minimum-scale = float_value ,
          maximum-scale = float_value ,
          user-scalable = ["yes" | "no"]
          " />
複製程式碼

指定視口寬度精確匹配裝置螢幕寬度同時禁用了縮放

<head>
    <title>Example</title>
    <meta name="viewport" content="width=device-width, user-scalable=no" />
</head>
複製程式碼

通過WebView設定初始縮放(initial-scale)

// 設定初始縮放百分比
// 0表示依賴於setUseWideViewPort和setLoadWithOverviewMode
// 100表示不縮放
web.setInitialScale(0)
複製程式碼

4.管理 Cookies

developer.mozilla.org/zh-CN/docs/…

Cookie 是伺服器傳送到使用者瀏覽器並儲存在瀏覽器上的一塊資料,它會在瀏覽器下一次發起請求時被攜帶併傳送到伺服器上。
可通過Cookie儲存瀏覽資訊來獲得更輕鬆的線上體驗,比如保持登入狀態、記住偏好設定,並提供本地的相關內容。

會話Cookie 與 持久Cookie

  • 會話cookie不需要指定Expires和Max-Age,瀏覽器關閉之後它會被自動刪除。
  • 持久cookie指定了Expires或Max-Age,會被儲存到磁碟上,不會因瀏覽器而失效。

第一方Cookie 與 第三方Cookie

每個Cookie都有與之關聯的域,與頁面域一樣的就是第一方Cookie,不一樣的就是第三方Cookie。

// 設定接收第三方Cookie
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    CookieManager.getInstance().setAcceptThirdPartyCookies(vWeb, true);
} 
複製程式碼

讀取/寫入/移除 Cookie

// 獲取指定url關聯的所有Cookie
// 返回值使用"Cookie"請求頭格式:"name=value; name2=value2; name3=value3"
CookieManager.getInstance().getCookie(url);

// 為指定的url設定一個Cookie
// 引數value使用"Set-Cookie"響應頭格式,參考:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Set-Cookie
CookieManager.getInstance().setCookie(url, value);

// 移除指定url下的指定Cookie
CookieManager.getInstance().setCookie(url, cookieName + "=");
複製程式碼

webkit cookie 工具類

public class WebkitCookieUtil { 

    // 移除指定url關聯的所有cookie
    public static void remove(String url) {
        CookieManager cm = CookieManager.getInstance();
        for (String cookie : cm.getCookie(url).split("; ")) {
            cm.setCookie(url, cookie.split("=")[0] + "=");
        }
        flush();
    }

    // sessionOnly 為true表示移除所有會話cookie,否則移除所有cookie
    public static void remove(boolean sessionOnly) {
        CookieManager cm = CookieManager.getInstance();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            if (sessionOnly) {
                cm.removeSessionCookies(null);
            } else {
                cm.removeAllCookies(null);
            }
        } else {
            if (sessionOnly) {
                cm.removeSessionCookie();
            } else {
                cm.removeAllCookie();
            }
        }
        flush();
    }

    // 寫入磁碟
    public static void flush() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            CookieManager.getInstance().flush();
        } else {
            CookieSyncManager.getInstance().sync();
        }
    }
}
複製程式碼

同步系統Cookie 與 Webkit Cookie

// 將系統級Cookie(比如`new URL(...).openConnection()`的Cookie) 同步到 WebView
public class WebkitCookieHandler extends CookieHandler {
    private static final String TAG = WebkitCookieHandler.class.getSimpleName();
    private CookieManager wcm;

    public WebkitCookieHandler() {
        this.wcm = CookieManager.getInstance();
    }

    @Override
    public void put(URI uri, Map<String, List<String>> headers) throws IOException {
        if ((uri == null) || (headers == null)) {
            return;
        }
        String url = uri.toString();

        for (String headerKey : headers.keySet()) {
            if ((headerKey == null) || !(headerKey.equalsIgnoreCase("set-cookie2") || headerKey.equalsIgnoreCase("set-cookie"))) {
                continue;
            }
            for (String headerValue : headers.get(headerKey)) {
                Log.e(TAG, headerKey + ": " + headerValue);
                this.wcm.setCookie(url, headerValue);
            }
        }
    }

    @Override
    public Map<String, List<String>> get(URI uri, Map<String, List<String>> headers) throws IOException {
        if ((uri == null) || (headers == null)) {
            throw new IllegalArgumentException("Argument is null");
        }
        String url = uri.toString();

        String cookie = this.wcm.getCookie(url);
        Log.e(TAG, "cookie: " + cookie);
        if (cookie != null) {
            return Collections.singletonMap("Cookie", Arrays.asList(cookie));
        } else {
            return Collections.emptyMap();
        }
    }
}

複製程式碼

5.快取(Cache)

設定快取模式

  • WebSettings.LOAD_DEFAULT 根據cache-control決定是否從網路上取資料
  • WebSettings.LOAD_CACHE_ELSE_NETWORK 無網,離線載入,優先載入快取(即使已經過期)
  • WebSettings.LOAD_NO_CACHE 僅從網路載入
  • WebSettings.LOAD_CACHE_ONLY 僅從快取載入
// 網路正常時根據cache-control決定是否從網路上取資料 
if (isNetworkConnected(mActivity)) {
    settings.setCacheMode(WebSettings.LOAD_DEFAULT); 
} else {
    settings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); 
}
複製程式碼

清除快取

// 傳入true表示同時記憶體與磁碟,false表示僅清除記憶體
// 由於核心快取是全域性的因此這個方法不僅僅針對webview而是針對整個應用程式
web.clearCache(true);
複製程式碼

6.預載入(Preload)

一個簡單的預載入示例(shouldInterceptRequest)
點選 assets/demo.xml 裡的連結"hello"時會載入本地的 assets/hello.html

assets/demo.xml

<html>
<body>
<a href="http://demo.com/assets/hello.html">hello</a>
</body>
</html>
複製程式碼

assets/hello.html

<html>
<body>
hello world!
</body>
</html>
複製程式碼

過載 shouldInterceptRequest

@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
    return preload("assets/", url);
}

WebResourceResponse preload(String path, String url) { 
    if (!url.contains(path)) {
        return null;
    }
    String local = url.replaceFirst("^http.*" + path, ""); 
    try {
        InputStream is = getApplicationContext().getAssets().open(local);
        String ext = MimeTypeMap.getFileExtensionFromUrl(local);
        String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
        return new WebResourceResponse(mimeType, "UTF-8", is);
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    } 
} 
複製程式碼

7.與Javascript互動

啟用Javascript

// 是否支援Javascript,預設值false
settings.setJavaScriptEnabled(true); 
複製程式碼

注入物件到Javascript

// 注入物件'jsobj',在網頁中通過`jsobj.say(...)`呼叫
web.addJavascriptInterface(new JSObject(), "jsobj") 
複製程式碼

在API17後支援白名單,只有新增了@JavascriptInterface註解的方法才會注入JS

public class JSObject {
    @JavascriptInterface
    public void say(String words) {
      // todo
    }
}
複製程式碼

移除已注入Javascript的物件

web.removeJavascriptInterface("jsobj")
複製程式碼

執行JS表示式

// 彈出提示框
web.loadUrl("javascript:alert('hello')");
// 呼叫注入的jsobj.say方法
web.loadUrl("javascript:jsobj.say('hello')"); 
複製程式碼

在API19後可非同步執行JS表示式,並通過回撥返回值

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    vWeb.evaluateJavascript("111+222", new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String value) {
            // value => "333"
        }
    });
}
複製程式碼

8.地理位置(Geolocation)

developer.mozilla.org/zh-CN/docs/…

需要以下許可權

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
複製程式碼

預設可用

settings.setGeolocationEnabled(true);
複製程式碼

當H5呼叫地理位置API時,會先通過WebChromeClient.onGeolocationPermissionsShowPrompt申請授權

// 指定源的網頁內容在沒有設定許可權狀態下嘗試使用地理位置API。 
@Override
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
    boolean allow = true;   // 是否允許origin使用定位API
    boolean retain = false; // 核心是否記住這次制授權
    callback.invoke(origin, true, false);
}

// 之前呼叫 onGeolocationPermissionsShowPrompt() 申請的授權被取消時,隱藏相關的UI。
@Override
public void onGeolocationPermissionsHidePrompt() {
}
複製程式碼

注:從API24開始,僅支援安全源(https)的請求,非安全源的請求將自動拒絕且不呼叫 onGeolocationPermissionsShowPrompt 與 onGeolocationPermissionsHidePrompt

9.彈框(alert/confirm/prompt/onbeforeunload)

在javascript中使用 alert/confirm/prompt 會彈出對話方塊,可通過過載 WebChromeClient 的下列方法控制彈框的互動,比如替換系統預設的對話方塊或遮蔽這些對話方塊

@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
    // 這裡處理互動邏輯
    // result.cancel(); 表示使用者取消了操作(點選了取消按鈕)
    // result.confirm(); 表示使用者確認了操作(點選了確認按鈕)
    // ...
    // 返回true表示自已處理,返回false表示由系統處理
    return false; 
} 
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
    return false;
} 
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
    return false;
}

@Override
public boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result) {
    return false;
}
複製程式碼

10.全屏(Fullscreen)

Fullscreen API
developer.mozilla.org/zh-CN/docs/…

  • 當H5請求全屏時,會回撥 WebChromeClient.onShowCustomView 方法
  • 當H5退出全屏時,會回撥 WebChromeClient.onHideCustomView 方法

1.manifest

自己處理螢幕尺寸方向的變化(切換螢幕方向時不重建activity) WebView播放視訊需要開啟硬體加速

<activity
    android:name=".WebViewActivity"
    android:configChanges="orientation|screenSize"
    android:hardwareAccelerated="true"
    android:screenOrientation="portrait" />
複製程式碼

2.頁面佈局

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        style="@style/Toolbar.Back"/>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <WebView
            android:id="@+id/web"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>

        ...
    </FrameLayout>

</LinearLayout>
複製程式碼

3.處理全屏回撥

CustomViewCallback mCallback;
View vCustom;

@Override
public void onShowCustomView(View view, CustomViewCallback callback) {
    setFullscreen(true);
    vCustom = view;
    mCallback = callback;
    if (vCustom != null) {
        ViewGroup parent = (ViewGroup) vWeb.getParent();
        parent.addView(vCustom);
    }
}

@Override
public void onHideCustomView() {
    setFullscreen(false);
    if (vCustom != null) {
        ViewGroup parent = (ViewGroup) vWeb.getParent();
        parent.removeView(vCustom);
        vCustom = null;
    }
    if (mCallback != null) {
        mCallback.onCustomViewHidden();
        mCallback = null;
    } 
} 
複製程式碼

4.設定全屏,切換螢幕方向

void setFullscreen(boolean fullscreen) { 
    if (fullscreen) {
        getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
        vToolbar.setVisibility(View.GONE);
        vWeb.setVisibility(View.GONE);
    } else {
        getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN, WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
        vToolbar.setVisibility(View.VISIBLE);
        vWeb.setVisibility(View.VISIBLE);
    }
    if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
    } else {
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
    }
}
複製程式碼

11.記憶體洩漏

直接 new WebView 並傳入 application context 代替在 XML 裡面宣告以防止 activity 引用被濫用,能解決90+%的 WebView 記憶體洩漏。

vWeb =  new WebView(getContext().getApplicationContext());
container.addView(vWeb);
複製程式碼

銷燬 WebView

if (vWeb != null) {
    vWeb.setWebViewClient(null);
    vWeb.setWebChromeClient(null);
    vWeb.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
    vWeb.clearHistory();

    ((ViewGroup) vWeb.getParent()).removeView(vWeb);
    vWeb.destroy();
    vWeb = null;
} 
複製程式碼

12.參考

developer.android.com/reference/a…

Fullscreen API 全屏顯示網頁 calefy.org/2012/06/03/…

WebView實現全屏播放的一種方法 segmentfault.com/a/119000000…

第一方Cookie和第三方Cookie區別 www.biaodianfu.com/first-party…

Android WebView的Js物件注入漏洞解決方案 blog.csdn.net/leehong2005…

Android安全開發之WebView中的地雷 yaq.qq.com/blog/10

Android WebView:效能優化不得不說的事 juejin.im/entry/57d64…

相關文章