На этой странице рассматриваются различные методы и лучшие практики для создания нативного моста, также известного как мост JavaScript, для обеспечения связи между веб-контентом в WebView и хост-приложением Android.
Это позволяет веб-разработчикам использовать JavaScript для доступа к нативным функциям платформы — таким как камера, файловая система или продвинутые аппаратные датчики — которые обычно не предоставляются стандартными веб-API.
Варианты использования
Реализация моста на JavaScript позволяет реализовать различные сценарии интеграции, в которых веб-контент требует более глубокого доступа к операционной системе Android. Ниже приведены некоторые примеры:
- Интеграция с платформой : запуск нативных компонентов пользовательского интерфейса Android (например, подсказок биометрической идентификации,
BottomSheetDialog) с веб-страницы. - Производительность : Перенос ресурсоемких вычислительных задач на собственный код Java или Kotlin.
- Сохранение данных : доступ к локальным зашифрованным базам данных или общим настройкам.
- Передача больших объемов данных : передача медиафайлов или сложных структур данных между приложением и веб-рендером.
Механизмы коммуникации
Android предлагает три основных поколения API для создания нативного моста. Хотя все они по-прежнему доступны, они значительно различаются по безопасности, удобству использования и производительности.
Используйте addWebMessageListener (рекомендуется).
addWebMessageListener — это наиболее современный и рекомендуемый подход к обмену данными между веб-контентом и кодом нативного приложения. Он сочетает в себе простоту использования интерфейса JavaScript с безопасностью системы обмена сообщениями.
Как это работает : приложение добавляет слушатель с определенным именем и набором правил разрешенных источников. Затем WebView гарантирует, что объект JavaScript присутствует в глобальной области видимости ( window.objectName ) с момента начала загрузки страницы.
Инициализация : Чтобы гарантировать внедрение объекта JavaScript в WebView до выполнения каких-либо скриптов, необходимо вызвать addWebMessageListener перед вызовом loadUrl() .
Основные характеристики :
Безопасность и доверие : В отличие от устаревших API, этот метод требует указания набора правил
allowedOriginRulesSet<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.
Поддержка нескольких кадров : работает со всеми кадрами, соответствующими исходным правилам.
Многопоточность : функция обратного вызова слушателя выполняется в основном потоке приложения (пользовательском интерфейсе). Если вашему мосту необходимо обрабатывать сложные данные, анализировать JSON или выполнять поиск в базе данных, необходимо перенести эту работу в фоновый поток, чтобы предотвратить зависание пользовательского интерфейса приложения с ошибкой «приложение не отвечает» (ANR).
Двунаправленность : Когда веб-страница отправляет сообщение, приложение получает объект
JavaScriptReplyProxy, который оно может использовать для отправки сообщений обратно в этот конкретный фрейм. Вы можете сохранить этот объектreplyProxyи использовать его в любое время для отправки любого количества сообщений на страницу, а не только для ответа на каждое отдельное сообщение, отправленное страницей. Если исходный фрейм переходит на другую страницу или уничтожается, сообщения, отправленные с помощьюpostMessage()в прокси, молча игнорируются.Инициализация на стороне приложения : Хотя веб-страница всегда должна инициировать канал связи с приложением, нативное приложение может в одностороннем порядке запросить у веб-страницы начало этого процесса. Нативная программа может взаимодействовать с веб-страницей с помощью
addDocumentStartJavaScript()(для выполнения JavaScript до загрузки страницы) илиevaluateJavaScript()(для выполнения JavaScript после загрузки страницы).
Ограничение : Этот API отправляет данные либо в виде строк, либо в виде массивов byte[] . Для более сложных структур данных, таких как объекты JSON, необходимо сериализовать их в один из этих форматов, а затем десериализовать на другой стороне для восстановления структуры данных.
Пример использования :
Для понимания полной последовательности двустороннего обмена сообщениями события происходят в следующем порядке:
- Инициализация (приложения) : Нативная программа регистрирует слушатель с помощью
addWebMessageListenerи загружает веб-страницу с помощьюloadUrl(). - Отправка сообщения (веб) : JavaScript веб-страницы вызывает метод
myObject.postMessage(message)для инициирования обмена данными. - Получение и отправка сообщения (приложение) : Приложение получает сообщение в обработчике обратного вызова и отправляет ответ, используя предоставленный метод
replyProxy.postMessage(). - Получение ответа (веб) : Веб-страница получает асинхронный ответ в функции обратного вызова
myObject.onmessage().
Котлин
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.
- Ограничения по URI : Этот метод нельзя использовать для контента, загружаемого с помощью URI
data:URIfile:илиloadData(), если вы не укажете "*" в качестве целевого источника. В этом случае сообщение может быть получено любой страницей. - Риск потери личности : отсутствует четкий способ проверки личности отправителя веб-контентом. Сообщение, полученное веб-страницей, могло быть отправлено из вашего нативного приложения или другого iframe.
Этот метод следует использовать, если вам нужен простой асинхронный канал для передачи строковых данных в более ранних версиях Android, которые не поддерживают addWebMessageListener .
Используйте addJavascriptInterface (устаревшая версия)
Самый старый метод предполагает внедрение экземпляра нативного объекта непосредственно в WebView.
Как это работает : вы определяете класс Kotlin или Java, аннотируете разрешенные методы с помощью @JavascriptInterface и добавляете экземпляр класса в WebView, используя addJavascriptInterface(Object, String) .
Характеристики :
- Синхронный режим : среда выполнения JavaScript блокируется до тех пор, пока метод в вашем коде Android не вернет управление.
- Потокобезопасность : система вызывает методы в фоновом потоке, что требует тщательной синхронизации на стороне Kotlin или Java.
- Риск безопасности : По умолчанию
addJavascriptInterfaceдоступен для каждого фрейма внутри WebView, включая iframe. Он не имеет контроля доступа на основе источника. Из-за асинхронного поведения WebView невозможно безопасно определить URL-адрес фрейма, вызывающего ваш интерфейс. Не следует полагаться на такие методы, какWebView.getUrl(), для проверки безопасности, поскольку они не гарантируют точность и не указывают, какой именно фрейм отправил запрос.
Краткое описание механизмов
В следующей таблице представлено краткое сравнение трех основных механизмов реализации нативных мостов:
| Метод | addWebMessageListener | postWebMessage | addJavascriptInterface |
|---|---|---|---|
| Выполнение | Асинхронный режим (слушатель в основном потоке) | Асинхронный | Синхронный |
| Безопасность | Наивысший (на основе списка разрешенных) | Высокий (с учетом происхождения) | Низкий уровень (без проверки происхождения) |
| Сложность | Умеренный | Умеренный | Простой |
| Направление | Двунаправленный | Двунаправленный | Веб-приложение |
| Минимальная версия WebView | Версия 82 (и Jetpack Webkit 1.3.0) | Версия 45 (и Jetpack Webkit 1.1.0) | Все версии |
| Рекомендуется | Да | Нет | Нет |
Обработка больших объемов данных
При передаче больших объемов данных, таких как многомегабайтные строки или бинарные файлы, необходимо тщательно управлять памятью, чтобы избежать ошибок «Приложение не отвечает» (ANR) или сбоев на 32-битных устройствах. В этом разделе рассматриваются различные методы и ограничения, связанные с передачей значительных объемов данных между хост-приложением и веб-контентом.
Передача двоичных данных с помощью массивов байтов.
Класс WebMessageCompat позволяет отправлять массивы byte[] напрямую, вместо сериализации двоичных данных в строки Base64. Поскольку Base64 увеличивает размер данных примерно на 33%, это значительно эффективнее с точки зрения использования памяти и быстрее.
- Преимущество бинарных данных : передача бинарных данных, таких как файлы изображений или аудио, между вашим нативным приложением и веб-контентом.
- Ограничение : Даже при использовании массивов байтов система копирует данные через границу межпроцессного взаимодействия (IPC) между приложением и изолированным процессом, который WebView использует для отображения веб-контента. Это по-прежнему потребляет значительное количество памяти для очень больших файлов.
Приведенные ниже примеры кода демонстрируют, как настроить addWebMessageListener на стороне нативного приложения для приема сообщений, помеченных как WebMessageCompat.TYPE_ARRAY_BUFFER , и, при необходимости, для ответа двоичными данными путем проверки наличия WebViewFeature.MESSAGE_ARRAY_BUFFER .
Котлин
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 , позволяющую веб-контенту отправлять и получать двоичные данные ( ArrayBuffer ) в нативное приложение и из него, используя прокси window.myBridge внедренный в предыдущем примере.
// 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 МБ) используйте метод shouldInterceptRequest для потоковой передачи данных:
- Веб-страница инициирует вызов функции
fetch()для пользовательского URL-адреса-заполнителя. Например,https://app.local/large-file. - Приложение для Android перехватывает этот запрос в
WebViewClient. - Приложение возвращает данные в виде
InputStream.
Это позволяет передавать данные порциями, а не загружать весь объем данных в память сразу.
Следующая функция JavaScript демонстрирует клиентский код для эффективной загрузки большого бинарного файла из нативного приложения с помощью стандартного вызова функции fetch() по пользовательскому URL-адресу-заполнителю.
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);
}
}
Приведенные ниже примеры кода демонстрируют работу нативного приложения, использующего метод WebViewClient.shouldInterceptRequest как в Kotlin, так и в Java, для потоковой передачи большого бинарного файла путем перехвата пользовательского URL-адреса-заполнителя, запрошенного веб-контентом.
Котлин
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-уязвимостей, злоумышленник может выполнить скрипт, действуя от имени вашего доверенного источника. Рекомендуется применять проверку полезной нагрузки сообщений перед выполнением конфиденциальных операций нативной платформы.
Минимизируйте площадь поверхности : отображайте только те методы или данные, которые необходимы веб-странице.
Проверка наличия функций во время выполнения : последние API-интерфейсы моста, включая
addWebMessageListener, являются частью библиотеки Jetpack Webkit. Поэтому всегда проверяйте наличие поддержки с помощьюWebViewFeature.isFeatureSupported()перед их вызовом.