Есть ли способ передать arraybuffer из javascript в java на Android?

Я застрял на этом деле.

У меня есть webview на Android 4.4.3, где у меня есть webapp, у которого float32array есть двоичные данные. Я хотел бы передать это array на Java Android через функцию, привязанную к JavascriptInterface. Однако, похоже, в Java я могу передавать только примитивные типы, такие как String, int и т.д.

Есть ли способ предоставить Java этот массивBuffer?

Спасибо!

Ответ 1

Хорошо, поэтому после беседы с инженерами Google и после прочтения кода я сделал следующие выводы.

Эффективность передачи двоичных данных невозможна

Невозможно эффективно передавать двоичные данные между JavaScript и Java через @JavascriptInterface:

На стороне Java:

@JavascriptInterface
void onBytes(byte[] bytes) {
   // bytes available here
}

И со стороны JavaScript:

var byteArray = new Uint8Array(buffer);
var arr = new Uint8Array(byteArray.length);
for(var i = 0; i < byteArray.length; i++) {
  arr[i] = byteArray[i];
}
javaObject.onBytes(arr);

В приведенном выше коде (из моего старого ответа) и в Alex - преобразование, выполняемое для массива, является жестоким:

case JavaType::TypeArray:
  if (value->IsType(base::Value::Type::DICTIONARY)) {
    result.l = CoerceJavaScriptDictionaryToArray(
        env, value, target_type, object_refs, error);
  } else if (value->IsType(base::Value::Type::LIST)) {
    result.l = CoerceJavaScriptListToArray(
        env, value, target_type, object_refs, error);
  } else {
    result.l = NULL;
  }
  break;

Что, в свою очередь, принуждает каждый элемент массива к объекту Java:

for (jsize i = 0; i < length; ++i) {
    const base::Value* value_element = null_value.get();
    list_value->Get(i, &value_element);
    jvalue element = CoerceJavaScriptValueToJavaValue(
        env, value_element, target_inner_type, false, object_refs, error);
    SetArrayElement(env, result, target_inner_type, i, element);

Итак, для 1024 * 1024 * 10 Uint8Array - десять миллионов Java-объектов создаются и уничтожаются на каждом проходе, что приводит к 10-секундному времени процессора на моем эмуляторе.

Создание HTTP-сервера

Мы попытались создать HTTP-сервер и POST получить результат с помощью XMLHttpRequest. Это сработало - но в итоге стоило около 200 мс латентности, а также ввело неприятную утечку памяти.

Каналы сообщений медленны

Android API 23 добавила поддержку MessageChannel s, которую можно использовать с помощью createWebMessageChannel(), как показано в этом ответе. Это очень медленно, все еще сериализуется с GIN (например, методом @JavascriptInterface) и наступает дополнительная латентность. Я не смог заставить это работать с разумной производительностью.

Стоит отметить, что Google сказал, что они считают, что это путь вперед и надеется продвинуть каналы сообщений через @JavascriptInterface в какой-то момент.

Передача строки работает

После прочтения кода преобразования - можно видеть (и это было подтверждено Google), что единственный способ избежать многих преобразований - передать значение String. Это происходит только через:

case JavaType::TypeString: {
  std::string string_result;
  value->GetAsString(&string_result);
  result.l = ConvertUTF8ToJavaString(env, string_result).Release();
  break;
}

Что преобразует результат один раз в UTF8, а затем снова в строку Java. Это по-прежнему означает, что данные (в этом случае 10 МБ) копируются три раза, но можно передавать 10 МБ данных только в "только" 60 мс, что намного более разумно, чем 10 секунд, которые принимает метод массива.

Petka придумал использовать 8859 кодировку, которая может конвертировать один байт в одну букву. К сожалению, он не поддерживается в JavaScript TextDecoder API - так Windows-1252, который является еще одним 1 байтовым кодированием.

На стороне JavaScript можно сделать:

var a = new Uint8Array(1024 * 1024 * 10); // your buffer
var b = a.buffer
// actually windows-1252 - but called iso-8859 in TextDecoder
var e = new TextDecoder("iso-8859-1"); 
var dec = e.decode(b);
proxy.onBytes(dec); // this is in the Java side.

Затем на стороне Java:

@JavascriptInterface
public void onBytes(String dec) throws UnsupportedEncodingException
    byte[] bytes = dec.getBytes("windows-1252");
    // work with bytes here
}

который работает примерно через 1/8 времени прямой сериализации. Он все еще не очень быстрый (поскольку строка заполняется до 16 бит вместо 8, затем через UTF8, а затем снова в UTF16). Тем не менее, он работает на разумной скорости по сравнению с альтернативой.

После того, как вы поговорите с соответствующими сторонами, которые поддерживают этот код, они сказали мне, что он так хорош, насколько это возможно, с текущим API. Мне сказали, что я первый человек, который попросит об этом (быстрая сериализация Java на Java).

Ответ 2

Это довольно просто

Раздел Init

 JavaScriptInterface jsInterface = new JavaScriptInterface(this);
 webView.getSettings().setJavaScriptEnabled(true);
 webView.addJavascriptInterface(jsInterface, "JSInterface");

JavaScriptInterface

public class JavaScriptInterface {
        private Activity activity;

        public JavaScriptInterface(Activity activiy) {
            this.activity = activiy;
        }
        @JavascriptInterface
        public void putData(byte[] bytes){
            //do whatever
        }
    }

Js section

<script>
  function putAnyBinaryArray(arr) {
        var uint8 = Uint8Array.from(arr);
        window.JSInterface.putData(uint8);
  };
</script>

TypedArray.from polyfill при необходимости: https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/from

Ответ 3

Сериализуйте свои данные в строку, а затем несериализуйте в своем приложении.

Ответ 4

Клонирование массива ArrayBuffer заставляет его работать - что-то о TypedArray, поддерживаемом ArrayBuffer, не очень хорошо работает в Android.

Если вы скопируете ArrayBuffer в новый TypedArray, вы можете избежать дорогостоящих издержек сериализации.

У читателя:

@JavascriptInterface
void onBytes(byte[] bytes) {
   // bytes available here
}

И на стороне JS:

var byteArray = new Uint8Array(buffer);
var arr = new Uint8Array(byteArray.length);
for(var i = 0; i < byteArray.length; i++) {
  arr[i] = byteArray[i];
}
javaObject.onBytes(arr);

Прекрасно работает:)

Ответ 5

Если вы хотите синхронизировать вызов, просто используйте base64 encode & decode: (Преобразовать строку base64 в ArrayBuffer)

@JavascriptInterface
void onBytes(String base64) {
    // decode here
}

Если вы хотите асинхронный вызов:

Вы можете создать http-сервер в приложении Android, а затем использовать "xhr" или "fetch" на стороне javascript для отправки двоичного или строкового асинхронного кода.


И не используйте "iso-8859-1" или "windows-1252", упомянутые выше, это опасно !!!
"iso-8859-1" имеет неопределенный код, который не может быть декодирован между javascript и java. (https://en.wikipedia.org/wiki/ISO/IEC_8859-1)