View on GitHub

JNI_doc

Some JNI memo docs

实现jni头文件

本文档主要介绍如何实现jni头文件,包括JNI函数宏,JNI数据类型,Java,JNI和C/C++之间的数据传递; JNI调用JVM,动态注册本地方法等等。 本文档里提到的JNI函数的更详细说明请查看:JNI函数手册

Java调用JNI

通过头文件实现jni方法

JNI_design中的本地方法链接部分,讲到java程序如何链接到本地方法——名字协议。 所以头文件并非是必要的,但是在头文件下实现本地方法,方向更明确,更不容易出错,也更符合C/C++风格。

这是实现jni的标准方法具体做法:

通过javah命令生成的头文件,看起来是这样的

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_testjni_MainActivity */

#ifndef _Included_com_example_testjni_MainActivity
#define _Included_com_example_testjni_MainActivity
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_testjni_MainActivity
 * Method:    getJniStringA
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_example_testjni_MainActivity_getJniStringA
  (JNIEnv *, jclass);

/*
 * Class:     com_example_testjni_MainActivity
 * Method:    getJniStringB
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_example_testjni_MainActivity_getJniStringB
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

extern C

#ifdef __cplusplus        //如果程序里包含了C++,ze采用extern C声明
extern "C" {              //C++是C的超集,但是二者的编译器并不相同; 
#endif                    //当程序是混合代码时,编译C++代码用C++编译器
                          //编译C代码时,需要链接C的变量和函数的声明,
                          //按照C的编译规范编译相关代码。这样,就可以
                          //整个程序里混合使用C++和C了。
/*...*/
 
#ifdef __cplusplus
}
#endif

jni.h

jni.h文件可以在Android源码或者ndk目录下找到,它包含了很多宏,变量,数据结构和函数的定义。

JNIEXPORT和JNICALL

JNIEXPORT和JNICALL是jni.h里的宏定义,表示此函数是要被JNI调用的。

jstring

JNI里的String类型,定义在jni.h,还有其他很多类型的数据类型。它本质上是个jobject:

class _jobject {};

更多介绍查看后文。

关于ndk

可参考Ndk-build配置

直接实现jni方法

当然也可以不用头文件做中介,而直接实现jni方法,但是本地方法的原型需要仔细配置,比较容易出错。

动态注册本地方法

根据jni方法的命名规范,将导致冗长的方法命名,使用起来也不方便。可以不使用这种链接本地方法的规范,而使用 动态注册的方式实现java中定义的函数,到本地函数的映射。

静态注册与动态注册

通过——java函数原型,生成标准jni头文件,实现头文件,编译加载库,java运行时链接本地方法,完成调用——这样的方式使用 JNI,由jni本身去保证java方法和本地方法的链接,这样的本地方法的注册方式成为静态注册; 而直接从java函数原型映射到本地方法,编译加载库,用额外的操作保证java函数原型链接到本地方法,完成调用——这样的方 式使用jni,本地方法的注册方式称为动态注册。其中“额外的操作”要由我们完成。

java函数原型

假设在java文件里声明了一个函数原型:

private static native String nativeGetJniStringD();

我们想让它直接映射到一个同名的本地方法:

JNIEXPORT jstring JNICALL nativeGetJniStringD
  (JNIEnv * env, jobject klass) {
	return (*env)->NewStringUTF(env, "Call from Java_com_example_testjni_MainActivity_getJniStringD");
}

构造JNINativeMethod数组

首先我们要构造一个JNINativeMethod数组,JNINativeMethod在jni.h中定义:

typedef struct {
    const char* name;             //java函数原型的名字
    const char* signature;        //java函数原型的签名,用(参数类型)返回值类型的格式组织,比如"()Ljava/lang/String;",
                                  //具体介绍在jni数据结构里详细介绍
    void*       fnPtr;            //本地函数指针
} JNINativeMethod;

在本例中,可以这样写:

static JNINativeMethod gMethods[] = {
    /* name, signature, funcPtr */
    { "nativeGetJniStringD", "()Ljava/lang/String;", (void*) nativeGetJniStringD }
};

注册单个类里的本地方法

假设已经为单个类的本地方法建立了一个JNINativeMethod数组,可以这样去注册它:

/*
 * 为一个类注册所有的本地方法
 */
static int registerNativeMethods(JNIEnv * env, const char* className,
    JNINativeMethod* gMethods, int numMethods)
{
    jclass clazz = (*env)->FindClass(env, className);
    if (clazz == NULL) {
        return JNI_FALSE;
    }
    if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {
        return JNI_FALSE;
    }

    return JNI_TRUE;
}

为所有类注册本地方法

/*
 * 为所有java类注册本地方法,示例只有一个类
 */
static int registerNatives(JNIEnv * env)
{
    if (!registerNativeMethods(env,
           "com/example/testjni/MainActivity",            //类名,每类是不相同的
            gMethods, sizeof(gMethods) / sizeof(gMethods[0])))
        return JNI_FALSE;

    return JNI_TRUE;
}

实现JNI_onLoad方法

JNI_onLoad方法在jni.h中定义,它在java类调用System.LoadLibarary方法后调用,去注册本地方法。

/*
 *
 * 实现JNI_OnLoad 方法,此方法在System.LoadLibrary后调用
 */
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM * vm, void* reserved)
{
    JNIEnv* env = NULL;
    jint result = -1;

    if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        return result;
    }
    assert(env != NULL);

    if (!registerNatives(env)) {
        return result;
    }

    /* success -- return valid version number */
    result = JNI_VERSION_1_4;

    return result;
}

小结

实现和链接本地方法有三种方式:

三种方式其实互相独立,互不影响。所以在一个实现文件里,可以同时包含这三种实现方式。

JNI调用java

一个示例

假设有个java层定义了一个类:

package com.example.calljava;

public class Student {
	public static final String TAG = "Student";
	
	public String name;
	private long score;

	public Student(String name, long score) {
		this.name = name;
		this.score = score;
	}

	public long getScore() {
		return score;
	}

	public void setScore(long score) {
		this.score = score;
	}

	public static String getTag() {
		return TAG;
	}
	
	@Override
	public String toString() {
		return "Student [name=" + name + ", score=" + score + "]";
	}
}

类里面有public, private, static成员和方法,在JNI层去使用这个类:

void jniCallJava(JNIEnv * env) {
	jclass cls = NULL;
	jmethodID mid = NULL;
	jfieldID fid = NULL;
	jobject obj = NULL;
	jstring arg = NULL;
	jstring ret = NULL;

	//创建一个con.example.calljava.Student类的引用
	cls = (*env)->FindClass(env, "com/example/calljava/Student");
	//获取此类的构造函数
	mid = (*env)->GetMethodID(env, cls, "<init>", "(Ljava/lang/String;J)V");
	//创建一个实例
	if (cls != NULL && mid != NULL) {
		arg = (*env)->NewStringUTF(env, "Jack");
		obj = (*env)->NewObject(env, cls, mid, arg, 100L);
	}

	fid = (*env)->GetFieldID(env, cls, "name", "Ljava/lang/String;");
	if (fid != 0) {
		//访问Student的name共有成员
		ret = (jstring)(*env)->GetObjectField(env, obj, fid);

		//修改Student的那么有成员
		arg = (*env)->NewStringUTF(env, "icejoywoo");
		(*env)->SetObjectField(env, obj, fid, arg);
	}

	//通过getter访问Student的score成员
	mid = (*env)->GetMethodID(env, cls, "getScore", "()J");
	if (mid != 0) {
		jlong score = (*env)->CallLongMethod(env, obj, mid);
	}
	//通过setter方法修改Student的score成员
	mid = (*env)->GetMethodID(env, cls, "setScore", "(J)V");
	if (mid != 0) {
		jlong math_score = 99;
		(*env)->CallVoidMethod(env, obj, mid, math_score);
	}

	//调用toString方法
	mid = (*env)->GetMethodID(env, cls, "toString", "()Ljava/lang/String;");
	if (mid != 0) {
		ret = (jstring)(*env)->CallObjectMethod(env, obj, mid);
	}

	fid = (*env)->GetStaticFieldID(env, cls, "TAG", "Ljava/lang/String;");
	if (fid != 0) {
		//访问Student的TAG静态共有成员
		ret = (jstring)(*env)->GetStaticObjectField(env, cls, fid);
	}

	//调用静态共有方法
	mid = (*env)->GetStaticMethodID(env, cls, "getTag", "()Ljava/lang/String;");
	if (mid != 0) {
		ret = (jstring)(*env)->CallStaticObjectMethod(env, cls, mid);
	}
}

创建对象

创建java对象,就是要创建一个jobject实例。关于jobject将在jni数据结构里介绍,它的本质是一个结构体指针。

访问public成员

访问public方法

访问public static成员

与访问public成员方式相同,只要将用到的jni方法换成static版本的即可,比如GetFieldID换成GetStaticFieldID

访问public static方法

与访问public成员方式相同,只要将用到的jni方法换成static版本的即可