JNI使用小结

以下内容主要参考深入理解Android卷1.

JNI函数动态注册:

使用这个数据结构存储一个JNI函数的注册信息:

typedef struct {

   //Java中native函数的名字,不用携带包的路径。例如“native_init“。

constchar* name;    

//Java函数的签名信息,用字符串表示,是参数类型和返回值类型的组合。

    const char* signature;

   void*       fnPtr;  //JNI层对应函数的函数指针,注意它是void*类型。

} JNINativeMethod;

从以上成员看出java中声明的native函数好像可以对应上该jni文件中的函数,但是实际上jni函数的在这个结构上是唯一的,但是java声明的不是,因为不带包路径,只是一个函数名,怎么知道这个函数是哪个类的呢,如果其他java文件也有这样一个同名的native声明呢?其实在注册时,会去获取对应的java类对象(Class类型的对象)。

为JNI动态库中的所有JNI函数建立一个JNINativeMethod数组,注册就用这个数组作为参数。示例:

static JNINativeMethod g_methods[] = {
    {"nativeSetBlueLightFilterStrength", "(I)Z", (void*)nativeSetBlueLightFilterStrength},
    {"nativeGetBlueLightFilterStrength", "()I", (void*)nativeGetBlueLightFilterStrength},
    {"nativeEnableBlueLightFilter", "(Z)Z", (void*)nativeEnableBlueLightFilter},
    {"nativeIsBlueLightFilterEnabled", "()Z", (void*)nativeIsBlueLightFilterEnabled},
    {"nativeGetBlueLightFilterStrengthRange", "()I", (void*)nativeGetBlueLightFilterStrengthRange},
    {"nativeBlueLightFilterInit", "()Z", (void*)nativeBlueLightFilterInit},
};

JNI函数签名:

 

JNI规范定义的函数签名信息格式:

(参数1类型标示参数2类型标示...参数n类型标示)返回值类型标示

比较繁琐,具体编码时,读者可以定义字符串宏

类型标示示意表

类型标示

Java类型

类型标示

Java类型

Z

boolean

F

float

B

byte

D

double

C

char

Ljava/lang/String;

String

S

short

[I

int[]

I

int

[Lava/lang/object;

Object[]

J

long

 

 

上面列出了一些常用的类型标示,如果Java类型是数组,则标示中会有一个“[”,另外,引用类型(除基本类型的数组外)的标示最后都有一个“;”,返回值签名时也是要加分号的

 函数签名小例子

函数签名

Java函数

“()Ljava/lang/String;”

String f()

“(ILjava/lang/Class;)J”

long f(int i, Class c)

“([B)V”

void f(byte[] bytes)

 

当Java层通过System.loadLibrary加载完JNI动态库后,紧接着会查找该库中一个叫JNI_OnLoad的函数,如果有,就调用它,而动态注册的工作就是在这里完成的。
 

在JNI_OnLoad函数中注册会用到一下两行代码:

/*

env指向一个JNIEnv结构体,classname为对应的Java类名(全路径名,包名+类名),由于

JNINativeMethod中使用的函数名并非全路径名,所以要指明是哪个类。

*/

jclass clazz =  (*env)->FindClass(env, className);

//调用JNIEnv的RegisterNatives函数,注册关联关系。

(*env)->RegisterNatives(env, clazz, gMethods,numMethods);

 

示例:

 

/*
 * Register several native methods for one class.
 */
static int registerNativeMethods(JNIEnv* env, const char* className,
    JNINativeMethod* gMethods, int numMethods)
{
    jclass clazz;

    clazz = env->FindClass(className);
    if (clazz == NULL) {
        ALOGE("Native registration unable to find class '%s'", className);
        return JNI_FALSE;
    }
    if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
        ALOGE("RegisterNatives failed for '%s'", className);
        return JNI_FALSE;
    }

    return JNI_TRUE;
}

// ----------------------------------------------------------------------------

/*
 * This is called by the VM when the shared library is first loaded.
 */

jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
//该函数的第一个参数类型为JavaVM,这可是虚拟机在JNI层的代表喔,每个Java进程只有一个
 JNIEnv* env = NULL;
    jint result = -1;

    UNUSED(reserved);

    ALOGI("JNI_OnLoad");

    if (JNI_OK != vm->GetEnv((void **)&env, JNI_VERSION_1_4)) {
        ALOGE("ERROR: GetEnv failed");
        goto bail;
    }

    if (!registerNativeMethods(env, classPathName, g_methods, sizeof(g_methods) / sizeof(g_methods[0]))) {
        ALOGE("ERROR: registerNatives failed");
        goto bail;
    }
//必须返回这个值,否则会报错。
    result = JNI_VERSION_1_4;

bail:
    return result;
}

静态注册:

extern "C" JNIEXPORT jstring
JNICALL
Java_com_champion_ndkhelloword_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

必须带上extern "C",但是JNIEXPORT和JNICALL是可以带也可以不带上。不影响程序执行,只是标记作用。动态注册则不用带上extern "C"。开始猜想可能是因为动态注册时带上了签名,包含了参数类型,这就符合了C++对重载函数的描述要求,而静态注册则只会根据名字查找。但是后面实践证明,无论是动态注册还是静态注册都不允许JNI函数重载

在一个Java文件中,允许部分native函数是静态注册,而部分是动态注册的。

-----------------------------------------------------------------------------------------------

JNI层代码中一般要包含jni.h这个头文件。Android源码中提供了一个帮助头文件JNIHelp.h,它内部其实就包含了jni.h,所以我们在自己的代码中直接包含这个JNIHelp.h即可。

 

头文件路径可以通过在Android.mk中指定:

LOCAL_C_INCLUDES := $(JNI_H_INCLUDE) \
        $(MTK_PATH_SOURCE)/hardware/pq/mt8163/inc

java调用native函数时,可以传递任意java类型的对象。在jni函数中,并不能识别具体的对象类型,jni函数通过类似java反射的技术调用java层传过来的对象的成员和方法。

 

2. 数据类型转换

通过前面的分析,解决了JNI函数的注册问题。下面来研究数据类型转换的问题。

在Java中调用native函数传递的参数是Java数据类型,那么这些参数类型到了JNI层会变成什么呢?

Java数据类型分为基本数据类型和引用数据类型两种,JNI层也是区别对待这二者的。先来看基本数据类型的转换。

(1)基本类型的转换

基本类型的转换很简单,可用表2-1表示:

表2-1  基本数据类型转换关系表

Java

Native类型

符号属性

字长

boolean

jboolean

无符号

8位

byte

jbyte

无符号

8位

char

jchar

无符号

16位

short

jshort

有符号

16位

int

jint

有符号

32位

long

jlong

有符号

64位

float

jfloat

有符号

32位

double

jdouble

有符号

64位

上面列出了Java基本数据类型和JNI层数据类型对应的转换关系,非常简单。不过,应务必注意,转换成Native类型后对应数据类型的字长,例如jchar在Native语言中是16位,占两个字节,这和普通的char占一个字节的情况完全不一样。上面的基本类型除了注意类型所占字节长度外,在jni中可以混合使用,不会报错。如一个JNIEnv中的方法要求jint类型的,可以直接传一个int类型的,native方法返回类型是jint的,可以直接返回int。

接下来看Java引用数据类型的转换。

(2)引用数据类型的转换

引用数据类型的转换如表2-2所示:

表2-2  Java引用数据类型转换关系表

Java引用类型

Native类型

Java引用类型

Native类型

All objects

jobject

char[]

jcharArray

java.lang.Class实例

jclass

short[]

jshortArray

java.lang.String实例

jstring

int[]

jintArray

Object[]

jobjectArray

long[]

jlongArray

boolean[]

jbooleanArray

float[]

floatArray

byte[]

jbyteArray

double[]

jdoubleArray

java.lang.Throwable实例

jthrowable

 

 

由上表可知:

·  除了Java中基本数据类型的数组、Class、String和Throwable外,其余所有Java对象的数据类型在JNI中都用jobject表示。

 

JNIEnv是一个和线程相关的,代表JNI环境的结构体。

 

JNIEnv实际上就是提供了一些JNI系统函数。通过这些函数可以做到:

·  调用Java的函数。

·  操作jobject对象等很多事情。

JNIEnv,是一个和线程有关的变量。也就是说,线程A有一个JNIEnv,线程B有一个JNIEnv。由于线程相关,所以不能在线程B中使用线程A的JNIEnv结构体。读者可能会问,JNIEnv不都是native函数转换成JNI层函数后由虚拟机传进来的吗?使用传进来的这个JNIEnv总不会错吧?是的,在这种情况下使用当然不会出错。不过当后台线程收到一个网络消息,而又需要由Native层函数主动回调Java层函数时,JNIEnv是从何而来呢?根据前面的介绍可知,我们不能保存另外一个线程的JNIEnv结构体,然后把它放到后台线程中来用。这该如何是好?

还记得前面介绍的那个JNI_OnLoad函数吗?它的第一个参数是JavaVM,它是虚拟机在JNI层的代表,代码如下所示:

//全进程只有一个JavaVM对象,所以可以保存,任何地方使用都没有问题。

jint JNI_OnLoad(JavaVM* vm, void* reserved)

正如上面代码所说,不论进程中有多少个线程,JavaVM却是独此一份,所以在任何地方都可以使用它。那么,JavaVM和JNIEnv又有什么关系呢?答案如下:

·  调用JavaVM的AttachCurrentThread函数,就可得到这个线程的JNIEnv结构体。这样就可以在后台线程中回调Java函数了。

·  另外,后台线程退出前,需要调用JavaVM的DetachCurrentThread函数来释放对应的资源。

 

操作对象jobject

成员变量和成员函数是由类定义的,它是类的属性,所以在JNI规则中,用jfieldID 和jmethodID 来表示Java类的成员变量和成员函数,它们通过JNIEnv的下面两个函数可以得到:

jfieldID GetFieldID(jclass clazz,const char*name, const char *sig);

jmethodID GetMethodID(jclass clazz, const char*name,const char *sig);

其中,jclass代表Java类,name表示成员函数或成员变量的名字,sig为这个函数和变量的签名信息。如前所示,成员函数和成员变量都是类的信息,这两个函数的第一个参数都是jclass。

调用method:

NativeType CallMethod(JNIEnv *env,jobject obj,jmethodID methodID, ...)。

示例:

/*

调用JNIEnv的CallVoidMethod函数,注意CallVoidMethod的参数:

第一个是代表MediaScannerClient的jobject对象,

第二个参数是函数scanFile的jmethodID,后面是Java中scanFile的参数。

*/

       mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr,

lastModified, fileSize);

获取成员:

 

//获得fieldID后,可调用GetField系列函数获取jobject对应成员变量的值。

NativeType GetField(JNIEnv *env,jobject obj,jfieldID fieldID)

//或者调用SetField系列函数来设置jobject对应成员变量的值。

void SetField(JNIEnv *env,jobject obj,jfieldID fieldID,NativeType value)

//下面我们列出一些参加的Get/Set函数。

GetObjectField()         SetObjectField()//jstring,jobject,jthrow等类型成员都可以使用该方法进行访问

GetBooleanField()         SetBooleanField()

GetByteField()           SetByteField()

GetCharField()           SetCharField()

GetShortField()          SetShortField()

GetIntField()            SetIntField()

GetLongField()           SetLongField()

GetFloatField()          SetFloatField()

GetDoubleField()                  SetDoubleField()

 

 

jstring介绍

获取Java对象中的String成员:

jfieldID fid = env->GetFieldID(clazz,"mStr","Ljava/lang/String;"); 
jstring js = (jstring)env->GetObjectField(instance,fid);

Java中的String也是引用类型,不过由于它的使用非常频繁,所以在JNI规范中单独创建了一个jstring类型来表示Java中的String类型。虽然jstring是一种独立的数据类型,但是它并没有提供成员函数供操作。相比而言,C++中的string类就有自己的成员函数了。那么该怎么操作jstring呢?还是得依靠JNIEnv提供的帮助。这里看几个有关jstring的函数:

·  调用JNIEnv的NewString(JNIEnv *env, const jchar*unicodeChars,jsize len),可以从Native的字符串得到一个jstring对象。其实,可以把一个jstring对象看成是Java中String对象在JNI层的代表,也就是说,jstring就是一个Java String。但由于Java String存储的是Unicode字符串,所以NewString函数的参数也必须是Unicode字符串。

·  调用JNIEnv的NewStringUTF将根据Native的一个UTF-8字符串得到一个jstring对象。在实际工作中,这个函数用得最多。

·  上面两个函数将本地字符串转换成了Java的String对象,JNIEnv还提供了GetStringChars和GetStringUTFChars函数,它们可以将Java String对象转换成本地字符串。其中GetStringChars得到一个Unicode字符串,而GetStringUTFChars得到一个UTF-8字符串。

·  另外,如果在代码中调用了上面几个函数,在做完相关工作后,就都需要调用ReleaseStringChars或ReleaseStringUTFChars函数对应地释放资源,否则会导致JVM内存泄露。这一点和jstring的内部实现有关系,读者写代码时务必注意这个问题。

为了加深印象,来看processFile是怎么做的:

[-->android_media_MediaScanner.cpp]

static void

android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz, jstring path, jstring mimeType, jobject client)

{

   MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz,fields.context);

......

//调用JNIEnv的GetStringUTFChars得到本地字符串pathStr

    constchar *pathStr = env->GetStringUTFChars(path, NULL);

......

//使用完后,必须调用ReleaseStringUTFChars释放资源

   env->ReleaseStringUTFChars(path, pathStr);

    ......

}

 

垃圾回收

 

 

我们知道,Java中创建的对象最后是由垃圾回收器来回收和释放内存的,可它对JNI有什么影响呢?下面看一个例子:

[-->垃圾回收例子]

static jobject save_thiz = NULL; //定义一个全局的jobject

static void

android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz, jstring path,

 jstringmimeType, jobject client)

{

  //保存Java层传入的jobject对象,代表MediaScanner对象

save_thiz = thiz;

return;

}

//假设在某个时间,有地方调用callMediaScanner函数

void callMediaScanner()

{

  //在这个函数中操作save_thiz,会有问题吗?

}

因为和save_thiz对应的Java层中的MediaScanner很有可能已经被垃圾回收了,也就是说,save_thiz保存的这个jobject可能是一个野指针,如使用它,后果会很严重。

将一个引用类型进行赋值操作,它的引用计数不会增加,而垃圾回收机制只会保证那些没有被引用的对象才会被清理。但如果在JNI层使用下面这样的语句,是不会增加引用计数的。

save_thiz = thiz; //这种赋值不会增加jobject的引用计数。

JNI规范已很好地解决了这一问题,JNI技术一共提供了三种类型的引用,它们分别是:

·  Local Reference:本地引用。在JNI层函数中使用的非全局引用对象都是Local Reference。它包括函数调用时传入的jobject、在JNI层函数中创建的jobject。LocalReference最大的特点就是,一旦JNI层函数返回,这些jobject就可能被垃圾回收。

·  Global Reference:全局引用,这种对象如不主动释放,就永远不会被垃圾回收。

·  Weak Global Reference:弱全局引用,一种特殊的GlobalReference,在运行过程中可能会被垃圾回收。所以在程序中使用它之前,需要调用JNIEnv的IsSameObject判断它是不是被回收了。

平时用得最多的是Local Reference和Global Reference,下面看一个实例,代码如下所示:

[-->android_media_MediaScanner.cpp::MyMediaScannerClient构造函数]

 MyMediaScannerClient(JNIEnv *env, jobjectclient)

       :   mEnv(env),

        //调用NewGlobalRef创建一个GlobalReference,这样mClient就不用担心被回收了。

           mClient(env->NewGlobalRef(client)),

           mScanFileMethodID(0),

           mHandleStringTagMethodID(0),

           mSetMimeTypeMethodID(0)

{

  ......

}

//析构函数

virtual ~MyMediaScannerClient()

{

  mEnv->DeleteGlobalRef(mClient);//调用DeleteGlobalRef释放这个全局引用。

 }

每当JNI层想要保存Java层中的某个对象时,就可以使用Global Reference,使用完后记住释放它就可以了。

-----------------------------------------------------------------------------------------------------------------------------------

创建对象:注意构造函数名为


	jclass jc = env->FindClass("com/smile/jnitest/JNITest$TestPoint");
	//获取构造函数ID(构造函数函数名为,第三个参数为签名,意指传参和返回的对应缩写)
	jmethodID mid = env->GetMethodID(jc,"","()V");
	jobject testPoint = env->NewObject(jc,mid );

----------------------------------------------------------------------------------------------------------------------------------------

JNI 的 call<>method 与 callNonVirtual<>method

nonVirtual应该就是忽略了继承的多态性,即访问的方法,不是实例实际类型的方法,而是引用类型的方法。下面是一个类型:

public class Father {

	@Override
	public void fun() {
		// TODO Auto-generated method stub
		Log.d("333", "Father involked");
	}

}
public class Child extends Father{
	@Override
	public void fun() {
		// TODO Auto-generated method stub
		Log.d("333", "Child involked");
	}
}

在如果定义

Father instance = new Child();

在C++中如下调用

jobject fObj = env->GetObjectField(obj,fID);
jclass fclass=env->FindClass("lc/test/jni/Father");
jmethodID fm= env->GetMethodID(fclass,"fun","()V");
env->CallNonvirtualVoidMethod(fObj,fclass,fm);

Calling Instance Methods of a Superclass(调用实例超类的方法)

You can call instance methods defined in a superclass that have been overridden in the class to which the object belongs. The JNI provides a set of CallNonvirtualMethod functions for this purpose. To call instance methods from the superclass that defined them, you do the following:

  • Obtain the method ID from the superclass using GetMethodID, as opposed to GetStaticMethodID).
  • Pass the object, superclass, method Id, and arguments to the family of nonvirtual invocation functions: CallNonvirtualVoidMethodCallNonvirtualBooleanMethod, and so on.

It is rare that you will need to invoke the instance methods of a superclass. This facility is similar to calling a superclass method, say f, using:

super.f();

in Java.

 

顺带说下 FindClass( ) 和 getObjectClass( )的区别,因为在写上面的测试代码的时候我出过这个错

FindClass( ) 就是通过包名类名去找,这个相当于绝对路径吧

getObjectClass( ) 是通过一个obj的类型去找,这个地方需要注意的是,他是通过对象类型去找,不是通过引用类型去找

比如一开始在java 里面写了

 

Father p=new Child();

那么在C++里面获得这个obj之后,如果用GetObjectClass( ) 获得的就是 Child 的 Class , 不是 Father 的 Class

 -------------------------------------------------------------------------------------------------------------------------------------------------------------------

GetStringUTFChars/ReleaseStringUTFChars和GetArrayElements/ReleaseArrayElements

其中GetStringUTFChars有一个isCopy的参数,代表是否复制一份在native堆,但是不知道有什么用,反正返回的都是一个const char*,就是说无法改变其内容的指针。这个const char*在被调用ReleaseStringUTFChars前有效。

GetArrayElements也有个isCopy参数,返回的是jbyte*,如果isCopy为JNI_TRUE,那么这个copy而来的buf是可以修改的,而且修改后的数据可以通过ReleaseArrayElements覆盖java堆中的数据,是否覆盖决定于ReleaseArrayElements的mode参数:

mode actions
0 copy back the content and free the elems buffer
JNI_COMMIT copy back the content but do not free the elems buffer
JNI_ABORT free the buffer without copying back the possible changes

 经过实践证明,无论GetArrayElements是否使用isCopy是JNI_FALSE还是JNI_TRUE还是NULL,对是否把修改覆盖JAVA堆中的对象都没有关系,只跟ReleaseArrayElements时使用的mode有关,而且必须使用ReleaseArrayElements才会把修改覆盖Java堆。COMMIT和0都将覆盖Java堆,而JNI_ABORT则不会。

-----------------------------------------------------------------------------------------------------------------------------------------------------------------

 

JNI参考指南

 

本人另一篇可以用于学习jni的文章:SQLite--SQLiteDatabase、SQLiteOpenHelper、sqlite3.c--(jni、头文件)--源码分析基于Android M

 

 

你可能感兴趣的:(android系统相关)