Search code examples
androidunity-game-engineandroid-ndkjava-native-interface

Using AAssetManager_fromJava within plugin not directly called from Java VM (called from Unity)


I'm using Android NDK and need access to assets. A requirement for asset access seems to be obtaining an AssetManager reference.

Looking at the NDK samples (https://github.com/android/ndk-samples), the pattern seems to be:

  • A JNIEnv* is passed into the func when called directly from the JavaVM, along with some jobject
  • Use these to get AAssetManager* and then use this to open assets

That seems simple enough, except in my case, the functions are being called from Unity so I don't have access to either a JNIEnv* or jobject. Getting the JNIEnv* seems easy enough as I can make use of JNI_OnLoad to get access to a JavaVM* and then use that to get a JNIEnv* via vm->GetEnv. My questions about this are:

1) My understanding is that, an Android app can only have one instance of a Java VM. Am I safe to take the JavaVM* passed into JNI_OnLoad and save it for use in other function calls?

2) What about the JNIEnv*? Can I grab that once during JNI_OnLoad and save it, or should I grab a fresh one every time I need to use assets within a function? Is JNIEnv* something I need to explicitly free? (i.e. what's the lifetime/ownership situation with JNIEnv*?)

3) AAssetManager_fromJava also requires a jobject with the documentation (https://developer.android.com/ndk/reference/group/asset#group___asset_1gadfd6537af41577735bcaee52120127f4) saying: "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.". I've seem some examples that simply pass in an empty (native) string like AAssetManager_fromJava(env, ""); - is that ok? I'd only be using the AssetManager for the lifetime of that call, and I could get a fresh one each time. (Again, is AAssetManager* a resource I need to manage, or am I just getting a reference to something owned elsewhere? The documentation seems to imply the latter.)

4) So given all the above, I'd probably do something like:

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...

}

Is that reasonable? No memory/resource leak issues? Any issues with multi-threading?

Thanks!

EDIT: Seems like there are some threading considerations with JNIEnv*. See: Unable to get JNIEnv* value in arbitrary context


Solution

  • Point-by point answer to your questions:

    1. Yes, there can be only one VM in Android. You are allowed to store this pointer or use JNI_GetCreatedJavaVMs.

    2. JNIEnv pointers are tightly coupled to the thread they were created on. In your situation you will first have to attach the thread to the VM using AttachCurrentThread. This will fill in a JNIEnv * for you. Don't forget to DetachCurrentThread when you're done.

      Also note the caveat about FindClass: you need to look up classes from the main thread or via the classloader of a class you looked up in the main thread.

    3. The implementation of AAssetmanager_fromJava is pretty clear: passing it anything other than an AssetManager object is undefined behavior. This answer shows one approach to grabbing the asset manager, another might be to call your own JNI function with a reference to the AssetManager object. Make sure to keep a global reference so it does not get GCed.

    4. Given the above, it would probably look more like this:

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