在未直接从 Java VM 调用的插件中使用 AAssetManager_fromJava(从 Unity 调用)
Using AAssetManager_fromJava within plugin not directly called from Java VM (called from Unity)
我正在使用 Android NDK 并且需要访问资产。资产访问的要求似乎是获取 AssetManager 引用。
查看NDK示例(https://github.com/android/ndk-samples),模式似乎是:
当直接从 JavaVM 调用时,- A
JNIEnv*
与一些 jobject
一起被传递到 func
- 用这些得到
AAssetManager*
然后用这个打开资产
这看起来很简单,除了在我的例子中,函数是从 Unity 调用的,所以我无法访问 JNIEnv*
或 jobject
。获得 JNIEnv*
似乎很容易,因为我可以利用 JNI_OnLoad
访问 JavaVM*
,然后使用它通过 vm->GetEnv
获得 JNIEnv*
。我的问题是:
1) 我的理解是,一个Android 应用程序只能有一个Java VM 实例。我可以安全地将 JavaVM*
传递给 JNI_OnLoad
并将其保存以供其他函数调用使用吗?
2) JNIEnv*
呢?我可以在 JNI_OnLoad
期间获取一次并保存它,还是每次我需要在函数中使用资产时都应该获取一个新的? JNIEnv*
是我需要明确释放的东西吗? (即 JNIEnv*
的 lifetime/ownership 情况是什么?)
3) AAssetManager_fromJava 还需要一个 jobject
,文档 (https://developer.android.com/ndk/reference/group/asset#group___asset_1gadfd6537af41577735bcaee52120127f4) 说:"Note that the caller is responsible for obtaining and holding a VM reference to the jobject to prevent its being garbage collected while the native object is in use."。我似乎有一些示例只是传递一个空(本机)字符串,如 AAssetManager_fromJava(env, "");
- 这样可以吗?我只会在那个调用的生命周期内使用 AssetManager,而且每次我都可以获得一个新的。 (同样,AAssetManager*
是我需要管理的资源,还是我只是获得对其他地方拥有的东西的引用?文档似乎暗示后者。)
4) 综上所述,我可能会这样做:
JavaVM* g_vm;
JNIEnv* g_env;
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
g_vm = vm;
g_vm->GetEnv((void **)&g_env, JNI_VERSION_1_6); // TODO: error checking
return JNI_VERSION_1_6;
}
void do_asset_stuff() {
AAssetManager* mgr = AAssetManager_fromJava(g_env, "");
// do stuff...
}
这样合理吗?没有 memory/resource 泄漏问题?多线程有什么问题吗?
谢谢!
编辑:似乎 JNIEnv*
有一些线程注意事项。参见:Unable to get JNIEnv* value in arbitrary context
对您问题的逐点回答:
是的,可以有only one VM in Android. You are allowed to store this pointer or use JNI_GetCreatedJavaVMs
.
JNIEnv
指针与创建它们的线程紧密耦合。在您的情况下,您首先必须 attach the thread to the VM using AttachCurrentThread
。这将为您填写一个JNIEnv *
。完成后别忘了DetachCurrentThread
。
还要注意关于 FindClass
的警告:您需要从主线程或通过您查找的 class 的 classloader 查找 classes在主线程中。
AAssetmanager_fromJava
is pretty clear: passing it anything other than an AssetManager
object is undefined behavior. This answer 的实现展示了获取资产管理器的一种方法,另一种可能是通过引用 AssetManager 对象来调用您自己的 JNI 函数。确保保留一个全局引用,这样它就不会被 GC。
鉴于以上,它可能看起来更像这样:
JavaVM* g_vm;
jobject cached_assetmanager;
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
g_vm = vm;
return JNI_VERSION_1_6;
}
void do_asset_stuff() {
JNIEnv *env;
JavaVMAttachArgs args = { JNI_VERSION_1_6, "my cool thread", NULL };
g_vm->AttachCurrentThread((void **)&env, &args);
AAssetManager* mgr = AAssetManager_fromJava(g_env, cached_assetmanager);
// do stuff...
}
// Assuming you call `com.shhhsecret.app.storeassetmanager(mgr)` somewhere.
void Java_com_shhhsecret_app_storeassetmanager(JNIEnv *env, jclass cls, jobject am) {
cached_assetmanager = env->NewGlobalRef(am);
}
我能够从 Unity c++ 插件读取 json 文件。
我必须扩展 UnityPlayerActivity 才能将 assetManager 作为 jobject。
棘手的部分还在于找到插件中资产的正确路径:
我将它放入 StreamingAssets/data 并能够使用此路径读取 'data/myfile'
查看我对代码的评论:
unity answers
看来Botje的回答很准确(可惜我没有早点知道)
以为我会 post 我最终做了什么,以防对其他人有帮助...
#include <jni.h>
#include <android/asset_manager.h>
#include <android/asset_manager_jni.h>
JavaVM* g_JavaVM;
jobject g_JavaAssetManager;
bool g_Initialized = false;
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
g_JavaVM = vm;
return JNI_VERSION_1_6;
}
// call this once from the main thread in C# land:
extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API NativeInit() {
if (g_Initialized) { return; }
g_Initialized = true;
JNIEnv* env = nullptr;
jint get_env_result = g_JavaVM->GetEnv((void **)&env, JNI_VERSION_1_6);
if (get_env_result == JNI_EDETACHED) {
jint attach_thread_result = g_JavaVM->AttachCurrentThreadAsDaemon(&env, nullptr);
if (attach_thread_result != 0) {
return;
}
get_env_result = JNI_OK;
}
if (env == nullptr || get_env_result != JNI_OK) {
return;
}
jclass unity_player = env->FindClass("com/unity3d/player/UnityPlayer");
jfieldID static_activity_id = env->GetStaticFieldID(unity_player, "currentActivity","Landroid/app/Activity;");
jobject unity_activity = env->GetStaticObjectField(unity_player, static_activity_id);
jmethodID get_assets_id = env->GetMethodID(env->GetObjectClass(unity_activity), "getAssets", "()Landroid/content/res/AssetManager;");
jobject java_asset_manager = env->CallObjectMethod(unity_activity, get_assets_id);
g_JavaAssetManager = env->NewGlobalRef(java_asset_manager);
}
现在g_JavaAssetManager
可以在任意线程调用AAssetManager_fromJava
。
我正在使用 Android NDK 并且需要访问资产。资产访问的要求似乎是获取 AssetManager 引用。
查看NDK示例(https://github.com/android/ndk-samples),模式似乎是:
-
当直接从 JavaVM 调用时,
- A
JNIEnv*
与一些jobject
一起被传递到 func
- 用这些得到
AAssetManager*
然后用这个打开资产
这看起来很简单,除了在我的例子中,函数是从 Unity 调用的,所以我无法访问 JNIEnv*
或 jobject
。获得 JNIEnv*
似乎很容易,因为我可以利用 JNI_OnLoad
访问 JavaVM*
,然后使用它通过 vm->GetEnv
获得 JNIEnv*
。我的问题是:
1) 我的理解是,一个Android 应用程序只能有一个Java VM 实例。我可以安全地将 JavaVM*
传递给 JNI_OnLoad
并将其保存以供其他函数调用使用吗?
2) JNIEnv*
呢?我可以在 JNI_OnLoad
期间获取一次并保存它,还是每次我需要在函数中使用资产时都应该获取一个新的? JNIEnv*
是我需要明确释放的东西吗? (即 JNIEnv*
的 lifetime/ownership 情况是什么?)
3) AAssetManager_fromJava 还需要一个 jobject
,文档 (https://developer.android.com/ndk/reference/group/asset#group___asset_1gadfd6537af41577735bcaee52120127f4) 说:"Note that the caller is responsible for obtaining and holding a VM reference to the jobject to prevent its being garbage collected while the native object is in use."。我似乎有一些示例只是传递一个空(本机)字符串,如 AAssetManager_fromJava(env, "");
- 这样可以吗?我只会在那个调用的生命周期内使用 AssetManager,而且每次我都可以获得一个新的。 (同样,AAssetManager*
是我需要管理的资源,还是我只是获得对其他地方拥有的东西的引用?文档似乎暗示后者。)
4) 综上所述,我可能会这样做:
JavaVM* g_vm;
JNIEnv* g_env;
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
g_vm = vm;
g_vm->GetEnv((void **)&g_env, JNI_VERSION_1_6); // TODO: error checking
return JNI_VERSION_1_6;
}
void do_asset_stuff() {
AAssetManager* mgr = AAssetManager_fromJava(g_env, "");
// do stuff...
}
这样合理吗?没有 memory/resource 泄漏问题?多线程有什么问题吗?
谢谢!
编辑:似乎 JNIEnv*
有一些线程注意事项。参见:Unable to get JNIEnv* value in arbitrary context
对您问题的逐点回答:
是的,可以有only one VM in Android. You are allowed to store this pointer or use
JNI_GetCreatedJavaVMs
.JNIEnv
指针与创建它们的线程紧密耦合。在您的情况下,您首先必须 attach the thread to the VM usingAttachCurrentThread
。这将为您填写一个JNIEnv *
。完成后别忘了DetachCurrentThread
。还要注意关于
FindClass
的警告:您需要从主线程或通过您查找的 class 的 classloader 查找 classes在主线程中。AAssetmanager_fromJava
is pretty clear: passing it anything other than anAssetManager
object is undefined behavior. This answer 的实现展示了获取资产管理器的一种方法,另一种可能是通过引用 AssetManager 对象来调用您自己的 JNI 函数。确保保留一个全局引用,这样它就不会被 GC。鉴于以上,它可能看起来更像这样:
JavaVM* g_vm;
jobject cached_assetmanager;
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
g_vm = vm;
return JNI_VERSION_1_6;
}
void do_asset_stuff() {
JNIEnv *env;
JavaVMAttachArgs args = { JNI_VERSION_1_6, "my cool thread", NULL };
g_vm->AttachCurrentThread((void **)&env, &args);
AAssetManager* mgr = AAssetManager_fromJava(g_env, cached_assetmanager);
// do stuff...
}
// Assuming you call `com.shhhsecret.app.storeassetmanager(mgr)` somewhere.
void Java_com_shhhsecret_app_storeassetmanager(JNIEnv *env, jclass cls, jobject am) {
cached_assetmanager = env->NewGlobalRef(am);
}
我能够从 Unity c++ 插件读取 json 文件。 我必须扩展 UnityPlayerActivity 才能将 assetManager 作为 jobject。 棘手的部分还在于找到插件中资产的正确路径: 我将它放入 StreamingAssets/data 并能够使用此路径读取 'data/myfile'
查看我对代码的评论: unity answers
看来Botje的回答很准确(可惜我没有早点知道)
以为我会 post 我最终做了什么,以防对其他人有帮助...
#include <jni.h>
#include <android/asset_manager.h>
#include <android/asset_manager_jni.h>
JavaVM* g_JavaVM;
jobject g_JavaAssetManager;
bool g_Initialized = false;
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
g_JavaVM = vm;
return JNI_VERSION_1_6;
}
// call this once from the main thread in C# land:
extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API NativeInit() {
if (g_Initialized) { return; }
g_Initialized = true;
JNIEnv* env = nullptr;
jint get_env_result = g_JavaVM->GetEnv((void **)&env, JNI_VERSION_1_6);
if (get_env_result == JNI_EDETACHED) {
jint attach_thread_result = g_JavaVM->AttachCurrentThreadAsDaemon(&env, nullptr);
if (attach_thread_result != 0) {
return;
}
get_env_result = JNI_OK;
}
if (env == nullptr || get_env_result != JNI_OK) {
return;
}
jclass unity_player = env->FindClass("com/unity3d/player/UnityPlayer");
jfieldID static_activity_id = env->GetStaticFieldID(unity_player, "currentActivity","Landroid/app/Activity;");
jobject unity_activity = env->GetStaticObjectField(unity_player, static_activity_id);
jmethodID get_assets_id = env->GetMethodID(env->GetObjectClass(unity_activity), "getAssets", "()Landroid/content/res/AssetManager;");
jobject java_asset_manager = env->CallObjectMethod(unity_activity, get_assets_id);
g_JavaAssetManager = env->NewGlobalRef(java_asset_manager);
}
现在g_JavaAssetManager
可以在任意线程调用AAssetManager_fromJava
。