有没有办法将数组缓冲区从javascript传递到Android上的java

Is there a way to pass an arraybuffer from javascript to java on Android?

本文关键字:Android java 上的 javascript 数组 缓冲区 有没有      更新时间:2023-09-26

我在这个案子上卡了一会儿。

我在Android 4.4.3上有一个网络视图,其中我有一个包含二进制数据的float32array的网络应用程序。我想通过与JavascriptInterface绑定的函数将该array传递给Java Android。但是,似乎在 Java 中,我只能传递原始类型,如 Stringint 等......

有没有办法给Java这个arrayBuffer?

谢谢!

好的

,所以在与谷歌工程部门聊天并阅读代码后,我得出了以下结论。

有效地传递二进制数据是不可能的

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 - 每次传递都会创建和销毁 1000 万个 Java 对象,从而导致模拟器上的 CPU 时间达到 10 秒。

创建 HTTP 服务器

我们尝试的一件事是创建一个HTTP服务器并通过XMLHttpRequest将结果POST给它。这有效 - 但最终花费了大约 200 毫秒的延迟,并且还引入了令人讨厌的内存泄漏。

消息通道很慢

Android API 23 增加了对 MessageChannel s 的支持,可以通过createWebMessageChannel()使用,如本答案所示。这非常慢,仍然使用 GIN 序列化(如 @JavascriptInterface 方法)并产生额外的延迟。我无法以合理的性能使其工作。

值得一提的是,谷歌表示,他们相信这是前进的方向,并希望在某个时候推广消息渠道超过@JavascriptInterface

传递字符串有效

阅读转换代码后 - 可以看到(谷歌证实了这一点)避免多次转化的唯一方法是传递String值。这只能通过:

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

它将结果一次转换为 UTF8,然后再次转换为 Java 字符串。这仍然意味着数据(在本例中为 10MB)被复制三次 - 但可以在"仅"60 毫秒内传递 10MB 的数据 - 这比上述数组方法花费的 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 一样好。有人告诉我我是第一个提出这个要求的人(快速JavaScript到Java序列化)。

这很简单

初始化部分

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

JavaScript接口

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

Js 部分

<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

将数据

序列化为字符串,然后在应用中取消序列化。

克隆 ArrayBuffer 可以工作 - 使用 ArrayBuffer 支持的 TypedArray 并不能很好地编入 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);

工作得很好:)

如果你想要同步调用,只需使用 base64 编码和解码:(将 base64 字符串转换为 ArrayBuffer)

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

如果需要异步调用:

您可以在 Android 应用程序中创建 http 服务器,然后在 javascript 端使用"xhr"或"fetch"来发送二进制或字符串异步


并且不要使用上面提到的"iso-8859-1"或"windows-1252",这是危险的!!
"ISO-8859-1"有未定义的代码,无法在JavaScript和Java之间解码。(https://en.wikipedia.org/wiki/ISO/IEC_8859-1)

第二个答案中链接的代码 https://source.chromium.org/chromium/chromium/src/+/master:content/browser/android/java/gin_java_script_to_java_types_coercion.cc;l=628?q=gin_java_scr&ss=chromium

实际上并不理解TypedArrays(看起来确实如此,因为它说TypeArray,但是该文件中的所有内容都是TypeXZY)

所以我可以肯定地想象复制字符串更快。但是,没有理由说它不应该能够在不复制的情况下传递类型化数组,或者至少只通过单个原始副本。

不过,这需要对铬进行修补。

我而言,应用程序在没有 http 服务器的情况下相互传输 blob 数据,因此别无选择,只能将 arrayBuffer 发送到 javainterface,如下所示:

//javascript
if ((window as any)?.JsBridge?.downloadFile) {
      file.arrayBuffer().then(arr => {
        (window as any)?.JsBridge?.downloadFile(new Uint8Array(arr), filename)
      })
}
//java
@JavascriptInterface
public void downloadFile(byte[] bytes, String filename) {
    NativeApi.log("downloadFile", filename + "," + bytes.length);
}

顺便说一下,文件限制为 10M,在 asyncTask 处理后烤一些东西!