На этой странице рассматриваются различные методы и лучшие практики для создания нативного моста, также известного как мост JavaScript, для обеспечения связи между веб-контентом в WebView и хост-приложением Android.
Это позволяет веб-разработчикам использовать JavaScript для доступа к нативным функциям платформы — таким как камера, файловая система или продвинутые аппаратные датчики — которые обычно не предоставляются стандартными веб-API.
Варианты использования
Реализация моста на JavaScript позволяет реализовать различные сценарии интеграции, в которых веб-контент требует более глубокого доступа к операционной системе Android. Ниже приведены некоторые примеры:
- Интеграция с платформой : запуск нативных компонентов пользовательского интерфейса Android (например, подсказок биометрической идентификации,
BottomSheetDialog) с веб-страницы. - Производительность : Перенос ресурсоемких вычислительных задач на собственный код Java или Kotlin.
- Сохранение данных : доступ к локальным зашифрованным базам данных или общим настройкам.
- Передача больших объемов данных : передача медиафайлов или сложных структур данных между приложением и веб-рендером.
Механизмы коммуникации
Android предлагает три основных поколения API для создания нативного моста. Хотя все они по-прежнему доступны, они значительно различаются по безопасности, удобству использования и производительности.
Используйте addWebMessageListener (рекомендуется).
addWebMessageListener — это наиболее современный и рекомендуемый подход к обмену данными между веб-контентом и кодом нативного приложения. Он сочетает в себе простоту использования интерфейса JavaScript с безопасностью системы обмена сообщениями.
Как это работает : приложение добавляет слушатель с определенным именем и набором правил разрешенных источников. Затем WebView гарантирует, что объект JavaScript присутствует в глобальной области видимости ( window.objectName ) с момента начала загрузки страницы.
Инициализация : Чтобы гарантировать внедрение объекта JavaScript в WebView до выполнения каких-либо скриптов, необходимо вызвать addWebMessageListener перед переходом на страницу (например, вызвав WebViewCompat.navigate или 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и инициирует навигацию по страницам (например, с помощьюWebViewCompat.navigateили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(), для проверки безопасности, поскольку они не гарантируют точность и не указывают, какой именно фрейм отправил запрос.
Преобразование и приведение типов данных
При использовании addJavascriptInterface , основанный на Chromium Java Bridge преобразует типы данных между средой выполнения JavaScript и кодом вашего Android-приложения.
К параметрам метода и возвращаемым значениям применяются следующие правила приведения типов.
Сопоставление типов параметров (JavaScript с Java)
Когда JavaScript передает аргументы аннотированному методу Java или Kotlin, мост преобразует значения JavaScript в соответствующие типы параметров Java:
| Тип параметра Java | значение аргумента JavaScript | поведение принуждения |
|---|---|---|
byte , short , int , long | Число (целое число) | Значения преобразуются к целевому целочисленному типу. Выход за пределы допустимого диапазона значений приводит к циклическому перебору в соответствии со стандартными правилами числового преобразования. |
byte , short , int , long | NaN | Приводит к 0 . |
byte , short , int , long | Infinity | Приводит к значению -1 для byte и short , или к Integer.MAX_VALUE и Long.MAX_VALUE для int и long . |
float , double | Число | Преобразует значение в соответствующее число с плавающей запятой в Java. |
float , double | NaN / Infinity | Преобразует в Float.NaN , Double.NaN , Float.POSITIVE_INFINITY или Double.POSITIVE_INFINITY . |
char | Число (целое число) | Преобразовано в соответствующий кодовый пункт Unicode. |
char | Нецелое число, NaN , Infinity | Принуждает к \u0000 . |
boolean | true / false | Преобразует значение в true или false в Java. |
boolean | Число, Строка, Объект | Приводит к значению false (включая непустые строки и ненулевые числа). |
String | Нить | Строковое значение сохраняется. |
String | Число, логическое значение | Отформатировано в виде строкового представления (например, "42" , "true" , "false" ). |
String | null / undefined | Значение null преобразуется в Java null ; значение undefined преобразуется в строковый литерал "undefined" . |
String | Объект, ArrayBuffer, TypedArray | Преобразует в строковый литерал "undefined" . |
Примитивный массив (например int[] , byte[] , boolean[] ) или String[] | Множество ( [...] ) | Преобразует в одномерный Java-массив целевого типа элементов. В разреженных массивах незаполненные индексы заполняются значениями по умолчанию ( 0 , false , null ). |
Примитивный массив (например, int[] , byte[] ) | TypedArray ( Int8Array , Uint8Array , Int32Array , Float64Array ) | Элементы преобразуются в соответствующий примитивный массив Java. |
Многомерный массив (например, int[][] ) | Вложенный массив ( [[...]] ) | Не поддерживается. Параметры многомерного массива возвращают значение null . |
ArrayBuffer , DataView | ArrayBuffer , DataView | Не поддерживается в качестве массивов. Экземпляры ArrayBuffer и DataView возвращают значение null . |
Object или пользовательский класс | Объект JavaScript ( {...} ) | Не поддерживается. Произвольные литералы объектов JavaScript в Java возвращают значение null . |
Object или пользовательский класс | Внедренная обертка для Java-объекта | Поддерживается (с передачей данных туда и обратно). Передает базовый экземпляр Java методу Java. Генерирует исключение JavaScript, если тип Java не соответствует сигнатуре параметра. |
Типы данных в виде блоков (например, Integer , Double , Boolean ) | Число, логическое значение | Не поддерживается. Упакованные примитивные типы рассматриваются как непрозрачные объекты и возвращают значение null . |
| Любой примитивный тип | null / undefined | Приводит к значениям по умолчанию ( 0 , 0.0 , \u0000 , false ). |
Object , String , массив | null | Приводит к значению null в Java. |
Сопоставление типов возвращаемых значений (Java и JavaScript)
Когда аннотированный метод Java или Kotlin возвращает значение, мост преобразует его в тип JavaScript:
| Тип возвращаемого значения Java | значение JavaScript | JavaScript typeof |
|---|---|---|
boolean | true / false | "boolean" |
byte , short , int , long , число с плавающей запятой, float double | Число | "number" |
char | Число (кодовая точка Unicode) | "number" |
String (непустая) | строковое значение | "string" |
String ( null ) | undefined | "undefined" |
void | undefined | "undefined" |
Массивы Java (например, int[] , String[] ) | undefined | "undefined" . Возвращаемые значения массивов не поддерживаются. Метод Java не выполняется, и возвращается undefined без генерации исключения. |
| Объект Java / пользовательский тип (не null) | Оболочка объекта | "object" . Создает JavaScript-обертку вокруг экземпляра Java. JavaScript-код может вызывать любой публичный метод этого объекта, аннотированного @JavascriptInterface . |
Объект Java / пользовательский тип ( null ) | null | "object" |
Упакованные примитивы (например, Integer , Double ) | Оболочка объекта | "object" . Возвращается в виде непрозрачной обертки Java-объекта без доступных методов @JavascriptInterface , что делает значение непригодным для использования в JavaScript. |
Доступность метода и участников
Мост JavaScript обеспечивает строгие правила доступа и видимости элементов для защиты от непреднамеренного выполнения кода:
- Поля не являются доступными извне : поля Java (включая
publicиpublic finalполя) недоступны из JavaScript и возвращают значениеundefined. - Требование к аннотации : Только методы, явно аннотированные с помощью
@JavascriptInterface, доступны для JavaScript. - Ограничения видимости : Методы должны быть
public.privateиprotectedметоды никогда не будут доступны JavaScript, даже если они содержат аннотацию@JavascriptInterface. - Статические методы : Статические методы, аннотированные
@JavascriptInterface, могут быть вызваны из JavaScript. - Наследование и переопределение : аннотации
@JavascriptInterfaceне наследуются, когда подкласс переопределяет метод. Если подкласс переопределяет аннотированный метод из суперкласса, подкласс должен явно включить аннотацию@JavascriptInterfaceв переопределенный метод, чтобы сделать его доступным для JavaScript. Непереопределенные публичные методы, унаследованные от суперкласса, остаются доступными, если они аннотированы в суперклассе. - Защита с помощью рефлексии : стандартные методы рефлексии Java (например,
getClass()) блокируются и генерируют исключение JavaScript для предотвращения уязвимостей удаленного выполнения кода. - Перегрузка методов : Поддерживаются перегруженные методы Java. Мост разрешает вызовы методов, основываясь только на количестве переданных аргументов, и не учитывает типы аргументов. Вызов перегруженного метода с недопустимым количеством аргументов вызовет исключение JavaScript. Если две перегрузки имеют одинаковое количество аргументов, одна из них будет выбрана произвольно.
Краткое описание механизмов
В следующей таблице представлено краткое сравнение трех основных механизмов реализации нативных мостов:
| Метод | 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()перед их вызовом.
На этой странице рассматриваются различные методы и лучшие практики для создания нативного моста, также известного как мост JavaScript, для обеспечения связи между веб-контентом в WebView и хост-приложением Android.
Это позволяет веб-разработчикам использовать JavaScript для доступа к нативным функциям платформы — таким как камера, файловая система или продвинутые аппаратные датчики — которые обычно не предоставляются стандартными веб-API.
Варианты использования
Реализация моста на JavaScript позволяет реализовать различные сценарии интеграции, в которых веб-контент требует более глубокого доступа к операционной системе Android. Ниже приведены некоторые примеры:
- Интеграция с платформой : запуск нативных компонентов пользовательского интерфейса Android (например, подсказок биометрической идентификации,
BottomSheetDialog) с веб-страницы. - Производительность : Перенос ресурсоемких вычислительных задач на собственный код Java или Kotlin.
- Сохранение данных : доступ к локальным зашифрованным базам данных или общим настройкам.
- Передача больших объемов данных : передача медиафайлов или сложных структур данных между приложением и веб-рендером.
Механизмы коммуникации
Android предлагает три основных поколения API для создания нативного моста. Хотя все они по-прежнему доступны, они значительно различаются по безопасности, удобству использования и производительности.
Используйте addWebMessageListener (рекомендуется).
addWebMessageListener — это наиболее современный и рекомендуемый подход к обмену данными между веб-контентом и кодом нативного приложения. Он сочетает в себе простоту использования интерфейса JavaScript с безопасностью системы обмена сообщениями.
Как это работает : приложение добавляет слушатель с определенным именем и набором правил разрешенных источников. Затем WebView гарантирует, что объект JavaScript присутствует в глобальной области видимости ( window.objectName ) с момента начала загрузки страницы.
Инициализация : Чтобы гарантировать внедрение объекта JavaScript в WebView до выполнения каких-либо скриптов, необходимо вызвать addWebMessageListener перед переходом на страницу (например, вызвав WebViewCompat.navigate или 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и инициирует навигацию по страницам (например, с помощьюWebViewCompat.navigateили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(), для проверки безопасности, поскольку они не гарантируют точность и не указывают, какой именно фрейм отправил запрос.
Преобразование и приведение типов данных
При использовании addJavascriptInterface , основанный на Chromium Java Bridge преобразует типы данных между средой выполнения JavaScript и кодом вашего Android-приложения.
К параметрам метода и возвращаемым значениям применяются следующие правила приведения типов.
Сопоставление типов параметров (JavaScript с Java)
Когда JavaScript передает аргументы аннотированному методу Java или Kotlin, мост преобразует значения JavaScript в соответствующие типы параметров Java:
| Тип параметра Java | значение аргумента JavaScript | поведение принуждения |
|---|---|---|
byte , short , int , long | Число (целое число) | Значения преобразуются к целевому целочисленному типу. Выход за пределы допустимого диапазона значений приводит к циклическому перебору в соответствии со стандартными правилами числового преобразования. |
byte , short , int , long | NaN | Приводит к 0 . |
byte , short , int , long | Infinity | Приводит к значению -1 для byte и short , или к Integer.MAX_VALUE и Long.MAX_VALUE для int и long . |
float , double | Число | Преобразует значение в соответствующее число с плавающей запятой в Java. |
float , double | NaN / Infinity | Преобразует в Float.NaN , Double.NaN , Float.POSITIVE_INFINITY или Double.POSITIVE_INFINITY . |
char | Число (целое число) | Преобразовано в соответствующий кодовый пункт Unicode. |
char | Нецелое число, NaN , Infinity | Принуждает к \u0000 . |
boolean | true / false | Преобразует значение в true или false в Java. |
boolean | Число, Строка, Объект | Приводит к значению false (включая непустые строки и ненулевые числа). |
String | Нить | Строковое значение сохраняется. |
String | Число, логическое значение | Отформатировано в виде строкового представления (например, "42" , "true" , "false" ). |
String | null / undefined | Значение null преобразуется в Java null ; значение undefined преобразуется в строковый литерал "undefined" . |
String | Объект, ArrayBuffer, TypedArray | Преобразует в строковый литерал "undefined" . |
Примитивный массив (например int[] , byte[] , boolean[] ) или String[] | Множество ( [...] ) | Преобразует в одномерный Java-массив целевого типа элементов. В разреженных массивах незаполненные индексы заполняются значениями по умолчанию ( 0 , false , null ). |
Примитивный массив (например, int[] , byte[] ) | TypedArray ( Int8Array , Uint8Array , Int32Array , Float64Array ) | Элементы преобразуются в соответствующий примитивный массив Java. |
Многомерный массив (например, int[][] ) | Вложенный массив ( [[...]] ) | Не поддерживается. Параметры многомерного массива возвращают значение null . |
ArrayBuffer , DataView | ArrayBuffer , DataView | Не поддерживается в качестве массивов. Экземпляры ArrayBuffer и DataView возвращают значение null . |
Object или пользовательский класс | Объект JavaScript ( {...} ) | Не поддерживается. Произвольные литералы объектов JavaScript в Java возвращают значение null . |
Object или пользовательский класс | Внедренная обертка для Java-объекта | Поддерживается (с передачей данных туда и обратно). Передает базовый экземпляр Java методу Java. Генерирует исключение JavaScript, если тип Java не соответствует сигнатуре параметра. |
Типы данных в виде блоков (например, Integer , Double , Boolean ) | Число, логическое значение | Не поддерживается. Упакованные примитивные типы рассматриваются как непрозрачные объекты и возвращают значение null . |
| Любой примитивный тип | null / undefined | Приводит к значениям по умолчанию ( 0 , 0.0 , \u0000 , false ). |
Object , String , массив | null | Приводит к значению null в Java. |
Сопоставление типов возвращаемых значений (Java и JavaScript)
Когда аннотированный метод Java или Kotlin возвращает значение, мост преобразует его в тип JavaScript:
| Тип возвращаемого значения Java | значение JavaScript | JavaScript typeof |
|---|---|---|
boolean | true / false | "boolean" |
byte , short , int , long , число с плавающей запятой, float double | Число | "number" |
char | Число (кодовая точка Unicode) | "number" |
String (непустая) | строковое значение | "string" |
String ( null ) | undefined | "undefined" |
void | undefined | "undefined" |
Массивы Java (например, int[] , String[] ) | undefined | "undefined" . Возвращаемые значения массивов не поддерживаются. Метод Java не выполняется, и возвращается undefined без генерации исключения. |
| Объект Java / пользовательский тип (не null) | Оболочка объекта | "object" . Создает JavaScript-обертку вокруг экземпляра Java. JavaScript-код может вызывать любой публичный метод этого объекта, аннотированного @JavascriptInterface . |
Объект Java / пользовательский тип ( null ) | null | "object" |
Упакованные примитивы (например, Integer , Double ) | Оболочка объекта | "object" . Возвращается в виде непрозрачной обертки Java-объекта без доступных методов @JavascriptInterface , что делает значение непригодным для использования в JavaScript. |
Доступность метода и участников
Мост JavaScript обеспечивает строгие правила доступа и видимости элементов для защиты от непреднамеренного выполнения кода:
- Поля не являются доступными извне : поля Java (включая
publicиpublic finalполя) недоступны из JavaScript и возвращают значениеundefined. - Требование к аннотации : Только методы, явно аннотированные с помощью
@JavascriptInterface, доступны для JavaScript. - Ограничения видимости : Методы должны быть
public.privateиprotectedметоды никогда не будут доступны JavaScript, даже если они содержат аннотацию@JavascriptInterface. - Статические методы : Статические методы, аннотированные
@JavascriptInterface, могут быть вызваны из JavaScript. - Inheritance and overriding :
@JavascriptInterfaceannotations are not inherited when a subclass overrides a method. If a subclass overrides an annotated method from a superclass, the subclass must explicitly include the@JavascriptInterfaceannotation on the overridden method to expose it to JavaScript. Non-overridden public methods inherited from a superclass remain accessible if annotated in the superclass. - Reflection protection : Standard Java reflection methods (such as
getClass()) are blocked and throw a JavaScript exception to prevent remote code execution vulnerabilities. - Method overloading : Overloaded Java methods are supported. The bridge resolves method calls based on the number of passed arguments only, and does not take argument types into account. Calling an overloaded method with an invalid argument count raises a JavaScript exception. If two overloads have the same argument count, one will be chosen arbitrarily.
Summary of mechanisms
The following table provides a quick comparison of the three primary native bridge implementation mechanisms:
| Метод | addWebMessageListener | postWebMessage | addJavascriptInterface |
|---|---|---|---|
| Выполнение | Asynchronous (Listener on main thread) | Асинхронный | Синхронный |
| Безопасность | Highest (Allowlist-based) | High (Origin aware) | Low (No origin checks) |
| Сложность | Умеренный | Умеренный | Простой |
| Направление | Двунаправленный | Двунаправленный | Web to app |
| Minimum WebView version | Version 82 (and Jetpack Webkit 1.3.0) | Version 45 (and Jetpack Webkit 1.1.0) | Все версии |
| Рекомендуется | Да | Нет | Нет |
Handle large data transfers
You must manage memory carefully when transferring large payloads, such as multi-megabyte strings or binary files, to avoid Application Not Responding (ANR) errors or crashes on 32-bit devices. This section discusses the various techniques and limitations associated with transferring significant amounts of data between the host application and web content.
Transfer binary data with byte arrays
With the WebMessageCompat class, you can send byte[] arrays directly instead of serializing binary data into Base64 strings. Since Base64 adds roughly 33% overhead to the data size, this is significantly more memory-efficient and faster.
- Binary advantage : Transfer binary data like image files or audio between your native app and web content.
- Limitation : Even with byte arrays, the system copies data across the inter-process communication (IPC) boundary between the app and the isolated process that WebView uses to render the web content. This still consumes significant memory for very large files.
The following code examples demonstrate how to set up addWebMessageListener on the native app side to receive messages marked with WebMessageCompat.TYPE_ARRAY_BUFFER and optionally reply with binary data by checking for 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
);
}
}
The following JavaScript code demonstrates the client-side implementation of addWebMessageListener , enabling the web content to send and receive binary data ( ArrayBuffer ) to and from the native app using the window.myBridge proxy injected in the previous example.
// 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]);
}
};
}
Efficient large-scale data loading
For very large files (>10 MB), use the shouldInterceptRequest method to stream data:
- The web page initiates a
fetch()call to a custom, placeholder URL. For example,https://app.local/large-file. - The Android app intercepts this request in
WebViewClient.shouldInterceptRequest. - The app returns the data as an
InputStream.
This enables streaming data in chunks rather than loading the entire payload into memory at once.
The following JavaScript function demonstrates the client-side code for efficiently loading a large binary file from the native application using a standard fetch() call to a custom, placeholder 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);
}
}
The following code examples demonstrate the native app side, using the WebViewClient.shouldInterceptRequest method in both Kotlin and Java, to stream a large binary file by intercepting a custom placeholder URL requested by the web content.
Котлин
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);
}
});
Follow security recommendations
To protect your application and user data, follow these guidelines when implementing a bridge:
Enforce HTTPS : To ensure that malicious third-party content can't invoke your application's native logic, only allow communication with secure origins.
Rely on origin rules : The best way to deal with trust is to strictly define your
allowedOriginRulesand check thesourceOriginprovided in the message callback. Avoid using the full wildcard (*), which matches all origins, as your only origin rule unless absolutely necessary. Using wildcards for subdomains (for example,*.example.com) remains valid and secure for matching multiple subdomains (for example,foo.example.com,bar.example.com).Note : While origin rules protect against malicious third-party websites and hidden iframes, they can't protect against cross-site scripting (XSS) vulnerabilities within your own trusted domain. For example, if your web page displays user-generated content and is vulnerable to stored XSS, an attacker could execute a script acting as your trusted origin. Consider applying validation to the message payloads before executing sensitive native platform operations.
Minimize surface area : Only expose the specific methods or data that the web page requires.
Check features at runtime : Recent bridge APIs, including
addWebMessageListener, are part of the Jetpack Webkit library. So, always check for support usingWebViewFeature.isFeatureSupported()before calling them.
This page discusses the various methods and best practices for establishing a native bridge, also known as JavaScript bridge, to facilitate communication between web content in a WebView and a host Android application.
This enables web developers to use JavaScript to access native platform features—such as the camera, file system, or advanced hardware sensors—that standard web APIs don't normally provide.
Варианты использования
A JavaScript bridge implementation enables various integration scenarios where web content requires deeper access to the Android operating system. The following are some examples:
- Platform integration : Triggering native Android UI components (for example, Biometric prompts,
BottomSheetDialog) from a web page. - Performance : Offloading heavy computational tasks to native Java or Kotlin code.
- Data persistence : Accessing local encrypted databases or shared preferences.
- Large data transfers : Passing media files or complex data structures between the app and the web renderer.
Механизмы коммуникации
Android offers three primary generations of APIs to establish a native bridge. While they are all still available, they differ significantly in security, usability, and performance.
Use addWebMessageListener (Recommended)
addWebMessageListener is the most modern and recommended approach for communication between the web content and native app code. It combines the ease of use of the JavaScript interface with the security of the messaging system.
How it works : The app adds a listener with a specific name and a set of allowed origin rules. The WebView then ensures the JavaScript object is present in the global scope ( window.objectName ) from the moment the page begins to load.
Initialization : To ensure the WebView injects the JavaScript object before any script runs, you must call addWebMessageListener before navigating to the page (such as calling WebViewCompat.navigate or loadUrl ).
Основные характеристики :
Security and trust : Unlike legacy APIs, this method requires a
Set<String>ofallowedOriginRulesduring initialization. This is the primary mechanism for establishing trust.When you specify a trusted origin, such as
https://example.com, the WebView guarantees that it only exposes the injected JavaScript objects to web pages loaded from that exact origin.The native listener callback receives a
sourceOriginparameter with every message. You can use this to verify the exact origin of the sender if your bridge supports multiple allowed origins.Because the WebView strictly enforces these origin checks at the platform level, your app can generally rely upon messages received from a trusted
sourceOriginas truthful, eliminating the need for rigorous payload validation in most standard implementations.- WebView matches rules against the scheme (HTTP/HTTPS), host, and port.
- WebView ignores paths. For example,
https://example.comallowshttps://example.com/loginandhttps://example.com/home. - WebView strictly limits wildcards to the start of the host for subdomains. For example,
https://*.example.commatcheshttps://foo.example.combut nothttps://example.com. If you need to match bothhttps://example.comand its subdomains, you must add each origin rule separately to the allowlist (for example,"https://example.com", "https://*.example.com"). You can't use wildcards for the scheme or in the middle of a domain.
This restricts the bridge to verified domains, preventing unauthorized third-party content or injected iframes from executing native code.
Multi-frame support : Works across all frames that match the origin rules.
Threading : The listener callback runs on the application's main (UI) thread. If your bridge needs to handle complex data processing, JSON parsing, or database lookups, you must offload that work to a background thread to prevent freezing the application UI with an "app not responding" (ANR) error.
Bidirectional : When the web page sends a message, the app receives a
JavaScriptReplyProxythat it can use to send messages back to that specific frame. You can retain thisreplyProxyobject and use it at any time to send any number of messages to the page, not just to reply to each individual message the page sends. If the originating frame navigates away or is destroyed, messages sent usingpostMessage()on the proxy are silently ignored.App-side initiation : Although the web page must always initiate the communication channel with the app, the native app can unilaterally prompt the web page to begin this process. The native app can communicate to the web page with
addDocumentStartJavaScript()(to evaluate JavaScript before the page loads) orevaluateJavaScript()(to evaluate JavaScript after the page has loaded).
Limitation : This API sends data as either strings or byte[] arrays. For more complicated data structures, such as, JSON objects, you must serialize this to one of those formats and then deserialize on the other side to reconstruct the data structure.
Usage example :
To understand the full sequence of a bidirectional message exchange, the events proceed in this order:
- Initiation (app) : The native app registers the listener with
addWebMessageListenerand initiates page navigation (such as withWebViewCompat.navigateorloadUrl). - Message send (web) : The web page's JavaScript calls
myObject.postMessage(message)to initiate the communication. - Message receive and reply (app) : The app receives the message in the listener callback and replies using the provided
replyProxy.postMessage(). - Reply receive (web) : The web page receives the asynchronous reply in the
myObject.onmessage()callback function.
Котлин
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);
}
The following JavaScript demonstrates the client-side implementation of addWebMessageListener , allowing the web content to receive messages from the native app and send its own messages through the myObject proxy.
myObject.onmessage = function(event) {
console.log("App says: " + event.data);
};
myObject.postMessage("Hello world!");
Use postWebMessage (Alternative)
Android introduced this to provide an asynchronous, messaging-based alternative similar to the web's window.postMessage .
How it works : The app uses WebViewCompat.postWebMessage to send a payload to the web page's main frame. To establish a bidirectional communication channel, you can create a WebMessageChannel and pass one of its ports with the message to the web content.
Характеристики :
- Asynchronous : Like
addWebMessageListener, this method uses asynchronous messaging, which ensures the web page remains responsive to user interactions while the app processes data in the background. - Origin aware : You can specify a
targetOriginto ensure the WebView delivers data only to a trusted website.
Ограничения :
- Scope : This API limits communication to the main frame. It doesn't support directly addressing or sending messages to iframes.
- URI restrictions : You cannot use this method for content loaded using
data:URIs,file:URIs, orloadData(), unless you specify "*" as the target origin. Doing this lets any page receive the message. - Identity risk : There is no clear way for the web content to verify the sender's identity. A message that the web page receives could have originated from your native app or another iframe.
Use this method when you need a simple, async channel for string-based data in earlier Android versions that don't support addWebMessageListener .
Use addJavascriptInterface (Legacy)
The oldest method involves injecting a native object instance directly into the WebView.
How it works : You define a Kotlin or Java class, annotate the allowed methods with @JavascriptInterface , and add an instance of the class to the WebView using addJavascriptInterface(Object, String) .
Характеристики :
- Synchronous : The JavaScript execution environment blocks until the method in your Android code returns.
- Thread safety : The system calls methods on a background thread, requiring careful synchronization on the Kotlin or Java side.
- Security risk : By default,
addJavascriptInterfaceis available to every frame within the WebView, including iframes. It lacks origin-based access control. Due to the asynchronous behavior of WebView, it isn't possible to safely determine the URL of the frame that is calling your interface. You must not rely on methods likeWebView.getUrl()for security verification, as they aren't guaranteed to be accurate and don't indicate which specific frame made the request.
Data type conversions and coercion
When using addJavascriptInterface , the Chromium-based Java Bridge converts data types between the JavaScript runtime and your Android app code.
The following coercion rules apply to method parameters and return values.
Parameter type mapping (JavaScript to Java)
When JavaScript passes arguments to an annotated Java or Kotlin method, the bridge coerces JavaScript values into the corresponding Java parameter types:
| Java parameter type | JavaScript argument value | Coercion behavior |
|---|---|---|
byte , short , int , long | Number (integer) | Values are cast to the target integer type. Out-of-bounds values wrap around according to standard numeric casting rules. |
byte , short , int , long | NaN | Coerces to 0 . |
byte , short , int , long | Infinity | Coerces to -1 for byte and short , or Integer.MAX_VALUE and Long.MAX_VALUE for int and long . |
float , double | Число | Coerces to the corresponding Java floating-point value. |
float , double | NaN / Infinity | Coerces to Float.NaN , Double.NaN , Float.POSITIVE_INFINITY , or Double.POSITIVE_INFINITY . |
char | Number (integer) | Converted to the corresponding Unicode code point. |
char | Non-integer, NaN , Infinity | Coerces to \u0000 . |
boolean | true / false | Coerces to Java true or false . |
boolean | Number, String, Object | Coerces to false (including non-empty strings and non-zero numbers). |
String | Нить | String value is preserved. |
String | Number, Boolean | Formatted as a string representation (for example, "42" , "true" , "false" ). |
String | null / undefined | null coerces to Java null ; undefined coerces to the literal string "undefined" . |
String | Object, ArrayBuffer, TypedArray | Coerces to the literal string "undefined" . |
Primitive array (such as int[] , byte[] , boolean[] ) or String[] | Множество ( [...] ) | Converts to a 1D Java array of the target element type. Sparse arrays fill unassigned indexes with default values ( 0 , false , null ). |
Primitive array (such as int[] , byte[] ) | TypedArray ( Int8Array , Uint8Array , Int32Array , Float64Array ) | Elements are coerced into the corresponding Java primitive array. |
Multi-dimensional array (such as int[][] ) | Nested array ( [[...]] ) | Not supported. Multi-dimensional array parameters evaluate to null . |
ArrayBuffer , DataView | ArrayBuffer , DataView | Not supported as arrays. ArrayBuffer and DataView instances evaluate to null . |
Object or custom class | JavaScript object ( {...} ) | Not supported. Arbitrary JavaScript object literals evaluate to null in Java. |
Object or custom class | Injected Java object wrapper | Supported (Round-tripping). Passes the underlying Java instance to the Java method. Throws a JavaScript exception if the Java type does not match the parameter signature. |
Boxed types (such as Integer , Double , Boolean ) | Number, Boolean | Not supported. Boxed primitive types are treated as opaque objects and evaluate to null . |
| Any primitive type | null / undefined | Coerces to default values ( 0 , 0.0 , \u0000 , false ). |
Object , String , array | null | Coerces to Java null . |
Return type mapping (Java to JavaScript)
When an annotated Java or Kotlin method returns a value, the bridge converts it to a JavaScript type:
| Java return type | значение JavaScript | JavaScript typeof |
|---|---|---|
boolean | true / false | "boolean" |
byte , short , int , long , float , double | Число | "number" |
char | Number (Unicode code point) | "number" |
String (non-null) | строковое значение | "string" |
String ( null ) | undefined | "undefined" |
void | undefined | "undefined" |
Java array (such as int[] , String[] ) | undefined | "undefined" . Array return values are not supported. The Java method is not executed, and undefined is returned without raising an exception. |
| Java Object / custom type (non-null) | Оболочка объекта | "object" . Creates a JavaScript wrapper around the Java instance. JavaScript code can call any public method on this object that is annotated with @JavascriptInterface . |
Java Object / custom type ( null ) | null | "object" |
Boxed primitive (such as Integer , Double ) | Оболочка объекта | "object" . Returned as an opaque Java object wrapper with no accessible @JavascriptInterface methods, making the value unusable in JavaScript. |
Method and member accessibility
The JavaScript bridge enforces strict member access and visibility rules to protect against unintended code execution:
- Fields are not exposed : Java fields (including
publicandpublic finalfields) are not accessible from JavaScript and evaluate toundefined. - Annotation requirement : Only methods explicitly annotated with
@JavascriptInterfaceare exposed to JavaScript. - Visibility restrictions : Methods must be
public.privateandprotectedmethods are never exposed to JavaScript, even if they carry the@JavascriptInterfaceannotation. - Static methods : Static methods annotated with
@JavascriptInterfaceare callable from JavaScript. - Inheritance and overriding :
@JavascriptInterfaceannotations are not inherited when a subclass overrides a method. If a subclass overrides an annotated method from a superclass, the subclass must explicitly include the@JavascriptInterfaceannotation on the overridden method to expose it to JavaScript. Non-overridden public methods inherited from a superclass remain accessible if annotated in the superclass. - Reflection protection : Standard Java reflection methods (such as
getClass()) are blocked and throw a JavaScript exception to prevent remote code execution vulnerabilities. - Method overloading : Overloaded Java methods are supported. The bridge resolves method calls based on the number of passed arguments only, and does not take argument types into account. Calling an overloaded method with an invalid argument count raises a JavaScript exception. If two overloads have the same argument count, one will be chosen arbitrarily.
Summary of mechanisms
The following table provides a quick comparison of the three primary native bridge implementation mechanisms:
| Метод | addWebMessageListener | postWebMessage | addJavascriptInterface |
|---|---|---|---|
| Выполнение | Asynchronous (Listener on main thread) | Асинхронный | Синхронный |
| Безопасность | Highest (Allowlist-based) | High (Origin aware) | Low (No origin checks) |
| Сложность | Умеренный | Умеренный | Простой |
| Направление | Двунаправленный | Двунаправленный | Web to app |
| Minimum WebView version | Version 82 (and Jetpack Webkit 1.3.0) | Version 45 (and Jetpack Webkit 1.1.0) | Все версии |
| Рекомендуется | Да | Нет | Нет |
Handle large data transfers
You must manage memory carefully when transferring large payloads, such as multi-megabyte strings or binary files, to avoid Application Not Responding (ANR) errors or crashes on 32-bit devices. This section discusses the various techniques and limitations associated with transferring significant amounts of data between the host application and web content.
Transfer binary data with byte arrays
With the WebMessageCompat class, you can send byte[] arrays directly instead of serializing binary data into Base64 strings. Since Base64 adds roughly 33% overhead to the data size, this is significantly more memory-efficient and faster.
- Binary advantage : Transfer binary data like image files or audio between your native app and web content.
- Limitation : Even with byte arrays, the system copies data across the inter-process communication (IPC) boundary between the app and the isolated process that WebView uses to render the web content. This still consumes significant memory for very large files.
The following code examples demonstrate how to set up addWebMessageListener on the native app side to receive messages marked with WebMessageCompat.TYPE_ARRAY_BUFFER and optionally reply with binary data by checking for 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
);
}
}
The following JavaScript code demonstrates the client-side implementation of addWebMessageListener , enabling the web content to send and receive binary data ( ArrayBuffer ) to and from the native app using the window.myBridge proxy injected in the previous example.
// 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]);
}
};
}
Efficient large-scale data loading
For very large files (>10 MB), use the shouldInterceptRequest method to stream data:
- The web page initiates a
fetch()call to a custom, placeholder URL. For example,https://app.local/large-file. - The Android app intercepts this request in
WebViewClient.shouldInterceptRequest. - The app returns the data as an
InputStream.
This enables streaming data in chunks rather than loading the entire payload into memory at once.
The following JavaScript function demonstrates the client-side code for efficiently loading a large binary file from the native application using a standard fetch() call to a custom, placeholder 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);
}
}
The following code examples demonstrate the native app side, using the WebViewClient.shouldInterceptRequest method in both Kotlin and Java, to stream a large binary file by intercepting a custom placeholder URL requested by the web content.
Котлин
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);
}
});
Follow security recommendations
To protect your application and user data, follow these guidelines when implementing a bridge:
Enforce HTTPS : To ensure that malicious third-party content can't invoke your application's native logic, only allow communication with secure origins.
Rely on origin rules : The best way to deal with trust is to strictly define your
allowedOriginRulesand check thesourceOriginprovided in the message callback. Avoid using the full wildcard (*), which matches all origins, as your only origin rule unless absolutely necessary. Using wildcards for subdomains (for example,*.example.com) remains valid and secure for matching multiple subdomains (for example,foo.example.com,bar.example.com).Note : While origin rules protect against malicious third-party websites and hidden iframes, they can't protect against cross-site scripting (XSS) vulnerabilities within your own trusted domain. For example, if your web page displays user-generated content and is vulnerable to stored XSS, an attacker could execute a script acting as your trusted origin. Consider applying validation to the message payloads before executing sensitive native platform operations.
Minimize surface area : Only expose the specific methods or data that the web page requires.
Check features at runtime : Recent bridge APIs, including
addWebMessageListener, are part of the Jetpack Webkit library. So, always check for support usingWebViewFeature.isFeatureSupported()before calling them.
This page discusses the various methods and best practices for establishing a native bridge, also known as JavaScript bridge, to facilitate communication between web content in a WebView and a host Android application.
This enables web developers to use JavaScript to access native platform features—such as the camera, file system, or advanced hardware sensors—that standard web APIs don't normally provide.
Варианты использования
A JavaScript bridge implementation enables various integration scenarios where web content requires deeper access to the Android operating system. The following are some examples:
- Platform integration : Triggering native Android UI components (for example, Biometric prompts,
BottomSheetDialog) from a web page. - Performance : Offloading heavy computational tasks to native Java or Kotlin code.
- Data persistence : Accessing local encrypted databases or shared preferences.
- Large data transfers : Passing media files or complex data structures between the app and the web renderer.
Механизмы коммуникации
Android offers three primary generations of APIs to establish a native bridge. While they are all still available, they differ significantly in security, usability, and performance.
Use addWebMessageListener (Recommended)
addWebMessageListener is the most modern and recommended approach for communication between the web content and native app code. It combines the ease of use of the JavaScript interface with the security of the messaging system.
How it works : The app adds a listener with a specific name and a set of allowed origin rules. The WebView then ensures the JavaScript object is present in the global scope ( window.objectName ) from the moment the page begins to load.
Initialization : To ensure the WebView injects the JavaScript object before any script runs, you must call addWebMessageListener before navigating to the page (such as calling WebViewCompat.navigate or loadUrl ).
Основные характеристики :
Security and trust : Unlike legacy APIs, this method requires a
Set<String>ofallowedOriginRulesduring initialization. This is the primary mechanism for establishing trust.When you specify a trusted origin, such as
https://example.com, the WebView guarantees that it only exposes the injected JavaScript objects to web pages loaded from that exact origin.The native listener callback receives a
sourceOriginparameter with every message. You can use this to verify the exact origin of the sender if your bridge supports multiple allowed origins.Because the WebView strictly enforces these origin checks at the platform level, your app can generally rely upon messages received from a trusted
sourceOriginas truthful, eliminating the need for rigorous payload validation in most standard implementations.- WebView matches rules against the scheme (HTTP/HTTPS), host, and port.
- WebView ignores paths. For example,
https://example.comallowshttps://example.com/loginandhttps://example.com/home. - WebView strictly limits wildcards to the start of the host for subdomains. For example,
https://*.example.commatcheshttps://foo.example.combut nothttps://example.com. If you need to match bothhttps://example.comand its subdomains, you must add each origin rule separately to the allowlist (for example,"https://example.com", "https://*.example.com"). You can't use wildcards for the scheme or in the middle of a domain.
This restricts the bridge to verified domains, preventing unauthorized third-party content or injected iframes from executing native code.
Multi-frame support : Works across all frames that match the origin rules.
Threading : The listener callback runs on the application's main (UI) thread. If your bridge needs to handle complex data processing, JSON parsing, or database lookups, you must offload that work to a background thread to prevent freezing the application UI with an "app not responding" (ANR) error.
Bidirectional : When the web page sends a message, the app receives a
JavaScriptReplyProxythat it can use to send messages back to that specific frame. You can retain thisreplyProxyobject and use it at any time to send any number of messages to the page, not just to reply to each individual message the page sends. If the originating frame navigates away or is destroyed, messages sent usingpostMessage()on the proxy are silently ignored.App-side initiation : Although the web page must always initiate the communication channel with the app, the native app can unilaterally prompt the web page to begin this process. The native app can communicate to the web page with
addDocumentStartJavaScript()(to evaluate JavaScript before the page loads) orevaluateJavaScript()(to evaluate JavaScript after the page has loaded).
Limitation : This API sends data as either strings or byte[] arrays. For more complicated data structures, such as, JSON objects, you must serialize this to one of those formats and then deserialize on the other side to reconstruct the data structure.
Usage example :
To understand the full sequence of a bidirectional message exchange, the events proceed in this order:
- Initiation (app) : The native app registers the listener with
addWebMessageListenerand initiates page navigation (such as withWebViewCompat.navigateorloadUrl). - Message send (web) : The web page's JavaScript calls
myObject.postMessage(message)to initiate the communication. - Message receive and reply (app) : The app receives the message in the listener callback and replies using the provided
replyProxy.postMessage(). - Reply receive (web) : The web page receives the asynchronous reply in the
myObject.onmessage()callback function.
Котлин
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);
}
The following JavaScript demonstrates the client-side implementation of addWebMessageListener , allowing the web content to receive messages from the native app and send its own messages through the myObject proxy.
myObject.onmessage = function(event) {
console.log("App says: " + event.data);
};
myObject.postMessage("Hello world!");
Use postWebMessage (Alternative)
Android introduced this to provide an asynchronous, messaging-based alternative similar to the web's window.postMessage .
How it works : The app uses WebViewCompat.postWebMessage to send a payload to the web page's main frame. To establish a bidirectional communication channel, you can create a WebMessageChannel and pass one of its ports with the message to the web content.
Характеристики :
- Asynchronous : Like
addWebMessageListener, this method uses asynchronous messaging, which ensures the web page remains responsive to user interactions while the app processes data in the background. - Origin aware : You can specify a
targetOriginto ensure the WebView delivers data only to a trusted website.
Ограничения :
- Scope : This API limits communication to the main frame. It doesn't support directly addressing or sending messages to iframes.
- URI restrictions : You cannot use this method for content loaded using
data:URIs,file:URIs, orloadData(), unless you specify "*" as the target origin. Doing this lets any page receive the message. - Identity risk : There is no clear way for the web content to verify the sender's identity. A message that the web page receives could have originated from your native app or another iframe.
Use this method when you need a simple, async channel for string-based data in earlier Android versions that don't support addWebMessageListener .
Use addJavascriptInterface (Legacy)
The oldest method involves injecting a native object instance directly into the WebView.
How it works : You define a Kotlin or Java class, annotate the allowed methods with @JavascriptInterface , and add an instance of the class to the WebView using addJavascriptInterface(Object, String) .
Характеристики :
- Synchronous : The JavaScript execution environment blocks until the method in your Android code returns.
- Thread safety : The system calls methods on a background thread, requiring careful synchronization on the Kotlin or Java side.
- Security risk : By default,
addJavascriptInterfaceis available to every frame within the WebView, including iframes. It lacks origin-based access control. Due to the asynchronous behavior of WebView, it isn't possible to safely determine the URL of the frame that is calling your interface. You must not rely on methods likeWebView.getUrl()for security verification, as they aren't guaranteed to be accurate and don't indicate which specific frame made the request.
Data type conversions and coercion
When using addJavascriptInterface , the Chromium-based Java Bridge converts data types between the JavaScript runtime and your Android app code.
The following coercion rules apply to method parameters and return values.
Parameter type mapping (JavaScript to Java)
When JavaScript passes arguments to an annotated Java or Kotlin method, the bridge coerces JavaScript values into the corresponding Java parameter types:
| Java parameter type | JavaScript argument value | Coercion behavior |
|---|---|---|
byte , short , int , long | Number (integer) | Values are cast to the target integer type. Out-of-bounds values wrap around according to standard numeric casting rules. |
byte , short , int , long | NaN | Coerces to 0 . |
byte , short , int , long | Infinity | Coerces to -1 for byte and short , or Integer.MAX_VALUE and Long.MAX_VALUE for int and long . |
float , double | Число | Coerces to the corresponding Java floating-point value. |
float , double | NaN / Infinity | Coerces to Float.NaN , Double.NaN , Float.POSITIVE_INFINITY , or Double.POSITIVE_INFINITY . |
char | Number (integer) | Converted to the corresponding Unicode code point. |
char | Non-integer, NaN , Infinity | Coerces to \u0000 . |
boolean | true / false | Coerces to Java true or false . |
boolean | Number, String, Object | Coerces to false (including non-empty strings and non-zero numbers). |
String | Нить | String value is preserved. |
String | Number, Boolean | Formatted as a string representation (for example, "42" , "true" , "false" ). |
String | null / undefined | null coerces to Java null ; undefined coerces to the literal string "undefined" . |
String | Object, ArrayBuffer, TypedArray | Coerces to the literal string "undefined" . |
Primitive array (such as int[] , byte[] , boolean[] ) or String[] | Множество ( [...] ) | Converts to a 1D Java array of the target element type. Sparse arrays fill unassigned indexes with default values ( 0 , false , null ). |
Primitive array (such as int[] , byte[] ) | TypedArray ( Int8Array , Uint8Array , Int32Array , Float64Array ) | Elements are coerced into the corresponding Java primitive array. |
Multi-dimensional array (such as int[][] ) | Nested array ( [[...]] ) | Not supported. Multi-dimensional array parameters evaluate to null . |
ArrayBuffer , DataView | ArrayBuffer , DataView | Not supported as arrays. ArrayBuffer and DataView instances evaluate to null . |
Object or custom class | JavaScript object ( {...} ) | Not supported. Arbitrary JavaScript object literals evaluate to null in Java. |
Object or custom class | Injected Java object wrapper | Supported (Round-tripping). Passes the underlying Java instance to the Java method. Throws a JavaScript exception if the Java type does not match the parameter signature. |
Boxed types (such as Integer , Double , Boolean ) | Number, Boolean | Not supported. Boxed primitive types are treated as opaque objects and evaluate to null . |
| Any primitive type | null / undefined | Coerces to default values ( 0 , 0.0 , \u0000 , false ). |
Object , String , array | null | Coerces to Java null . |
Return type mapping (Java to JavaScript)
When an annotated Java or Kotlin method returns a value, the bridge converts it to a JavaScript type:
| Java return type | значение JavaScript | JavaScript typeof |
|---|---|---|
boolean | true / false | "boolean" |
byte , short , int , long , float , double | Число | "number" |
char | Number (Unicode code point) | "number" |
String (non-null) | строковое значение | "string" |
String ( null ) | undefined | "undefined" |
void | undefined | "undefined" |
Java array (such as int[] , String[] ) | undefined | "undefined" . Array return values are not supported. The Java method is not executed, and undefined is returned without raising an exception. |
| Java Object / custom type (non-null) | Оболочка объекта | "object" . Creates a JavaScript wrapper around the Java instance. JavaScript code can call any public method on this object that is annotated with @JavascriptInterface . |
Java Object / custom type ( null ) | null | "object" |
Boxed primitive (such as Integer , Double ) | Оболочка объекта | "object" . Returned as an opaque Java object wrapper with no accessible @JavascriptInterface methods, making the value unusable in JavaScript. |
Method and member accessibility
The JavaScript bridge enforces strict member access and visibility rules to protect against unintended code execution:
- Fields are not exposed : Java fields (including
publicandpublic finalfields) are not accessible from JavaScript and evaluate toundefined. - Annotation requirement : Only methods explicitly annotated with
@JavascriptInterfaceare exposed to JavaScript. - Visibility restrictions : Methods must be
public.privateandprotectedmethods are never exposed to JavaScript, even if they carry the@JavascriptInterfaceannotation. - Static methods : Static methods annotated with
@JavascriptInterfaceare callable from JavaScript. - Inheritance and overriding :
@JavascriptInterfaceannotations are not inherited when a subclass overrides a method. If a subclass overrides an annotated method from a superclass, the subclass must explicitly include the@JavascriptInterfaceannotation on the overridden method to expose it to JavaScript. Non-overridden public methods inherited from a superclass remain accessible if annotated in the superclass. - Reflection protection : Standard Java reflection methods (such as
getClass()) are blocked and throw a JavaScript exception to prevent remote code execution vulnerabilities. - Method overloading : Overloaded Java methods are supported. The bridge resolves method calls based on the number of passed arguments only, and does not take argument types into account. Calling an overloaded method with an invalid argument count raises a JavaScript exception. If two overloads have the same argument count, one will be chosen arbitrarily.
Summary of mechanisms
The following table provides a quick comparison of the three primary native bridge implementation mechanisms:
| Метод | addWebMessageListener | postWebMessage | addJavascriptInterface |
|---|---|---|---|
| Выполнение | Asynchronous (Listener on main thread) | Асинхронный | Синхронный |
| Безопасность | Highest (Allowlist-based) | High (Origin aware) | Low (No origin checks) |
| Сложность | Умеренный | Умеренный | Простой |
| Направление | Двунаправленный | Двунаправленный | Web to app |
| Minimum WebView version | Version 82 (and Jetpack Webkit 1.3.0) | Version 45 (and Jetpack Webkit 1.1.0) | Все версии |
| Рекомендуется | Да | Нет | Нет |
Handle large data transfers
You must manage memory carefully when transferring large payloads, such as multi-megabyte strings or binary files, to avoid Application Not Responding (ANR) errors or crashes on 32-bit devices. This section discusses the various techniques and limitations associated with transferring significant amounts of data between the host application and web content.
Transfer binary data with byte arrays
With the WebMessageCompat class, you can send byte[] arrays directly instead of serializing binary data into Base64 strings. Since Base64 adds roughly 33% overhead to the data size, this is significantly more memory-efficient and faster.
- Binary advantage : Transfer binary data like image files or audio between your native app and web content.
- Limitation : Even with byte arrays, the system copies data across the inter-process communication (IPC) boundary between the app and the isolated process that WebView uses to render the web content. This still consumes significant memory for very large files.
The following code examples demonstrate how to set up addWebMessageListener on the native app side to receive messages marked with WebMessageCompat.TYPE_ARRAY_BUFFER and optionally reply with binary data by checking for 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
);
}
}
The following JavaScript code demonstrates the client-side implementation of addWebMessageListener , enabling the web content to send and receive binary data ( ArrayBuffer ) to and from the native app using the window.myBridge proxy injected in the previous example.
// 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]);
}
};
}
Efficient large-scale data loading
For very large files (>10 MB), use the shouldInterceptRequest method to stream data:
- The web page initiates a
fetch()call to a custom, placeholder URL. For example,https://app.local/large-file. - The Android app intercepts this request in
WebViewClient.shouldInterceptRequest. - The app returns the data as an
InputStream.
This enables streaming data in chunks rather than loading the entire payload into memory at once.
The following JavaScript function demonstrates the client-side code for efficiently loading a large binary file from the native application using a standard fetch() call to a custom, placeholder 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);
}
}
The following code examples demonstrate the native app side, using the WebViewClient.shouldInterceptRequest method in both Kotlin and Java, to stream a large binary file by intercepting a custom placeholder URL requested by the web content.
Котлин
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);
}
});
Follow security recommendations
To protect your application and user data, follow these guidelines when implementing a bridge:
Enforce HTTPS : To ensure that malicious third-party content can't invoke your application's native logic, only allow communication with secure origins.
Rely on origin rules : The best way to deal with trust is to strictly define your
allowedOriginRulesand check thesourceOriginprovided in the message callback. Avoid using the full wildcard (*), which matches all origins, as your only origin rule unless absolutely necessary. Using wildcards for subdomains (for example,*.example.com) remains valid and secure for matching multiple subdomains (for example,foo.example.com,bar.example.com).Note : While origin rules protect against malicious third-party websites and hidden iframes, they can't protect against cross-site scripting (XSS) vulnerabilities within your own trusted domain. For example, if your web page displays user-generated content and is vulnerable to stored XSS, an attacker could execute a script acting as your trusted origin. Consider applying validation to the message payloads before executing sensitive native platform operations.
Minimize surface area : Only expose the specific methods or data that the web page requires.
Check features at runtime : Recent bridge APIs, including
addWebMessageListener, are part of the Jetpack Webkit library. So, always check for support usingWebViewFeature.isFeatureSupported()before calling them.