动态库中使用 C++ 直接将 ARGB 数据写入 App 的 Java int 数组中

需求

我们的 YUV 超级夜景算法的输出是 YUV 数据,客户由于要在结果图上绘制水印,需要我们给他们输出 ARGB_8888 的数据。

分析

YUV 数据在 Java 中可以直接通过公式计算转换为 ARGB 数据,但是由于用了多个循环,效率会比较低,在客户的平台 Qualcomm MSM6125 上 12M 的图大概需要用 400ms。400ms 对于 2.5s 的算法耗时来说算得上庞然大物了。所以得想一下其他思路来解决这个问题。

我们内部有很多 HPC 团队写的图形格式转换的高性能代码,速度很快,所以自然想到能不能在这个方向找一下解决方案。最简单的方案当然是像 YUV 数据一样,在 SDK 中利用高性能代码将 YUV 数据转换为 ARGB 格式,然后直接将 ARGB 格式的数据写入到客户传进来的 buffer 中。

实现

当输出数据格式为 YUV 时,流程是这样的:

  1. 客户在 Java 代码中分配符合 YUV 大小的 byte 数组给我们写入输出图数据。

    1
    2
    int dataSize = width * height * 3 / 2;
    byte[] outData = new byte[dataSize];
  2. 在 JNI 代码中将 byte 数组转换为 char 数组,由于 Java 中的 byte 和 C++ 中的 char 都是 1 字节,所以我们这里转换后数据是一一对应的。

    1
    2
    unsigned char *image_data = reinterpret_cast<unsigned char *>(
    env->GetByteArrayElements(outYUVData, 0));
  3. SDK 中直接将输出图的 YUV 数据写入 char 数组中。

  4. 客户在 Java 代码中处理格式为 byte[] 的 YUV 数据,比如直接用来创建 YuvImage。

当输出图为 ARGB 格式时,首先想到的是分配如下大小的 byte 数组来存储 ARGB 格式的输出图数据。

1
2
int dataSize = width * height * 4;
byte[] outData = new byte[dataSize];

但是 Bitmap 需要 int[] 的数据来创建,客户现有接口也是直接操作 int[]。

虽然有了 byte[width * height * 4] 的输出图数据后,可以把 4(ARGB) 个 8 位的 byte 放在 1 个 32 位的 int 里,把他们转换为 int[width * height]。但是这样又多了一些操作和一部分 Java 内存的使用。

那么能不能直接在 C++ 中就把我们的 ARGB 数据放在 Java 的 int 数组里呢?

答案是可以的。

来看一下当输出数据格式为 ARGB 时的流程:

  1. 客户在 Java 代码中分配 int 数组给我们写入输出图。由于 int 是 32 位的,所以这里数组的大小只需要为 width * height,内存大小等价于 new byte[width * height * 4];

    1
    2
    int dataSize = width * height;
    int[] outData = new int[dataSize];
  2. 在 JNI 代码中将 int 数组转换为 char 数组,由于在 Java 中分配的数组内存地址是连续的,所以我们这里直接将 int 数组转换为 char 数组来使用,即 char 数组中每 4 个值组成了 int 数组中的 1 个值。这样就做到了直接将每一个 ARGB 数据都放在 int 中。

    1
    2
    jint *image_data_int = env->GetIntArrayElements(outARGBData, 0);
    unsigned char *image_data = (unsigned char *) image_data_int;
  3. SDK 中将输出图的 YUV 数据利用 Neon 代码转换为 BGRA 数据,并写入到 char 数组中。

  4. 客户在 Java 代码中处理格式为 int[] 的 ARGB_8888 数据,比如直接用来创建 Bitmap。

    1
    2
    Bitmap bitmap = Bitmap.createBitmap(argbData, 0, width, width, height,
    Bitmap.Config.ARGB_8888);

整个过程和前面直接写入到保存 YUV 的 byte[] 中差不太多,关键点是:

  • 根据数据在地址中的表现来将 Java int[] 和 C++ char * 进行转换。
  • 利用速度更快的 Neon 代码来做到更快的速度。

上面的过程有一点需要单独讲一讲,Java 中需要的是 ARGB 格式的数据,但我们在 SDK 中是将 YUV 转换为 BGRA 格式并写入到 buffer 中,而不是转换为 ARGB,这是为什么呢?

这一点同样和数据在地址中的表现有关,这里我们只看输出图的前 4 个字节(即一个 ARGB 像素),分别在 C++ 和 Java 中将前 4 个字节打印出来。

1
LOGE("image_data[0~3] c++ hexadecimal: %x%x%x%x ", image_data[0], image_data[1], image_data[2], image_data[3]);
1
System.out.printf("outData[0] java hexadecimal %08x\n", outData[0]);

打印的结果为:

1
2
image_data[0~3] c++ hexadecimal: 7c8080ff
outData[0] java hexadecimal ff80807c

可以看到前四个字节的数据在 C++ 中和 Java 中的十六进制表示反过来了, 7c8080ff 变成了 ff80807c,这是为什么呢?这是因为在客户的平台上(或者说在 Android 中,未考证) Java 中 int 型的字节顺序为小端法

所以这也就是为什么 Java 中需要的是 ARGB,但我们在 C++ 中往内存中写入的是 BGRA 数据,因为到了 Java 中数据自然就变成了 ARGB。

性能节省

通过使用 Neon 代码将 YUV 转换为 ARGB,在客户平台上耗时仅为 20ms~30ms,相比在 Java 上用 for 循环来做节省了 400ms,满足客户需求的同时对性能的影响几乎可以忽略。