在未直接从 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),模式似乎是:

这看起来很简单,除了在我的例子中,函数是从 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

对您问题的逐点回答:

  1. 是的,可以有only one VM in Android. You are allowed to store this pointer or use JNI_GetCreatedJavaVMs.

  2. JNIEnv 指针与创建它们的线程紧密耦合。在您的情况下,您首先必须 attach the thread to the VM using AttachCurrentThread。这将为您填写一个JNIEnv *。完成后别忘了DetachCurrentThread

    还要注意关于 FindClass 的警告:您需要从主线程或通过您查找的 class 的 classloader 查找 classes在主线程中。

  3. AAssetmanager_fromJava is pretty clear: passing it anything other than an AssetManager object is undefined behavior. This answer 的实现展示了获取资产管理器的一种方法,另一种可能是通过引用 AssetManager 对象来调用您自己的 JNI 函数。确保保留一个全局引用,这样它就不会被 GC。

  4. 鉴于以上,它可能看起来更像这样:

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