如何在Android上从JavaScript传递ArrayBuffer到Java?

17

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

我在 Android 4.4.3 上有一个 webview,其中有一个包含二进制数据的 float32array 的 webapp。我想通过一个使用 JavascriptInterface 绑定的函数将该 array 传递给 Java Android。 然而,在 Java 中,似乎我只能传递像 Stringint 等基本类型。

是否有一种方法可以把这个 arrayBuffer 传递给 Java 呢?

谢谢!


你能在服务器上创建数组,然后让你的Android应用程序通过HTTP请求它吗? - Code-Apprentice
7个回答

24

好的,经过与Google工程师的交流和阅读代码后,我得出了以下结论。

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

通过@JavascriptInterface在JavaScript和Java之间高效地传递二进制数据是不可能的:

在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 * 10Uint8Array - 每次通过创建和销毁1000万个Java对象,在我的模拟器上需要10秒的CPU时间。

创建HTTP服务器

我们尝试过的一件事是创建一个HTTP服务器,并通过XMLHttpRequest将结果POST到它。这个方法可以实现 - 但最终会造成大约200ms的延迟并引入一个nasty memory leak

MessageChannels很慢

Android API 23添加了对MessageChannel的支持,可以通过createWebMessageChannel()使用,如this answer所示。这非常慢,仍然与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)被复制了三次,但是可以在“仅”60ms内传递10MB的数据,这比上述数组方法需要的10秒要合理得多。

Petka提出了使用8859编码的想法,它可以将单个字节转换为单个字母。不幸的是,它不受JavaScript的TextDecoder API支持,因此可以使用另一种1字节编码Windows-1252代替。

在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序列化的人。


我想感谢CommonWare、Petka和Chromium的Tarbo在帮助和指导我解决这个问题时所提供的帮助 :) - Benjamin Gruenbaum
它们在JavaScript和Java之间的解码非常顺畅,而且这是唯一具有合理性能的解决方案,它比base64编码/解码快一个数量级。 - Benjamin Gruenbaum
1
我确实检查了每个可能的字节值(并将其运送到生产环境,在那里它每天移动数百TB,一年前)。我不认为JavaScript编码API实际上执行x-user-defined,这可能是有趣的检查。 - Benjamin Gruenbaum
1
本,这个修复太棒了。你真是个牛人。 - John Lanzivision
@tangobravo,你能提供一个Java到Javascript的用例示例吗?即如何使用shouldInterceptRequest将ArrayBuffer从Android传递到Javascript。 - Avner Moshkovitz
显示剩余9条评论

4

很简单

初始化部分

 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部分

<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


所以,事实证明这并不是真正的有效方法,如果您尝试传递超过10mb的数据,它会导致Chrome进程崩溃,并且传递10mb需要10秒钟(传递1mb需要1秒钟)。 - Benjamin Gruenbaum
看起来它正在进行字符串转换或者其他缓慢的操作 :( 如果我有一个10mb的数组,传递需要超过10秒,这太荒谬了,因为下载所需的时间比这还要短。 - Benjamin Gruenbaum
1
我已经修复了,这周晚些时候我会发布一个可行的答案。 - Benjamin Gruenbaum
@BenjaminGruenbaum 我期待着你的帖子 :) - Alex Nikulin
@BenjaminGruenbaum 我期待着你的帖子 :) - Brandon Ros
显示剩余4条评论

3

将您的数据序列化为一个字符串,然后在您的应用程序中反序列化。


似乎Cordova也是这样做的。 - ShrekOverflow

2
复制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);

完全正常运作 :)

1

如果你想进行同步调用,只需使用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)

使用iso-8859-1/windows-1252是一个不好的选择,但x-user-defined可以工作。 你比较过这个和base64的性能吗? - Nuno Cruces

0
在我的情况下,应用程序在没有HTTP服务器的情况下相互传输Blob数据,因此只能将ArrayBuffer发送到Java接口,如下所示:
//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,在异步任务处理后弹出提示信息!


0

第二个答案中链接的代码 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)

所以我肯定可以想象它复制字符串更快。 然而,没有理由它不能传递一个不需要复制或者至少只需要一次原始复制的类型数组。

这需要对 Chromium 进行补丁。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接