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();
}
问题:
- Accessing primitive arrays 从 Java 到 native 仅在垃圾收集器支持固定时作为参考。其他方式是否正确?
- android 平台是否保证 ALWAYS 引用从 NDK 共享到 SDK 而没有冗余副本?
- 共享内存的方式正确吗?
让我总结一下我的发现,
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 是一个很好的做法。
用 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();
}
问题:
- Accessing primitive arrays 从 Java 到 native 仅在垃圾收集器支持固定时作为参考。其他方式是否正确?
- android 平台是否保证 ALWAYS 引用从 NDK 共享到 SDK 而没有冗余副本?
- 共享内存的方式正确吗?
让我总结一下我的发现,
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 是一个很好的做法。