文章目錄
- 摘要
- WebView基礎
- 一、啟動調整模式
- 二、WebChromeClient
- 三、WebViewClient
- 四、WebSettings
- 五、WebView和Native交互
- Androidx-WebKit
- 一、啟動安全瀏覽服務
- 二、設置代理
- 三、安全的 WebView 和 Native 通信支持
- 四、文件傳遞
- 五、深色主題的支持
- 六、JavaScript and WebAssembly執行引擎支持
摘要
文章主要分2部分:
- Webview的基礎相關知識:如調試、基礎API、和原生交互等
- Androidx-WebKit 的使用: 如和原生的消息交互、文件傳遞、以及啟動安全瀏覽等
WebView基礎
一、啟動調整模式
在開發過程中,我們可以啟用 WebView 的調試模式,以便在 Chrome DevTools 中查看 WebView 的內容、網絡請求等信息。
WebView.setWebContentsDebuggingEnabled(true)
調試界面 chrome://inspect/#devices
二、WebChromeClient
WebChromeClient 是一個抽象基類,它的實例可以被傳遞給 WebView.setWebChromeClient() 方法,以處理與 JavaScript 交互和網頁元素相關的事件。
2.1 WebChromeClient一進度相關
onProgressChanged(WebView view, int newProgress)
: 當頁面加載進度改變時調用。newProgress參數表示當前頁面加載的百分比。
2.2 WebChromeClient-標題、圖標相關
onReceivedTitle(WebView view, String title)
: 當前頁面的標題已經被接收到時調用。title參數是新的標題。onReceivedIcon(WebView view, Bitmap icon)
: 當前頁面的圖標已經被接收到時調用。icon參數是新的圖標。onReceivedTouchIconUrl(WebView view, String url, boolean precomposed)
: 當網頁的觸摸圖標URL被接收到時調用。url參數是圖標的URL,precomposed參數表示圖標是否已經被合成。
2.3 WebChromeClient-權限相關
onPermissionRequest(PermissionRequest request)
: 當網頁請求一個權限時調用,例如攝像頭、麥克風等。你可以在這個方法中處理權限請求。onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback)
: 當網頁請求獲取地理位置權限時調用。origin參數是請求權限的網頁的源,callback參數是用于設置權限的回調。
override fun onPermissionRequest(request: PermissionRequest?) {"onPermissionRequest".logD(WEB_TAG)try {request?.let {kotlin.runCatching {if (isVideo(request.resources)) {startRequestPermissions(permissions = arrayOf(permission.CAMERA, permission.RECORD_AUDIO)) {if (it.filter { !it.value }.isEmpty()) {request.grant(request.resources)request.origin}}} else if (isOnlyAudio(request.resources)) {startRequestPermission(permission = permission.RECORD_AUDIO) {if (it) {request.grant(request.resources)request.origin}}}}}}catch (e :Exception){e.message.logE(WEB_TAG)}}private fun isVideo(resources: Array<String>): Boolean {val strings = listOf(*resources)return strings.contains(PermissionRequest.RESOURCE_VIDEO_CAPTURE)}private fun isOnlyAudio(resources: Array<String>): Boolean {val strings = listOf(*resources)return !strings.contains(PermissionRequest.RESOURCE_VIDEO_CAPTURE) && strings.contains(PermissionRequest.RESOURCE_AUDIO_CAPTURE)}}
2.4 WebChromeClient-文件處理
onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams)
: 當網頁需要用戶選擇文件時調用,例如HTML的元素被點擊時。你可以在這個方法中打開一個文件選擇器,并將用戶選擇的文件的URI通過filePathCallback返回。
2.5 WebChromeClient-彈窗、JS相關
onJsAlert(WebView view, String url, String message, JsResult result)
: 當JavaScript的alert()函數被調用時調用。message參數是alert()函數的參數。onJsConfirm(WebView view, String url, String message, JsResult result)
: 當JavaScript的confirm()函數被調用時調用。message參數是confirm()函數的參數。onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result)
: 當JavaScript的prompt()函數被調用時調用。message參數是prompt()函數的第一個參數,defaultValue參數是prompt()函數的第二個參數。onConsoleMessage(ConsoleMessage consoleMessage)
: 當JavaScript的console.log()函數被調用時調用。consoleMessage參數包含了日志消息的詳細信息。onJsBeforeUnload(WebView view, String url, String message, JsResult result)
: 當JavaScript的beforeunload事件被觸發時調用。message參數是beforeunload事件的返回值。onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg)
: 當JavaScript的window.open()函數被調用時調用。你可以在這個方法中創建一個新的WebView,并將它通過resultMsg返回。onCloseWindow(WebView window)
: 當JavaScript的window.close()函數被調用時調用。你可以在這個方法中關閉之前通過onCreateWindow創建的WebView。
2.6 WebChromeClient-視頻相關
onShowCustomView(View view, CustomViewCallback callback)
: 當網頁進入全屏模式時調用。在這種情況下,網頁內容將不再在WebView中渲染,而是在傳入的view中渲染。你應該將這個View添加到一個配置了WindowManager.LayoutParams.FLAG_FULLSCREEN標志的Window中,以實際全屏顯示這個網頁內容。onHideCustomView()
: 當網頁退出全屏模式時調用。你應該隱藏自定義的View(之前傳給onShowCustomView(View view, CustomViewCallback callback)的View)。在這個方法調用后,網頁內容將再次在原來的WebView中渲染。getDefaultVideoPoster()
: 當視頻元素不在播放狀態時,它們由一個’poster’圖像表示。可以通過HTML中的video標簽的poster屬性指定要使用的圖像。如果該屬性不存在,則使用默認的poster。這個方法允許ChromeClient提供默認的poster圖像。getVideoLoadingProgressView()
: 獲取在全屏視頻緩沖期間顯示的View。主應用程序可以覆蓋此方法以提供包含旋轉器或類似物的View。
三、WebViewClient
WebViewClient 是一個抽象基類,它的實例可以被傳遞給 WebView.setWebViewClient() 方法,以處理與網頁加載和渲染相關的事件。
3.1 WebViewClient-重定向
shouldOverrideUrlLoading(WebView view, String url)
: 當 WebView 即將加載一個 URL 時調用。你可以在這個方法中決定是否要覆蓋這個 URL 的加載,如果你想覆蓋這個 URL 的加載,那么你應該返回 true,并在這個方法中進行你自己的處理,例如打開一個新的 Activity 來加載這個 URL。
3.2 WebViewClient-頁面加載
onPageStarted(WebView view, String url, Bitmap favicon)
: 當網頁開始加載時調用。url 參數是正在加載的網頁的 URL。onPageFinished(WebView view, String url)
: 當網頁加載完成時調用。url 參數是剛剛加載完成的網頁的 URL。onLoadResource(WebView view, String url)
: 當 WebView 正在加載一個資源(例如圖片或者 JavaScript 文件)時調用。url 參數是正在加載的資源的 URL。
3.3 WebViewClient-認證請求相關
onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm)
: 當 WebView 需要進行 HTTP 認證時調用。你可以在這個方法中處理認證請求,例如顯示一個輸入用戶名和密碼的對話框。onReceivedLoginRequest(WebView view, String realm, @Nullable String account, String args)
: 當 WebView 需要進行自動登錄時調用。你可以在這個方法中處理登錄請求,例如從存儲的賬戶信息中獲取用戶名和密碼。
3.4 WebViewClient-其他
onReceivedError(WebView view, int errorCode, String description, String failingUrl)
: 當 WebView 加載網頁時發生錯誤時調用。errorCode 參數是錯誤碼,description 參數是錯誤描述,failingUrl 參數是發生錯誤的網頁的 URL。onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse)
: 當 WebView 接收到 HTTP 錯誤時調用。request 參數是發生錯誤的請求,errorResponse 參數是服務器的響應。onReceivedSslError(WebView view, SslErrorHandler handler, SslError error)
: 當 WebView 加載的網頁有 SSL 錯誤時調用。你可以在這個方法中處理 SSL 錯誤,例如顯示一個對話框讓用戶決定是否要繼續加載。onSafeBrowsingHit(WebView view, WebResourceRequest request, int threatType, SafeBrowsingResponse callback)
: 當 WebView 訪問一個被 Safe Browsing 判斷為可能是惡意的網站時被調用。你可以在這個方法中處理這個事件,例如顯示一個警告對話框,或者導航到一個安全的網頁。callback 參數是一個 SafeBrowsingResponse,你可以調用它的 showInterstitial(boolean) 或 proceed(boolean) 方法來決定是否顯示一個安全警告的插頁廣告,或者繼續加載這個網頁。onFormResubmission(WebView view, Message dontResend, Message resend)
: 當 WebView 嘗試重新提交一個表單,并且需要用戶確認是否重新提交時調用。你可以在這個方法中處理這個事件,例如顯示一個確認對話框。onScaleChanged(WebView view, float oldScale, float newScale)
: 當 WebView 的縮放級別改變時調用。你可以在這個方法中處理縮放級別的改變,例如更新一個縮放級別的顯示。onUnhandledKeyEvent(WebView view, KeyEvent event)
: 當 WebView 收到一個未處理的按鍵事件時調用。你可以在這個方法中處理未處理的按鍵事件,例如實現自定義的按鍵處理。doUpdateVisitedHistory(WebView view, String url, boolean isReload)
: 一個頁面的訪問歷史記錄被更新時,這個方法會被調用。
四、WebSettings
WebSettings
是一個類,它用于管理 WebView 的各種設置。你可以通過 WebView.getSettings()
方法獲取到一個 WebSettings
實例,然后通過這個實例來配置 WebView 的設置。
以下是一些常用的 WebSettings
方法:
setJavaScriptEnabled(boolean enabled)
: 設置 WebView 是否支持 JavaScript。默認值為false
。setSupportZoom(boolean support)
: 設置 WebView 是否支持縮放。默認值為false
。setDisplayZoomControls(boolean enabled)
: 設置 WebView 是否顯示縮放控件。默認值為true
。setBuiltInZoomControls(boolean enabled)
: 設置 WebView 是否使用內置的縮放機制。默認值為false
。setLoadWithOverviewMode(boolean overview)
: 設置 WebView 是否應該啟用概覽模式,即總是縮放內容以適應屏幕寬度。默認值為false
。setUseWideViewPort(boolean use)
: 設置 WebView 是否應該啟用寬視圖端口。默認值為false
。setJavaScriptCanOpenWindowsAutomatically(boolean allow)
: 設置 WebView 的 JavaScript 是否可以自動打開窗口。默認值為false
。setMediaPlaybackRequiresUserGesture(boolean require)
: 設置為 false,那么 WebView 中的音頻和視頻將會自動播放,不需要用戶交互。setLoadsImagesAutomatically(boolean flag)
: 用于設置 WebView 是否自動加載圖片。 如果設置為 true,WebView 會自動加載網頁中的圖片。如果設置為 false,所有的圖片都不會被加載,只有當 LOAD_CACHE_ELSE_NETWORK 或 LOAD_NO_CACHE 被使用時,才會加載。
WebSettings
提供了一些方法來管理 WebView 的緩存:
setCacheMode(int mode)
: 設置 WebView 的緩存模式。可選的值有:LOAD_DEFAULT
: 默認的緩存模式。如果沒有Cache-Control
或Expires
頭,緩存會被存儲,當資源過期時,WebView 會嘗試從網絡加載。如果沒有網絡,WebView 會從緩存加載。LOAD_CACHE_ELSE_NETWORK
: 只要緩存存在,即使過期也會從緩存加載。如果緩存不存在,WebView 會從網絡加載。LOAD_NO_CACHE
: 不使用緩存,WebView 會從網絡加載。LOAD_CACHE_ONLY
: 不從網絡加載,只從緩存加載。
setAppCacheEnabled(boolean enabled)
: 設置 WebView 是否啟用應用緩存。默認值為false
。注意,你還需要通過setAppCachePath
方法設置一個應用緩存的路徑。setAppCachePath(String appCachePath)
: 設置應用緩存的路徑。這個路徑必須是可以讓應用讀寫的。setAppCacheMaxSize(long appCacheMaxSize)
: 設置應用緩存的最大大小。setDatabaseEnabled(boolean enabled)
: 設置 WebView 是否啟用數據庫存儲 API。默認值為false
。setDomStorageEnabled(boolean enabled)
: 設置 WebView 是否啟用 DOM 存儲 API。默認值為false
,true
可以使用 sessionStorage 和 localStorage 對象來存儲和檢索數據。。
//設置Cookiefun setCookie(map: MutableMap<String, String>) {val cookieManager: CookieManager = CookieManager.getInstance()cookieManager.setAcceptCookie(true)map.onEach { entry ->cookieManager.setCookie(entry.key, entry.value)}cookieManager.flush()}/*** 給設置localStorage 設置數據*/fun setLocalStorage(itmes: Map<String, String>) {val jsonBuf = StringBuilder()for (key in itmes.keys) {if (isNotEmpty(itmes[key])) {jsonBuf.append("localStorage.setItem('key', '").append(itmes[key]).append("');")}}val info = jsonBuf.toString()if (isNotEmpty(info)) {webView.evaluateJavascript(info, null)}}
五、WebView和Native交互
WebView 和 Native 交互主要有兩種方式:JavaScriptInterface 和 WebView.evaluateJavascript。
- JavaScriptInterface:這是一種將 Java 對象映射到 JavaScript 的方式。你可以創建一個 Java 對象,這個對象的公共方法可以在 JavaScript 中被調用。例如:
class JavaScriptInterface(private val context: Context) {@JavascriptInterfacefun showToast(message: String) {Toast.makeText(context, message, Toast.LENGTH_SHORT).show()}
}val webView: WebView = findViewById(R.id.webview)
webView.addJavascriptInterface(JavaScriptInterface(this), "Android")
在上述代碼中,我們創建了一個 JavaScriptInterface
類,并將其實例添加到了 WebView 中。
在 JavaScript 中:
Android.showToast(message);
WebView.evaluateJavascript:這是一種在 WebView 中執行 JavaScript 代碼的方式。你可以使用這個方法來調用 JavaScript 函數,并獲取返回值。例如:
webView.evaluateJavascript("document.title") { title ->Log.d("WebView", "Document title: $title")
}
Androidx-WebKit
dependencies {implementation("androidx.webkit:webkit:1.9.0")
}
這里下面就是WebKit對于的 WebView 的增強方法
一、啟動安全瀏覽服務
/*** Start safe browsing* 用于啟動安全瀏覽服務。這個服務可以幫助 WebView 防止用戶訪問被認為是惡意的網站*/fun startSafeBrowsing(){if (WebViewFeature.isFeatureSupported(WebViewFeature.START_SAFE_BROWSING)) {WebViewCompat.startSafeBrowsing(BaseKit.app) {("WebView.startSafeBrowsing isSuccess = $it").logI()}}}
二、設置代理
if (WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) {ProxyConfig proxyConfig = new ProxyConfig.Builder().addProxyRule("localhost:7890") //添加要用于所有 URL 的代理.addProxyRule("localhost:1080") //優先級低于第一個代理,僅在上一個失敗時應用.addDirect() //當前面的代理失敗時,不使用代理直連.addBypassRule("www.baidu.com") //該網址不使用代理,直連服務.addBypassRule("*.cn") //以.cn結尾的網址不使用代理.build();Executor executor = ...Runnable listener = ...ProxyController.getInstance().setProxyOverride(proxyConfig, executor, listener);
}
三、安全的 WebView 和 Native 通信支持
// Appval myListener = object : WebViewCompat.WebMessageListener {/*** On post message** @param view WebView* @param message js代碼發送的消息* @param sourceOrigin 發送消息的網頁地址* @param isMainFrame 是否是主頁面,iFrame中的頁面為false* @param replyProxy 回復消息的代理*/override fun onPostMessage(view: WebView, message: WebMessageCompat, sourceOrigin: Uri, isMainFrame: Boolean, replyProxy: JavaScriptReplyProxy) {// do something about view, message, sourceOrigin and isMainFrame.}}val allowedOriginRules = allowedRules ?: setOf()WebViewCompat.addWebMessageListener(/* webView = */ webView,/* jsObjectName = */jsObjectName,/* allowedOriginRules = */ allowedOriginRules,/* listener = */myListener)
// Web page (in JavaScript)
myObject.onmessage = function(event) {// prints "Got it!" when we receive the app's response.console.log(event.data);
}
myObject.postMessage("I'm ready!");
四、文件傳遞
4.1 Native 傳遞文件給 WebView:
// App (in Java)
WebMessageListener myListener = new WebMessageListener() {@Overridepublic void onPostMessage(WebView view, WebMessageCompat message, Uri sourceOrigin,boolean isMainFrame, JavaScriptReplyProxy replyProxy) {// Communication is setup, send file data to web.if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER)) {// Suppose readFileData method is to read content from file.byte[] fileData = readFileData("myFile.dat");replyProxy.postMessage(fileData);}}
}
// Web page (in JavaScript)
myObject.onmessage = function(event) {if (event.data instanceof ArrayBuffer) {const data = event.data; // Received file content from app.const dataView = new DataView(data);// Consume file content by using JavaScript DataView to access ArrayBuffer.}
}
myObject.postMessage("Setup!");
4.2 WebView 傳遞文件給 Native:
// Web page (in JavaScript)
const response = await fetch('example.jpg');
if (response.ok) {const imageData = await response.arrayBuffer();myObject.postMessage(imageData);
}
// App (in Java)
WebMessageListener myListener = new WebMessageListener() {@Overridepublic void onPostMessage(WebView view, WebMessageCompat message, Uri sourceOrigin,boolean isMainFrame, JavaScriptReplyProxy replyProxy) {if (message.getType() == WebMessageCompat.TYPE_ARRAY_BUFFER) {byte[] imageData = message.getArrayBuffer();// do something like draw image on ImageView.}}
};
五、深色主題的支持
簡單來說如果您想讓 WebView 的內容和應用的主題相匹配,您應該始終定義深色主題并實現 prefers-color-scheme,而對于未定義 prefers-color-scheme 的頁面,系統按照不同的策略選擇算法生成或者顯示默認頁面。
六、JavaScript and WebAssembly執行引擎支持
JavascriptEngine 直接使用了 WebView 的 V8 實現,由于不用分配其他 WebView 資源所以資源消耗更低,并可以開啟多個獨立運行的沙箱環境,還針對傳遞大量數據做了優化。
if(!JavaScriptSandbox.isSupported()){
return;
}
//連接到引擎
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =JavaScriptSandbox.createConnectedInstanceAsync(context);
//創建上下文 上下文間有簡單的數據隔離
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();
//執行函數 && 獲取結果
final String code = "function sum(a, b) { let r = a + b; return r.toString(); }; sum(3, 4)";
ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
String result = resultFuture.get(5, TimeUnit.SECONDS);
Futures.addCallback(resultFuture,new FutureCallback<String>() {@Overridepublic void onSuccess(String result) {text.append(result);}@Overridepublic void onFailure(Throwable t) {text.append(t.getMessage());}},mainThreadExecutor); //Wasm運行
final byte[] hello_world_wasm = {0x00 ,0x61 ,0x73 ,0x6d ,0x01 ,0x00 ,0x00 ,0x00 ,0x01 ,0x0a ,0x02 ,0x60 ,0x02 ,0x7f ,0x7f ,0x01,0x7f ,0x60 ,0x00 ,0x00 ,0x03 ,0x03 ,0x02 ,0x00 ,0x01 ,0x04 ,0x04 ,0x01 ,0x70 ,0x00 ,0x01 ,0x05,0x03 ,0x01 ,0x00 ,0x00 ,0x06 ,0x06 ,0x01 ,0x7f ,0x00 ,0x41 ,0x08 ,0x0b ,0x07 ,0x18 ,0x03 ,0x06,0x6d ,0x65 ,0x6d ,0x6f ,0x72 ,0x79 ,0x02 ,0x00 ,0x05 ,0x74 ,0x61 ,0x62 ,0x6c ,0x65 ,0x01 ,0x00,0x03 ,0x61 ,0x64 ,0x64 ,0x00 ,0x00 ,0x09 ,0x07 ,0x01 ,0x00 ,0x41 ,0x00 ,0x0b ,0x01 ,0x01 ,0x0a,0x0c ,0x02 ,0x07 ,0x00 ,0x20 ,0x00 ,0x20 ,0x01 ,0x6a ,0x0b ,0x02 ,0x00 ,0x0b,
};
final String jsCode = "android.consumeNamedDataAsArrayBuffer('wasm-1').then(" +"(value) => { return WebAssembly.compile(value).then(" +"(module) => { return new WebAssembly.Instance(module).exports.add(20, 22).toString(); }" +")})";
boolean success = js.provideNamedData("wasm-1", hello_world_wasm);
if (success) {FluentFuture.from(js.evaluateJavaScriptAsync(jsCode)).transform(this::println, mainThreadExecutor).catching(Throwable.class, e -> println(e.getMessage()), mainThreadExecutor);
} else {// the data chunk name has been used before, use a different name
}