API 26 级以下 NDK 和 SDK 之间的共享内存

Shared memory between NDK and SDK below API Level 26

用 c++ 编写的库会产生连续的数据流,并且必须在不同的平台上进行移植。现在将库集成到 android 应用程序,我正在尝试在 NDK 和 SDK 之间创建共享内存。

下面是工作片段,

本地代码:

#include <jni.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <linux/ashmem.h>
#include <android/log.h>
#include <string>

char  *buffer;
constexpr size_t BufferSize=100;
extern "C" JNIEXPORT jobject JNICALL
Java_test_com_myapplication_MainActivity_getSharedBufferJNI(
        JNIEnv* env,
        jobject /* this */) {

    int fd = open("/dev/ashmem", O_RDWR);

    ioctl(fd, ASHMEM_SET_NAME, "shared_memory");
    ioctl(fd, ASHMEM_SET_SIZE, BufferSize);

    buffer = (char*) mmap(NULL, BufferSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

    return (env->NewDirectByteBuffer(buffer, BufferSize));
}

extern "C" JNIEXPORT void JNICALL
Java_test_com_myapplication_MainActivity_TestBufferCopy(
        JNIEnv* env,
        jobject /* this */) {

   for(size_t i=0;i<BufferSize;i = i+2) {
       __android_log_print(ANDROID_LOG_INFO, "native_log", "Count %d value:%d", i,buffer[i]);
   }

   //pass `buffer` to dynamically loaded library to update share memory
   //

}

SDK代码:

//MainActivity.java
public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.

    static {
        System.loadLibrary("native-lib");
    }

    final int BufferSize = 100;
    @RequiresApi(api = Build.VERSION_CODES.Q)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ByteBuffer byteBuffer = getSharedBufferJNI();

        //update the command to shared memory here
        //byteBuffer updated with commands
        //Call JNI to inform update and get the response
        TestBufferCopy();
    }


    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native ByteBuffer getSharedBufferJNI();
    public native int TestBufferCopy();
}

问题:

  1. Accessing primitive arrays 从 Java 到 native 仅在垃圾收集器支持固定时作为参考。其他方式是否正确?
  2. android 平台是否保证 ALWAYS 引用从 NDK 共享到 SDK 而没有冗余副本?
  3. 共享内存的方式正确吗?

让我总结一下我的发现,

Accessing primitive arrays from Java to native is reference only if garbage collector supports pinning. Is it true for other way around ?

直接缓冲区的内容可能驻留在普通垃圾收集堆之外的本机内存中。因此垃圾收集器不能索取内存。

Is it guaranteed by android platform that ALWAYS reference is shared from NDK to SDK without redundant copy?

是的,根据 NewDirectByteBuffer 的文档。

jobject NewDirectByteBuffer(JNIEnv* env, void* address, jlong capacity);

分配并 returns 直接 java.nio.ByteBuffer 引用从内存地址 address 开始并扩展 capacity 字节的内存块。

您只需要 /dev/ashmem 即可在进程之间共享内存。 NDK 和 SDK (Java/Kotlin) 在相同的 Linux 进程中工作并且可以完全访问相同的内存 space.

定义可在 C++ 和 Java 中使用的内存的常用方法是创建直接字节缓冲区。您不需要 JNI,Java API 具有您在问题中使用的 ByteBuffer.allocateDirect(int capacity). If it's more natural for your logical flow to allocate the buffer on the C++ side, JNI has the NewDirectByteBuffer(JNIEnv* env, void* address, jlong capacity) 函数。

在 C++ 端使用 Direct ByteBuffer 非常容易,但在 JVM 端效率不高。原因是这个缓冲区没有数组支持,你唯一的 API 涉及缓冲区中的 ByteBuffer.get() with typed variations (getting byte array, char, int, …). You have control of current position,但以这种方式工作需要一定的纪律:每个 get() 操作更新当前位置。此外,对该缓冲区的随机访问相当慢,因为它涉及调用定位和获取 APIs。因此,在某些非平凡数据结构的情况下,用 C++ 编写自定义访问代码并通过 JNI 调用 'intelligent' getter 可能更容易。

重要的是不要忘记设置 ByteBuffer.order(ByteOrder.nativeOrder())。新创建的字节缓冲区的顺序是违反直觉的 BIG_ENDIAN。这适用于从 Java 和 C++ 创建的缓冲区。

如果您可以在 C++ 需要访问此类共享内存时隔离实例,并且不需要一直固定它,则值得考虑使用字节数组。在 Java 中,您拥有更高效的随机访问。在 NDK 端,您将调用 GetByteArrayElements() or GetPrimitiveArrayCritical()。后者效率更高,但它的使用对在释放数组之前可以调用的 Java 函数施加了限制。在 Android 上,这两种方法都不涉及内存分配和复制(尽管没有官方保证)。即使 C++ 端使用与 Java 相同的内存,您的 JNI 代码也必须调用适当的 Release…() 函数,最好尽早调用。通过 RAII 处理这个 Get/Release 是一个很好的做法。