本頁將討論建立原生橋接器 (又稱 JavaScript 橋接器) 的各種方法和最佳做法,以利 WebView 中的網頁內容與主機 Android 應用程式之間的通訊。
這項功能可讓網頁開發人員使用 JavaScript 存取原生平台功能,例如攝影機、檔案系統或進階硬體感應器,這些功能通常不屬於標準網頁 API。
用途
JavaScript 橋接器實作項目可支援各種整合情境,讓網路內容更深入存取 Android 作業系統。以下列舉幾個範例:
- 平台整合:從網頁觸發原生 Android UI 元件 (例如生物特徵辨識提示、
BottomSheetDialog)。 - 效能:將大量運算工作卸載至原生 Java 或 Kotlin 程式碼。
- 資料持久性:存取本機加密資料庫或共用偏好設定。
- 大型資料轉移:在應用程式和網頁算繪器之間傳遞媒體檔案或複雜的資料結構。
通訊機制
Android 提供三種主要世代的 API,可建立原生橋接器。 雖然這些方法仍可使用,但安全性、可用性和效能差異很大。
使用 addWebMessageListener (建議)
addWebMessageListener 是最先進且建議採用的方法,可讓網頁內容與原生應用程式程式碼進行通訊。這項服務結合了 JavaScript 介面的易用性與訊息系統的安全性。
運作方式:應用程式會新增具有特定名稱和一組允許來源規則的接聽程式。接著,WebView 會確保 JavaScript 物件從網頁開始載入時,就存在於全域範圍 (window.objectName) 中。
初始化:為確保 WebView 在任何指令碼執行前插入 JavaScript 物件,您必須先呼叫 addWebMessageListener,再呼叫 loadUrl()。
主要功能:
安全與信任:與舊版 API 不同,這個方法在初始化期間需要
allowedOriginRules的Set<String>。這是建立信任感的主要機制。指定信任來源 (例如
https://example.com) 時,WebView 會確保只向從該確切來源載入的網頁公開插入的 JavaScript 物件。原生事件監聽器回呼會在每則訊息中收到
sourceOrigin參數。如果橋接器支援多個允許的來源,您可以使用這項資訊驗證寄件者的確切來源。由於 WebView 會在平台層級嚴格執行這些來源檢查,因此您的應用程式通常可以信任從
sourceOrigin收到的訊息,不必在大多數標準實作中進行嚴格的酬載驗證。- WebView 會根據通訊協定 (HTTP/HTTPS)、主機和通訊埠比對規則。
- WebView 會忽略路徑。例如,
https://example.com允許https://example.com/login和https://example.com/home。 - WebView 會嚴格限制萬用字元只能用於子網域主機的開頭。舉例來說,
https://*.example.com符合https://foo.example.com,但不符合https://example.com。如要比對https://example.com和子網域,必須分別將每個原始規則新增至許可清單 (例如"https://example.com", "https://*.example.com")。您無法使用萬用字元做為架構,或在網域中間使用萬用字元。
這項限制可確保只有經過驗證的網域能使用橋接器,防止未經授權的第三方內容或插入的 iFrame 執行原生程式碼。
支援多個框架:適用於符合來源規則的所有框架。
執行緒:監聽器回呼會在應用程式的主要 (UI) 執行緒上執行。如果橋接器需要處理複雜的資料處理、JSON 剖析或資料庫查詢作業,您必須將這些工作卸載至背景執行緒,以免應用程式 UI 凍結,並發生「應用程式無回應」(ANR) 錯誤。
雙向:網頁傳送訊息時,應用程式會收到
JavaScriptReplyProxy,可用於將訊息傳回該特定影格。您可以保留這個replyProxy物件,隨時用來傳送任意數量的訊息給粉絲專頁,而不只是回覆粉絲專頁傳送的每則訊息。如果原始影格導覽離開或遭到毀損,系統會自動忽略透過 Proxy 傳送的postMessage()訊息。應用程式端啟動:雖然網頁一律必須啟動與應用程式的通訊管道,但原生應用程式可以單方面提示網頁開始這個程序。原生應用程式可透過
addDocumentStartJavaScript()(在網頁載入前評估 JavaScript) 或evaluateJavaScript()(在網頁載入後評估 JavaScript) 與網頁通訊。
限制:這個 API 會以字串或 byte[] 陣列的形式傳送資料。如果是 JSON 物件等較複雜的資料結構,您必須將其序列化為其中一種格式,然後在另一端還原序列化,以重建資料結構。
使用範例:
如要瞭解雙向訊息交換的完整順序,請依下列順序進行事件:
- 啟動 (應用程式):原生應用程式會使用
addWebMessageListener註冊事件監聽器,並透過loadUrl()載入網頁。 - 傳送訊息 (網頁):網頁的 JavaScript 會呼叫
myObject.postMessage(message)來啟動通訊。 - 接收及回覆訊息 (應用程式):應用程式會在接聽程式回呼中接收訊息,並使用提供的
replyProxy.postMessage()回覆。 - 接收回覆 (網頁):網頁會在
myObject.onmessage()回呼函式中收到非同步回覆。
Kotlin
val myListener = WebViewCompat.WebMessageListener { _, _, _, _, replyProxy ->
// Handle the message from JS
replyProxy.postMessage("Acknowledged!")
}
// Check whether the WebView version supports the feature.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
val allowedOrigins = setOf("https://www.example.com")
WebViewCompat.addWebMessageListener(webView, "myObject", allowedOrigins, myListener)
}
Java
WebMessageListener myListener = (view, message, sourceOrigin, isMainFrame, replyProxy) -> {
// Handle the message from JS
replyProxy.postMessage("Acknowledged!");
};
// Check whether the WebView version supports the feature.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
Set<String> allowedOrigins = Set.of("https://www.example.com");
WebViewCompat.addWebMessageListener(webView, "myObject", allowedOrigins, myListener);
}
下列 JavaScript 示範 addWebMessageListener 的用戶端實作方式,可讓網頁內容透過 myObject 代理程式接收來自原生應用程式的訊息,並傳送自己的訊息。
myObject.onmessage = function(event) {
console.log("App says: " + event.data);
};
myObject.postMessage("Hello world!");
使用 postWebMessage (替代方案)
Android 推出這項功能,是為了提供類似網頁 window.postMessage 的非同步訊息傳遞替代方案。
運作方式:應用程式會使用 WebViewCompat.postWebMessage 將酬載傳送至網頁的主要框架。如要建立雙向通訊管道,可以建立 WebMessageChannel,並將其中一個通訊埠連同訊息傳遞至網頁內容。
特徵:
- 非同步:與
addWebMessageListener類似,這個方法會使用非同步訊息傳遞,確保網頁在應用程式於背景處理資料時,仍能回應使用者互動。 - 可辨識來源:您可以指定
targetOrigin,確保 WebView 只會將資料傳送至信任的網站。
限制:
- 範圍:這項 API 會將通訊限制在主要框架內。不支援直接傳送訊息至 iframe 或在 iframe 中傳送訊息。
- URI 限制:您無法對使用
data:URI、file:URI 或loadData()載入的內容使用這個方法,除非您將「*」指定為目標來源。這樣一來,任何網頁都能接收訊息。 - 身分風險:網頁內容無法清楚驗證傳送者的身分。網頁收到的訊息可能來自原生應用程式或其他 iframe。
如果 Android 版本較舊,不支援 addWebMessageListener,且您需要簡單的字串資料非同步管道,請使用這個方法。
使用「addJavascriptInterface」(舊版)
最舊的方法是將原生物件例項直接注入 WebView。
運作方式:定義 Kotlin 或 Java 類別,使用 @JavascriptInterface 註解允許的方法,然後使用 addJavascriptInterface(Object, String) 將類別例項新增至 WebView。
特徵:
- 同步:JavaScript 執行環境會封鎖,直到 Android 程式碼中的方法傳回為止。
- 執行緒安全:系統會在背景執行緒上呼叫方法,因此 Kotlin 或 Java 端必須謹慎同步處理。
- 安全風險:根據預設,WebView 中的每個影格 (包括 iframe) 都能使用
addJavascriptInterface。缺少以來源為準的存取控管機制。由於 WebView 的非同步行為,無法安全地判斷呼叫介面的影格網址。您不得依賴WebView.getUrl()等方法進行安全性驗證,因為這些方法無法保證準確性,且不會指出發出要求的特定影格。
機制摘要
下表快速比較三種主要的原生橋接器實作機制:
| 方法 | addWebMessageListener |
postWebMessage |
addJavascriptInterface |
|---|---|---|---|
| 實作 | 非同步 (主執行緒上的接聽程式) | 非同步 | 同步 |
| 安全性 | 最高 (以許可清單為準) | 高 (來源感知) | 低 (不檢查來源) |
| 複雜度 | 中 | 中 | 簡潔 |
| 方向 | 雙向 | 雙向 | 網站到應用程式 |
| 最低 WebView 版本 | 82 版 (和 Jetpack Webkit 1.3.0) | 版本 45 (和 Jetpack Webkit 1.1.0) | 所有版本 |
| 推薦項目 | 是 | 否 | 否 |
處理大型資料移轉作業
傳輸大型酬載 (例如數 MB 的字串或二進位檔案) 時,您必須謹慎管理記憶體,以免 32 位元裝置發生應用程式無回應 (ANR) 錯誤或當機。本節將討論在主機應用程式和網頁內容之間傳輸大量資料時,相關的各種技術和限制。
使用位元組陣列傳輸二進位資料
使用 WebMessageCompat 類別時,您可以直接傳送 byte[] 陣列,不必將二進位資料序列化為 Base64 字串。由於 Base64 會為資料大小增加約 33% 的額外負荷,因此這種做法的記憶體效率更高,速度也更快。
- 二進位優勢:在原生應用程式和網頁內容之間傳輸二進位資料,例如圖片檔案或音訊。
- 限制:即使使用位元組陣列,系統也會在應用程式與 WebView 用於算繪網頁內容的隔離程序之間,跨程序間通訊 (IPC) 邊界複製資料。但如果檔案非常大,仍會耗用大量記憶體。
下列程式碼範例說明如何在原生應用程式端設定 addWebMessageListener,接收標示 WebMessageCompat.TYPE_ARRAY_BUFFER 的訊息,並視需要檢查 WebViewFeature.MESSAGE_ARRAY_BUFFER,以二進位資料回覆。
Kotlin
fun setupWebView(webView: WebView) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
val listener = WebViewCompat.WebMessageListener { view, message, sourceOrigin, isMainFrame, replyProxy ->
// Check if the received message is an ArrayBuffer
if (message.type == WebMessageCompat.TYPE_ARRAY_BUFFER) {
val binaryData: ByteArray = message.arrayBuffer
// Process your binary data (image, audio, etc.)
println("Received bytes: ${binaryData.size}")
// Optional: Send a binary reply back to JavaScript.
// This example sends a 3-byte array for simplicity.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER)) {
val replyBytes = byteArrayOf(0x01, 0x02, 0x03)
replyProxy.postMessage(replyBytes)
}
}
}
// "myBridge" matches the window.myBridge in JavaScript
WebViewCompat.addWebMessageListener(
webView,
"myBridge",
setOf("https://example.com"), // Security: restrict origins
listener
)
}
}
Java
public void setupWebView(WebView webView) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
WebViewCompat.WebMessageListener listener = (view, message, sourceOrigin, isMainFrame, replyProxy) -> {
// Check if the received message is an ArrayBuffer
if (message.getType() == WebMessageCompat.TYPE_ARRAY_BUFFER) {
byte[] binaryData = message.getArrayBuffer();
// Process your binary data (image, audio, etc.)
System.out.println("Received bytes: " + binaryData.length);
// Optional: Send a binary reply back to JavaScript.
// This example sends a 3-byte array for simplicity.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER)) {
byte[] replyBytes = new byte[]{0x01, 0x02, 0x03};
replyProxy.postMessage(replyBytes);
}
}
};
// "myBridge" matches the window.myBridge in JavaScript
WebViewCompat.addWebMessageListener(
webView,
"myBridge",
Set.of("https://example.com"), // Security: restrict origins
listener
);
}
}
下列 JavaScript 程式碼示範了 addWebMessageListener 的用戶端實作方式,可讓網頁內容使用上一個範例中插入的 window.myBridge 代理程式,與原生應用程式來回傳送及接收二進位資料 (ArrayBuffer)。
// Function to send an image or binary buffer to the app
async function sendBinaryToApp() {
const response = await fetch('image.jpg');
const buffer = await response.arrayBuffer();
// Check if the injected bridge object exists
if (window.myBridge) {
// You can send the ArrayBuffer directly
window.myBridge.postMessage(buffer);
}
}
// Receiving binary data from the app
if (window.myBridge) {
window.myBridge.onmessage = function(event) {
if (event.data instanceof ArrayBuffer) {
console.log('Received binary data from App, length:', event.data.byteLength);
// Process the binary data (for example, as a Uint8Array)
const bytes = new Uint8Array(event.data);
console.log('First byte:', bytes[0]);
}
};
}
有效率地載入大量資料
如果是非常大的檔案 (超過 10 MB),請使用 shouldInterceptRequest 方法串流資料:
- 網頁會對自訂預留位置網址發出
fetch()呼叫。例如:https://app.local/large-file。 - Android 應用程式會在
WebViewClient.shouldInterceptRequest 中攔截這項要求。 - 應用程式會以
InputStream形式傳回資料。
這樣一來,您就能以區塊形式串流資料,不必一次將整個酬載載入記憶體。
下列 JavaScript 函式示範用戶端程式碼,可使用標準 fetch() 呼叫自訂預留位置網址,從原生應用程式有效率地載入大型二進位檔案。
async function fetchBinaryFromApp() {
try {
// This URL doesn't need to exist on the internet
const response = await fetch('https://app.local/data/large-file.bin');
if (!response.ok) throw new Error('Network response was not okay');
// For raw binary data:
const arrayBuffer = await response.arrayBuffer();
console.log('Received binary data, size:', arrayBuffer.byteLength);
// Process buffer (for example, new Uint8Array(arrayBuffer))
/*
// OR for an image:
const blob = await response.blob();
const imageUrl = URL.createObjectURL(blob);
document.getElementById('myImage').src = imageUrl;
*/
} catch (error) {
console.error('Fetch error:', error);
}
}
下列程式碼範例示範原生應用程式端,如何使用 Kotlin 和 Java 中的 WebViewClient.shouldInterceptRequest 方法,攔截網頁內容要求的自訂預留位置網址,藉此串流大型二進位檔案。
Kotlin
webView.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest?
): WebResourceResponse? {
val url = request?.url ?: return null
// Check if this is our custom placeholder URL
if (url.host == "app.local" && url.path == "/data/large-file.bin") {
try {
// 1. Get your data as an InputStream
// (from Assets, Files, or a generated byte stream)
val inputStream: InputStream = context.assets.open("my_data.pb")
// 2. Define Response Headers (Crucial for CORS/Fetch)
val headers = mutableMapOf<String, String>()
headers["Access-Control-Allow-Origin"] = "*" // Allow fetch from any origin
// 3. Return the response
return WebResourceResponse(
"application/octet-stream", // MIME type (for example, image/jpeg)
"UTF-8", // Encoding
200, // Status Code
"OK", // Reason Phrase
headers, // Custom Headers
inputStream // The actual data stream
)
} catch (e: Exception) {
// Handle exception
}
}
return super.shouldInterceptRequest(view, request)
}
}
Java
webView.setWebViewClient(new WebViewClient() {
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
String urlPath = request.getUrl().getPath();
String host = request.getUrl().getHost();
// Check if this is our custom placeholder URL
if ("app.local".equals(host) && "/data/large-file.bin".equals(urlPath)) {
try {
// 1. Get your data as an InputStream
// (from Assets, Files, or a generated byte stream)
InputStream inputStream = getContext().getAssets().open("my_data.pb");
// 2. Define Response Headers (Crucial for CORS/Fetch)
Map<String, String> headers = new HashMap<>();
headers.put("Access-Control-Allow-Origin", "*"); // Allow fetch from any origin
// 3. Return the response
return new WebResourceResponse(
"application/octet-stream", // MIME type (for example, image/jpeg)
"UTF-8", // Encoding
200, // Status Code
"OK", // Reason Phrase
headers, // Custom Headers
inputStream // The actual data stream
);
} catch (Exception e) {
// Handle exception
}
}
return super.shouldInterceptRequest(view, request);
}
});
依循安全性建議
為保護應用程式和使用者資料,實作橋接器時請遵循下列規範:
強制執行 HTTPS:為確保惡意第三方內容無法叫用應用程式的原生邏輯,請僅允許與安全來源通訊。
依據來源規則:處理信任問題的最佳方式,就是嚴格定義
allowedOriginRules,並檢查訊息回呼中提供的sourceOrigin。除非絕對必要,否則請避免使用完整萬用字元 (*) 做為唯一的來源規則,因為這會比對所有來源。使用子網域的萬用字元 (例如*.example.com) 仍有效且安全,可比對多個子網域 (例如foo.example.com、bar.example.com)。注意:來源規則可防範惡意第三方網站和隱藏的 iframe,但無法防範您信任網域內的跨網站指令碼 (XSS) 安全漏洞。舉例來說,如果網頁顯示使用者自製內容,且容易遭受儲存型 XSS 攻擊,攻擊者就能執行指令碼,假冒您信任的來源。執行敏感的原生平台作業前,請考慮對訊息酬載套用驗證。
盡量減少公開的內容:只公開網頁所需的方法或資料。
在執行階段檢查功能:包括
addWebMessageListener在內,最近的 Bridge API 屬於 Jetpack Webkit 程式庫。因此,請務必先使用WebViewFeature.isFeatureSupported()檢查是否支援,再撥打電話。