【问题标题】:Using AAssetManager_fromJava within plugin not directly called from Java VM (called from Unity)在插件中使用 AAssetManager_fromJava 不直接从 Java VM 调用(从 Unity 调用)
【发布时间】:2019-11-21 17:05:55
【问题描述】:

我正在使用 Android NDK,需要访问资产。资产访问的要求似乎是获取 AssetManager 引用。

查看 NDK 样本 (https://github.com/android/ndk-samples),模式似乎是:

  • 当直接从 JavaVM 调用时,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* 的生命周期/所有权情况如何?)

3) AAssetManager_fromJava 还需要 jobject 和文档 (https://developer.android.com/ndk/reference/group/asset#group___asset_1gadfd6537af41577735bcaee52120127f4) 说:“请注意,调用者负责获取并持有对作业对象的 VM 引用,以防止其在本机对象被垃圾收集时被垃圾回收。正在使用。”。我似乎有一些示例只是传入一个空(本机)字符串,例如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...

}

这样合理吗?没有内存/资源泄漏问题?多线程有什么问题吗?

谢谢!

编辑:JNIEnv* 似乎有一些线程注意事项。见:Unable to get JNIEnv* value in arbitrary context

【问题讨论】:

    标签: android unity3d android-ndk java-native-interface


    【解决方案1】:

    逐点回答您的问题:

    1. 是的,Android 中可以有only one VM。您可以存储此指针或使用JNI_GetCreatedJavaVMs

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

      还要注意关于FindClass 的警告:您需要从主线程或通过您在主线程中查找的类的类加载器查找类。

    3. AAssetmanager_fromJava 的实现非常明确:传递 AssetManager 对象以外的任何东西都是未定义的行为。 This answer 展示了一种获取资产管理器的方法,另一种可能是使用对 AssetManager 对象的引用调用您自己的 JNI 函数。确保保留一个全局引用,以免它被 GCed。

    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);
    }
    

    【讨论】:

    • 非常感谢。几个后续行动:1)所以我应该在do_asset_stuff 的末尾打电话给DetachCurrentThread?我需要对JNIenv* 进行任何其他清理吗?我可以改用AttachCurrentThreadAsDaemon 吗? 2) 拥有cached_assetmanager 后,我能否在不同的线程中使用它?
    • 1) 如果您的应用程序的生命周期允许正常停止点,请致电 DetachCurrentThread。 (没有更多的工作或一些关于退出的信号)将您的线程作为守护程序附加将允许应用程序退出而不退出该线程,但这在 Android 上并不是真正的问题。 2) 是的,资产管理器本身是线程安全的。
    【解决方案2】:

    我能够从 Unity c++ 插件中读取 json 文件。 我不得不扩展 UnityPlayerActivity 以将assetManager 作为作业。 棘手的部分还在于在插件中找到资产的正确路径: 我将它放入 StreamingAssets/data 并能够使用此路径 'data/myfile' 进行读取

    查看我对代码的评论: unity answers

    看来 Botje 的回答很准确(可惜我之前没有)

    【讨论】:

    • 谢谢!是否可以看到 MyUnityPlayerActivity.java 代码?
    • 是的,我在同一unity answers的评论中附上了它
    • 只能看到原生代码?不是Java代码?但这很酷 - 我已经设法解决它而无需覆盖 UnityPlayerActivity,使用 Botje 的答案
    • 我在那里添加了 2 次评论,但也看不到它。这是它的主要部分: public class MyUnityPlayerActivity extends UnityPlayerActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); AssetManagerassetManager = getAssets();加载配置文件(资产管理器); } public static native int loadConfigFile(AssetManagerassetManager);静态 { System.loadLibrary("插件"); } }
    【解决方案3】:

    我想我会发布我最终做的事情,以防它对其他人有帮助......

    #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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-06-16
      • 1970-01-01
      • 1970-01-01
      • 2021-05-15
      相关资源
      最近更新 更多