Android using JNI without static variables

2019-07-21 06:37发布

问题:

I created my own version of Android's MediaMetadataRetriever using the source code for MediaMetadataRetriever.java as the basis. My version uses FFmpeg to retrieve the metadata. This approach works however it relies on static variables in the C code to retain state in between JNI calls. This means I can only use one instance of this class at a time or the state can get corrupted. The two Java functions are defined as follows:

public class MediaMetadataRetriever
{
    static {
        System.loadLibrary("metadata_retriever_jni");
    }

    public MediaMetadataRetriever() {

    }

    public native void setDataSource(String path) throws IllegalArgumentException;
    public native String extractMetadata(String key);

}

The corresponding C (JNI) code code is:

const char *TAG = "Java_com_example_metadataexample_MediaMetadataRetriever";
static AVFormatContext *pFormatCtx = NULL;

JNIEXPORT void JNICALL
Java_com_example_metadataexample_MediaMetadataRetriever_setDataSource(JNIEnv *env, jclass obj, jstring jpath) {

    if (pFormatCtx) {
        avformat_close_input(&pFormatCtx);
    }

    char duration[30] = "0";
    const char *uri;

    uri = (*env)->GetStringUTFChars(env, jpath, NULL);

    if (avformat_open_input(&pFormatCtx, uri, NULL, NULL) != 0) {
        __android_log_write(ANDROID_LOG_INFO, TAG, "Metadata could not be retrieved");
        (*env)->ReleaseStringUTFChars(env, jpath, uri);
        jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
        return;
    }

    if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
        __android_log_write(ANDROID_LOG_INFO, TAG, "Metadata could not be retrieved");
        avformat_close_input(&pFormatCtx);
        (*env)->ReleaseStringUTFChars(env, jpath, uri);
        jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
        return;
    }

    (*env)->ReleaseStringUTFChars(env, jpath, uri);
}

JNIEXPORT jstring JNICALL
Java_com_example_metadataexample_MediaMetadataRetriever_extractMetadata(JNIEnv *env, jclass obj, jstring jkey) {

    const char *key;
    jstring value = NULL;

    key = (*env)->GetStringUTFChars(env, jkey, NULL) ;

    if (!pFormatCtx) {
        goto fail;
    }

    if (key) {
        if (av_dict_get(pFormatCtx->metadata, key, NULL, AV_DICT_IGNORE_SUFFIX)) {
            value = (*env)->NewStringUTF(env, av_dict_get(pFormatCtx->metadata, key, NULL, AV_DICT_IGNORE_SUFFIX)->value);
        }
    }

    fail:
    (*env)->ReleaseStringUTFChars(env, jkey, key);

    return value;
}

Sample usage that outlines my issue would be:

MediaMetadataRetriever mmr = new MediaMetadataRetriever();
mmr.setDataSource("one.mp3");

MediaMetadataRetriever mmr2 = new MediaMetadataRetriever();
// This line resets the data source to two.mp3
mmr2.setDataSource("two.mp3");

// should retrieve the artist from one.mp3 but retrieves it from two.mp3 due to the static
// variable being reset in the previous statement       
String artist = mmr.extractMetadata(MediaMetadataRetriever.ARTIST);

Can someone explain how I would structure this code so I could use multiple instances of MediaMetadataRetriever without them interfering with one another? I don't want to switch the code to C++ and I'm fairly certain I don't need to modify MediaMetadataRetriever.java since this code is taken line-for-line from the Android framework (which allows multiple instances, see example below). It appears I need to re-structure the C code but I'm unsure how to retain state across JNI calls without using a static variable. Thanks in advance.

File file1 = new File(Environment.getExternalStorageDirectory(), "Music/one.mp3");
File file2 = new File(Environment.getExternalStorageDirectory(), "Music/two.mp3");

android.media.MediaMetadataRetriever mmr = new android.media.MediaMetadataRetriever();
mmr.setDataSource(file1.toString());

android.media.MediaMetadataRetriever mmr2 = new android.media.MediaMetadataRetriever();
mmr2.setDataSource(file2.toString());

// Returns the artist of one.mp3, not two.mp3, as expected. This is the expected behavior
// and confirms that multiple instances of MediaMetadataRetriever can be used simultaneously
mmr.extractMetadata(android.media.MediaMetadataRetriever.METADATA_KEY_ARTIST));

回答1:

Restructuring the code would be very straight forward. Instead of using pFormatCtx you simply extend the interfaces of the JNI calls so you can pass around the pointer you stored in pFormatCtx before. Now the big guestion is how to pass around the pointers while java doesn't know such a datatype? The most straight forward soulution would be to use ints (for 32 bit systems) or longs (for 64 bit systems) to passing pointers to and from the Java environment. Unfortunately you could get you a bit in hot water as soon as you switch between 64 and 32 bit versions of your library.

While I was trying to solve this problem some months ago I stumbled over an article of Clebert Suconic. He pointed out a very elegant way for passing pointers safely through JNI without "hacking" around with typecasting. Instead he proposes to use java.nio.ByteBuffer.

The concept put in a nutshell is: He suggest to create a new ByteBuffer object of length zero: env->NewDirectByteBuffer(myPointer, 0); and pass the resulting jobject through the JNI back and forth.

The call env->NewDirectByteBuffer(myPointer, 0); creates a imutable byte buffer object pointing to the location you wanted to pass around. The fact that the buffer is imutable is perfect as you don't want to modify the memory location, you only want to store the location itself. What you get is an object encapsulating your pointer and you can leave the pointer size issues to the JVM.

Edit: Just for completeness: The pointer can later be retrieved calling env->GetDirectBufferAddress(myPointer);.



回答2:

Add a long field (or int if you know you'll only ever be on a 32-bit system) to store the native pointer, and to dispose of it in your object's finalizer when the object is GC'd. From JNI, you can then save to that field the pointer obtained when you "open" your resource.

From the JNI manual:

Procedure for Accessing a Java Field

To get and set Java fields from a native method, you must do the following:

Obtain the identifier for that field from its class, name, and type signature. For example, in FieldAccess.c, we have:

fid = (*env)->GetStaticFieldID(env, cls, "si", "I");

and:

fid = (*env)->GetFieldID(env, cls, "s", "Ljava/lang/String;");

Use one of several JNI functions to either get or set the field specified by the field identifier. Pass the class to the appropriate static field access functions. Pass the object to the appropriate instance field access functions. For example, in FieldAccess.c, we have:

si = (*env)->GetStaticIntField(env, cls, fid);

and:

jstr = (*env)->GetObjectField(env, obj, fid);

Similar to calling a Java method, we factor out the cost of field lookup using a two-step process. The field ID uniquely identifies a field in a given class. Similar to method IDs, a field ID remains valid until the class from which it is derived is unloaded. Field Signatures

Field signatures are specified following the same encoding scheme as method signatures. The general form of a field signature is:

"field type"

The field signature is the encoded symbol for the type of the field, enclosed in double quotes (""). The field symbols are the same as the argument symbols in the method signature. That is, you represent an integer field with "I", a float field with "F", a double field with "D", a boolean field with "Z", and so on.

The signature for a Java object, such as a String, begins with the letter L, followed by the fully-qualified class for the object, and terminated by a semicolon (;). Thus, you form the field signature for a String variable (c.s in FieldAccess.java) as follows:

"Ljava/lang/String;"

Arrays are indicated by a leading square bracket ([) followed by the type of the array. For example, you designate an integer array as follows:

"[I"

Refer to the table in previous section which summarizes the encoding for the Java type signatures and their matching Java types.

You can use javap with option "-s" to generate the field signatures from class files. For example, run:

> javap -s -p FieldAccess

This gives you output containing:

... 
static si I 
s Ljava/lang/String; 
...