Android使用没有静态变量的JNI

时间:2013-02-19 22:13:09

标签: android ffmpeg java-native-interface

我使用MediaMetadataRetriever.java的源代码作为基础,创建了我自己的Android版MediaMetadataRetriever。我的版本使用FFmpeg来检索元数据。这种方法有效,但它依赖于C代码中的静态变量来保持JNI调用之间的状态。这意味着我一次只能使用此类的一个实例,否则状态可能会被破坏。两个Java函数定义如下:

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

}

相应的C(JNI)代码是:

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

概述我的问题的示例用法是:

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

有人可以解释我将如何构建此代码,以便我可以使用MediaMetadataRetriever的多个实例而不会相互干扰吗?我不想将代码切换到C ++,我很确定我不需要修改MediaMetadataRetriever.java,因为这段代码是从Android框架逐行进行的(允许多个实例,请参见下面的示例) )。看来我需要重新构造C代码,但我不确定如何在不使用静态变量的情况下跨JNI调用保留状态。提前谢谢。

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

2 个答案:

答案 0 :(得分:3)

重构代码非常简单。您只需扩展JNI调用的接口,而不是使用pFormatCtx,这样您就可以传递存储在pFormatCtx中的指针。现在最重要的是如何传递指针,而java不知道这样的数据类型?最直接的解决方案是使用int(对于32位系统)或long(对于64位系统)来传递指向Java环境的指针。不幸的是,只要在库的64位和32位版本之间切换,就可以让你有点热水。

几个月前,当我试图解决这个问题时,我偶然发现了一篇Clebert Suconic的文章。他指出了一种非常优雅的方式,可以安全地通过JNI指针,而不会在类型转换时“乱砍”。相反,他建议使用java.nio.ByteBuffer

简而言之,概念是:他建议创建一个长度为零的新ByteBuffer对象:env->NewDirectByteBuffer(myPointer, 0);并通过JNI来回传递生成的jobject。

调用env->NewDirectByteBuffer(myPointer, 0);创建一个指向您想要传递的位置的可忽略字节缓冲区对象。缓冲区是不可变的这一事实是完美的,因为您不想修改内存位置,您只想存储位置本身。你得到的是一个封装指针的对象,你可以将指针大小问题留给JVM。

编辑:为了完整性:以后可以通过调用env->GetDirectBufferAddress(myPointer);检索指针。

答案 1 :(得分:0)

添加一个长字段(如果你知道你只会在32位系统上,则为int)来存储本机指针,并在对象为GC时将其丢弃在对象的终结器中。从JNI,您可以将“打开”资源时获得的指针保存到该字段。

来自JNI手册:

  

访问Java字段的过程

     

要从本机方法获取和设置Java字段,您必须执行此操作   以下内容:

     

从其类,名称和类型中获取该字段的标识符   签名。例如,在FieldAccess.c中,我们有:

     

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

     

和:

     

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

     

使用其中一个   几个JNI函数来获取或设置由指定的字段   字段标识符。将类传递给适当的静态字段   访问功能。将对象传递给适当的实例字段   访问功能。例如,在FieldAccess.c中,我们有:

     

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

     

     

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

     

与调用Java类似   方法,我们使用两步分解字段查找的成本   处理。字段ID唯一标识给定类中的字段。   与方法ID类似,字段ID在类之前保持有效   它的派生是卸载的。字段签名

     

字段签名遵循与之相同的编码方案   方法签名。字段签名的一般形式是:

     

"field type"

     

字段签名是类型的编码符号   字段,用双引号括起来(“”)。字段符号是   与方法签名中的参数符号相同。那是你   表示带有“I”的整数字段,带有“F”的浮点字段,双精度数   带“D”的字段,带“Z”的布尔字段,依此类推。

     

Java对象的签名(如String)以   字母L,后跟对象的完全限定类,和   以分号(;)结束。因此,您形成了字段签名   一个String变量(FieldAccess.java中的c.s)如下:

     

"Ljava/lang/String;"

     

阵列由前方括号表示   ([]后跟数组的类型。例如,您指定一个   整数数组如下:

     

"[I"

     

请参阅上一节中总结的表格   Java类型签名的编码及其匹配的Java类型。

     

您可以使用带有选项“-s”的javap来生成字段签名   来自类文件。例如,运行:

     

> javap -s -p FieldAccess

     

这将为您提供包含以下内容的输出:

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